Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node/EVM: Verify EVM chain ID #4116

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions node/pkg/watchers/evm/watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"math"
"math/big"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
Expand All @@ -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"
)

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.env == common.UnsafeDevNet {
// 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
Expand Down
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
Expand Up @@ -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
Expand Up @@ -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,
}
Loading