Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Node/EVM: Verify EVM chain ID
Browse files Browse the repository at this point in the history
bruce-riley committed Jan 28, 2025
1 parent c888252 commit dc63747
Showing 5 changed files with 250 additions and 2 deletions.
51 changes: 49 additions & 2 deletions node/pkg/watchers/evm/watcher.go
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ import (
"fmt"
"math"
"math/big"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
@@ -28,6 +30,7 @@ import (
"github.com/certusone/wormhole/node/pkg/query"
"github.com/certusone/wormhole/node/pkg/readiness"
"github.com/certusone/wormhole/node/pkg/supervisor"
"github.com/wormhole-foundation/wormhole/sdk"
"github.com/wormhole-foundation/wormhole/sdk/vaa"
)

@@ -211,14 +214,18 @@ func (w *Watcher) Run(parentCtx context.Context) error {
ContractAddress: w.contract.Hex(),
})

timeout, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
if err := w.verifyEvmChainID(ctx, logger); err != nil {
return fmt.Errorf("failed to verify evm chain id: %w", err)
}

finalizedPollingSupported, safePollingSupported, err := w.getFinality(ctx)
if err != nil {
return fmt.Errorf("failed to determine finality: %w", err)
}

timeout, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()

if finalizedPollingSupported {
if safePollingSupported {
logger.Info("polling for finalized and safe blocks")
@@ -794,6 +801,46 @@ func (w *Watcher) getFinality(ctx context.Context) (bool, bool, error) {
return finalized, safe, nil
}

// verifyEvmChainID reads the EVM chain ID from the node and verifies that it matches the expected value (making sure we aren't connected to the wrong chain).
func (w *Watcher) verifyEvmChainID(ctx context.Context, logger *zap.Logger) error {
timeout, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()

c, err := rpc.DialContext(timeout, w.url)
if err != nil {
return fmt.Errorf("failed to connect to endpoint: %w", err)
}

var str string
err = c.CallContext(ctx, &str, "eth_chainId")
if err != nil {
return fmt.Errorf("failed to read evm chain id: %w", err)
}

evmChainID, err := strconv.ParseUint(strings.TrimPrefix(str, "0x"), 16, 64)
if err != nil {
return fmt.Errorf(`eth_chainId returned an invalid int: "%s"`, str)
}

logger.Info("queried evm chain id", zap.Uint64("evmChainID", evmChainID))

if w.unsafeDevMode {
// In devnet we log the result but don't enforce it.
return nil
}

expectedEvmChainID, err := sdk.GetEvmChainID(string(w.env), w.chainID)
if err != nil {
return fmt.Errorf("failed to look up evm chain id: %w", err)
}

if evmChainID != uint64(expectedEvmChainID) {
return fmt.Errorf("evm chain ID miss match, expected %d, received %d", expectedEvmChainID, evmChainID)
}

return nil
}

// SetL1Finalizer is used to set the layer one finalizer.
func (w *Watcher) SetL1Finalizer(l1Finalizer interfaces.L1Finalizer) {
w.l1Finalizer = l1Finalizer
45 changes: 45 additions & 0 deletions sdk/evm_chain_ids.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package sdk

import (
"errors"
"strings"

"github.com/wormhole-foundation/wormhole/sdk/vaa"
)

var ErrInvalidEnv = errors.New("invalid environment")
var ErrNotFound = errors.New("not found")

// IsEvmChainID if the specified chain is defined as an EVM chain ID in the specified environment.
func IsEvmChainID(env string, chainID vaa.ChainID) (bool, error) {
var m *map[vaa.ChainID]int
if env == "prod" || env == "mainnet" {
m = &MainnetEvmChainIDs
} else if env == "test" || env == "testnet" {
m = &TestnetEvmChainIDs
} else {
return false, ErrInvalidEnv
}
_, exists := (*m)[chainID]
return exists, nil
}

// GetEvmChainID returns the expected EVM chain ID associated with the given Wormhole chain ID and environment passed it.
func GetEvmChainID(env string, chainID vaa.ChainID) (int, error) {
env = strings.ToLower(env)
if env == "prod" || env == "mainnet" {
return getEvmChainID(MainnetEvmChainIDs, chainID)
}
if env == "test" || env == "testnet" {
return getEvmChainID(TestnetEvmChainIDs, chainID)
}
return 0, ErrInvalidEnv
}

func getEvmChainID(evmChains map[vaa.ChainID]int, chainID vaa.ChainID) (int, error) {
id, exists := evmChains[chainID]
if !exists {
return 0, ErrNotFound
}
return id, nil
}
74 changes: 74 additions & 0 deletions sdk/evm_chain_ids_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package sdk

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/wormhole-foundation/wormhole/sdk/vaa"
)

func TestGetEvmChainID(t *testing.T) {
type test struct {
env string
input vaa.ChainID
output int
err error
}

// Note: Don't intend to list every chain here, just enough to verify `GetEvmChainID`.
tests := []test{
{env: "mainnet", input: vaa.ChainIDUnset, output: 0, err: ErrNotFound},
{env: "mainnet", input: vaa.ChainIDSepolia, output: 0, err: ErrNotFound},
{env: "mainnet", input: vaa.ChainIDEthereum, output: 1},
{env: "mainnet", input: vaa.ChainIDArbitrum, output: 42161},
{env: "testnet", input: vaa.ChainIDSepolia, output: 11155111},
{env: "testnet", input: vaa.ChainIDEthereum, output: 17000},
{env: "junk", input: vaa.ChainIDEthereum, output: 17000, err: ErrInvalidEnv},
}

for _, tc := range tests {
t.Run(tc.env+"-"+tc.input.String(), func(t *testing.T) {
evmChainID, err := GetEvmChainID(tc.env, tc.input)
if tc.err != nil {
assert.ErrorIs(t, tc.err, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.output, evmChainID)
}
})
}
}
func TestIsEvmChainID(t *testing.T) {
type test struct {
env string
input vaa.ChainID
output bool
err error
}

// Note: Don't intend to list every chain here, just enough to verify `GetEvmChainID`.
tests := []test{
{env: "mainnet", input: vaa.ChainIDUnset, output: false},
{env: "mainnet", input: vaa.ChainIDSepolia, output: false},
{env: "mainnet", input: vaa.ChainIDEthereum, output: true},
{env: "mainnet", input: vaa.ChainIDArbitrum, output: true},
{env: "mainnet", input: vaa.ChainIDSolana, output: false},
{env: "testnet", input: vaa.ChainIDSepolia, output: true},
{env: "testnet", input: vaa.ChainIDEthereum, output: true},
{env: "testnet", input: vaa.ChainIDTerra, output: false},
{env: "junk", input: vaa.ChainIDEthereum, output: true, err: ErrInvalidEnv},
}

for _, tc := range tests {
t.Run(tc.env+"-"+tc.input.String(), func(t *testing.T) {
result, err := IsEvmChainID(tc.env, tc.input)
if tc.err != nil {
assert.ErrorIs(t, tc.err, err)
} else {
require.NoError(t, err)
assert.Equal(t, tc.output, result)
}
})
}
}
38 changes: 38 additions & 0 deletions sdk/mainnet_consts.go
Original file line number Diff line number Diff line change
@@ -192,3 +192,41 @@ var KnownAutomaticRelayerEmitters = []struct {
{ChainId: vaa.ChainIDSnaxchain, Addr: "00000000000000000000000027428DD2d3DD32A4D7f7C497eAaa23130d894911"},
{ChainId: vaa.ChainIDWorldchain, Addr: "0000000000000000000000001520cc9e779c56dab5866bebfb885c86840c33d3"},
}

// Please keep these in chainID order.
var MainnetEvmChainIDs = map[vaa.ChainID]int{
vaa.ChainIDEthereum: 1,
vaa.ChainIDBSC: 56,
vaa.ChainIDPolygon: 137,
vaa.ChainIDAvalanche: 43114,
vaa.ChainIDOasis: 42262,
vaa.ChainIDAurora: 1313161554,
vaa.ChainIDFantom: 250,
vaa.ChainIDKarura: 686,
vaa.ChainIDAcala: 787,
vaa.ChainIDKlaytn: 8217,
vaa.ChainIDCelo: 42220,
vaa.ChainIDMoonbeam: 1284,
vaa.ChainIDArbitrum: 42161,
vaa.ChainIDOptimism: 10,
vaa.ChainIDGnosis: 100,
vaa.ChainIDBtc: 200901,
vaa.ChainIDBase: 8453,
vaa.ChainIDFileCoin: 314,
vaa.ChainIDRootstock: 30,
vaa.ChainIDScroll: 534352,
vaa.ChainIDMantle: 5000,
vaa.ChainIDBlast: 81457,
vaa.ChainIDXLayer: 196,
vaa.ChainIDLinea: 59144,
vaa.ChainIDBerachain: 80084,
vaa.ChainIDSeiEVM: 1329,
vaa.ChainIDEclipse: 17172,
vaa.ChainIDBOB: 60808,
vaa.ChainIDSnaxchain: 2192,
vaa.ChainIDUnichain: 130,
vaa.ChainIDWorldchain: 480,
vaa.ChainIDInk: 57073,
//vaa.ChainIDHyperEVM: 0, // TODO: Not in mainnet yet.
vaa.ChainIDMonad: 143,
}
44 changes: 44 additions & 0 deletions sdk/testnet_consts.go
Original file line number Diff line number Diff line change
@@ -102,3 +102,47 @@ var KnownTestnetAutomaticRelayerEmitters = []struct {
{ChainId: vaa.ChainIDOptimismSepolia, Addr: "00000000000000000000000093BAD53DDfB6132b0aC8E37f6029163E63372cEE"},
{ChainId: vaa.ChainIDBaseSepolia, Addr: "00000000000000000000000093BAD53DDfB6132b0aC8E37f6029163E63372cEE"},
}

// Please keep these in chainID order.
var TestnetEvmChainIDs = map[vaa.ChainID]int{
vaa.ChainIDEthereum: 17000, // This is actually the value for Holesky, since Goerli deprecated.
vaa.ChainIDBSC: 97,
vaa.ChainIDPolygon: 80001,
vaa.ChainIDAvalanche: 43113,
vaa.ChainIDOasis: 42261,
vaa.ChainIDAurora: 1313161555,
vaa.ChainIDFantom: 4002,
vaa.ChainIDKarura: 596,
vaa.ChainIDAcala: 597,
vaa.ChainIDKlaytn: 1001,
vaa.ChainIDCelo: 44787,
vaa.ChainIDMoonbeam: 1287,
vaa.ChainIDArbitrum: 421613,
vaa.ChainIDOptimism: 420,
vaa.ChainIDGnosis: 77,
vaa.ChainIDBtc: 2203,
vaa.ChainIDBase: 84531,
vaa.ChainIDFileCoin: 314159,
vaa.ChainIDRootstock: 31,
vaa.ChainIDScroll: 534353,
vaa.ChainIDMantle: 5003,
vaa.ChainIDBlast: 168587773,
vaa.ChainIDXLayer: 195,
vaa.ChainIDLinea: 59141,
vaa.ChainIDBerachain: 80084,
vaa.ChainIDSeiEVM: 713715,
vaa.ChainIDEclipse: 555666,
vaa.ChainIDBOB: 808813,
vaa.ChainIDSnaxchain: 13001,
vaa.ChainIDUnichain: 1301,
vaa.ChainIDWorldchain: 4801,
vaa.ChainIDInk: 763373,
vaa.ChainIDHyperEVM: 998,
vaa.ChainIDMonad: 10143,
vaa.ChainIDSepolia: 11155111,
vaa.ChainIDArbitrumSepolia: 10003,
vaa.ChainIDBaseSepolia: 10004,
vaa.ChainIDOptimismSepolia: 10005,
vaa.ChainIDHolesky: 17000,
vaa.ChainIDPolygonSepolia: 10007,
}

0 comments on commit dc63747

Please sign in to comment.