From df41aeec95d0b76a470c6305af76bcd92ede9b7f Mon Sep 17 00:00:00 2001 From: Bruce Riley <bruce@wormholelabs.xyz> Date: Tue, 10 Sep 2024 13:54:07 -0500 Subject: [PATCH 1/3] Node/EVM: Verify EVM chain ID --- node/pkg/watchers/evm/watcher.go | 51 +++++++++++++++++++++- sdk/evm_chain_ids.go | 45 +++++++++++++++++++ sdk/evm_chain_ids_test.go | 74 ++++++++++++++++++++++++++++++++ sdk/mainnet_consts.go | 38 ++++++++++++++++ sdk/testnet_consts.go | 44 +++++++++++++++++++ 5 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 sdk/evm_chain_ids.go create mode 100644 sdk/evm_chain_ids_test.go diff --git a/node/pkg/watchers/evm/watcher.go b/node/pkg/watchers/evm/watcher.go index 643714b41a..1d72498886 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -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") @@ -793,6 +800,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 diff --git a/sdk/evm_chain_ids.go b/sdk/evm_chain_ids.go new file mode 100644 index 0000000000..ad9733cc09 --- /dev/null +++ b/sdk/evm_chain_ids.go @@ -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 +} diff --git a/sdk/evm_chain_ids_test.go b/sdk/evm_chain_ids_test.go new file mode 100644 index 0000000000..e6360f5fc1 --- /dev/null +++ b/sdk/evm_chain_ids_test.go @@ -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) + } + }) + } +} diff --git a/sdk/mainnet_consts.go b/sdk/mainnet_consts.go index 6fdfcdb7a1..1b014b58e5 100644 --- a/sdk/mainnet_consts.go +++ b/sdk/mainnet_consts.go @@ -194,3 +194,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, +} diff --git a/sdk/testnet_consts.go b/sdk/testnet_consts.go index a19e89d336..6ebd2ec751 100644 --- a/sdk/testnet_consts.go +++ b/sdk/testnet_consts.go @@ -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, +} From 070b11555957175d7198a906375562204b8f6338 Mon Sep 17 00:00:00 2001 From: Bruce Riley <bruce@wormholelabs.xyz> Date: Thu, 20 Feb 2025 09:12:27 -0500 Subject: [PATCH 2/3] Node/EVM: Chain ID verification redesign --- node/cmd/guardiand/node.go | 75 ++--- node/pkg/watchers/evm/chain_config.go | 274 ++++++++++++++++++ node/pkg/watchers/evm/chain_config_test.go | 183 ++++++++++++ .../evm/verify_chain_config/verify.go | 88 ++++++ node/pkg/watchers/evm/watcher.go | 115 +------- sdk/evm_chain_ids.go | 45 --- sdk/evm_chain_ids_test.go | 74 ----- sdk/mainnet_consts.go | 38 --- sdk/testnet_consts.go | 44 --- 9 files changed, 587 insertions(+), 349 deletions(-) create mode 100644 node/pkg/watchers/evm/chain_config.go create mode 100644 node/pkg/watchers/evm/chain_config_test.go create mode 100644 node/pkg/watchers/evm/verify_chain_config/verify.go delete mode 100644 sdk/evm_chain_ids.go delete mode 100644 sdk/evm_chain_ids_test.go diff --git a/node/cmd/guardiand/node.go b/node/cmd/guardiand/node.go index 8d5b5fa6d5..d696ed4f9d 100644 --- a/node/cmd/guardiand/node.go +++ b/node/cmd/guardiand/node.go @@ -814,41 +814,41 @@ func runNode(cmd *cobra.Command, args []string) { } // Validate the args for all the EVM chains. The last flag indicates if the chain is allowed in mainnet. - *ethContract = checkEvmArgs(logger, *ethRPC, *ethContract, "eth", true) - *bscContract = checkEvmArgs(logger, *bscRPC, *bscContract, "bsc", true) - *polygonContract = checkEvmArgs(logger, *polygonRPC, *polygonContract, "polygon", true) - *avalancheContract = checkEvmArgs(logger, *avalancheRPC, *avalancheContract, "avalanche", true) - *oasisContract = checkEvmArgs(logger, *oasisRPC, *oasisContract, "oasis", true) - *fantomContract = checkEvmArgs(logger, *fantomRPC, *fantomContract, "fantom", true) - *karuraContract = checkEvmArgs(logger, *karuraRPC, *karuraContract, "karura", true) - *acalaContract = checkEvmArgs(logger, *acalaRPC, *acalaContract, "acala", true) - *klaytnContract = checkEvmArgs(logger, *klaytnRPC, *klaytnContract, "klaytn", true) - *celoContract = checkEvmArgs(logger, *celoRPC, *celoContract, "celo", true) - *moonbeamContract = checkEvmArgs(logger, *moonbeamRPC, *moonbeamContract, "moonbeam", true) - *arbitrumContract = checkEvmArgs(logger, *arbitrumRPC, *arbitrumContract, "arbitrum", true) - *optimismContract = checkEvmArgs(logger, *optimismRPC, *optimismContract, "optimism", true) - *baseContract = checkEvmArgs(logger, *baseRPC, *baseContract, "base", true) - *scrollContract = checkEvmArgs(logger, *scrollRPC, *scrollContract, "scroll", true) - *mantleContract = checkEvmArgs(logger, *mantleRPC, *mantleContract, "mantle", true) - *blastContract = checkEvmArgs(logger, *blastRPC, *blastContract, "blast", true) - *xlayerContract = checkEvmArgs(logger, *xlayerRPC, *xlayerContract, "xlayer", true) - *lineaContract = checkEvmArgs(logger, *lineaRPC, *lineaContract, "linea", true) - *berachainContract = checkEvmArgs(logger, *berachainRPC, *berachainContract, "berachain", true) - *snaxchainContract = checkEvmArgs(logger, *snaxchainRPC, *snaxchainContract, "snaxchain", true) - *unichainContract = checkEvmArgs(logger, *unichainRPC, *unichainContract, "unichain", true) - *worldchainContract = checkEvmArgs(logger, *worldchainRPC, *worldchainContract, "worldchain", true) - *inkContract = checkEvmArgs(logger, *inkRPC, *inkContract, "ink", false) - *hyperEvmContract = checkEvmArgs(logger, *hyperEvmRPC, *hyperEvmContract, "hyperEvm", false) - *monadContract = checkEvmArgs(logger, *monadRPC, *monadContract, "monad", false) - *seiEvmContract = checkEvmArgs(logger, *seiEvmRPC, *seiEvmContract, "seiEvm", false) + *ethContract = checkEvmArgs(logger, *ethRPC, *ethContract, vaa.ChainIDEthereum) + *bscContract = checkEvmArgs(logger, *bscRPC, *bscContract, vaa.ChainIDBSC) + *polygonContract = checkEvmArgs(logger, *polygonRPC, *polygonContract, vaa.ChainIDPolygon) + *avalancheContract = checkEvmArgs(logger, *avalancheRPC, *avalancheContract, vaa.ChainIDAvalanche) + *oasisContract = checkEvmArgs(logger, *oasisRPC, *oasisContract, vaa.ChainIDOasis) + *fantomContract = checkEvmArgs(logger, *fantomRPC, *fantomContract, vaa.ChainIDFantom) + *karuraContract = checkEvmArgs(logger, *karuraRPC, *karuraContract, vaa.ChainIDKarura) + *acalaContract = checkEvmArgs(logger, *acalaRPC, *acalaContract, vaa.ChainIDAcala) + *klaytnContract = checkEvmArgs(logger, *klaytnRPC, *klaytnContract, vaa.ChainIDKlaytn) + *celoContract = checkEvmArgs(logger, *celoRPC, *celoContract, vaa.ChainIDCelo) + *moonbeamContract = checkEvmArgs(logger, *moonbeamRPC, *moonbeamContract, vaa.ChainIDMoonbeam) + *arbitrumContract = checkEvmArgs(logger, *arbitrumRPC, *arbitrumContract, vaa.ChainIDArbitrum) + *optimismContract = checkEvmArgs(logger, *optimismRPC, *optimismContract, vaa.ChainIDOptimism) + *baseContract = checkEvmArgs(logger, *baseRPC, *baseContract, vaa.ChainIDBase) + *scrollContract = checkEvmArgs(logger, *scrollRPC, *scrollContract, vaa.ChainIDScroll) + *mantleContract = checkEvmArgs(logger, *mantleRPC, *mantleContract, vaa.ChainIDMantle) + *blastContract = checkEvmArgs(logger, *blastRPC, *blastContract, vaa.ChainIDBlast) + *xlayerContract = checkEvmArgs(logger, *xlayerRPC, *xlayerContract, vaa.ChainIDXLayer) + *lineaContract = checkEvmArgs(logger, *lineaRPC, *lineaContract, vaa.ChainIDLinea) + *berachainContract = checkEvmArgs(logger, *berachainRPC, *berachainContract, vaa.ChainIDBerachain) + *snaxchainContract = checkEvmArgs(logger, *snaxchainRPC, *snaxchainContract, vaa.ChainIDSnaxchain) + *unichainContract = checkEvmArgs(logger, *unichainRPC, *unichainContract, vaa.ChainIDUnichain) + *worldchainContract = checkEvmArgs(logger, *worldchainRPC, *worldchainContract, vaa.ChainIDWorldchain) + *inkContract = checkEvmArgs(logger, *inkRPC, *inkContract, vaa.ChainIDInk) + *hyperEvmContract = checkEvmArgs(logger, *hyperEvmRPC, *hyperEvmContract, vaa.ChainIDHyperEVM) + *monadContract = checkEvmArgs(logger, *monadRPC, *monadContract, vaa.ChainIDMonad) + *seiEvmContract = checkEvmArgs(logger, *seiEvmRPC, *seiEvmContract, vaa.ChainIDSeiEVM) // These chains will only ever be testnet / devnet. - *sepoliaContract = checkEvmArgs(logger, *sepoliaRPC, *sepoliaContract, "sepolia", false) - *arbitrumSepoliaContract = checkEvmArgs(logger, *arbitrumSepoliaRPC, *arbitrumSepoliaContract, "arbitrumSepolia", false) - *baseSepoliaContract = checkEvmArgs(logger, *baseSepoliaRPC, *baseSepoliaContract, "baseSepolia", false) - *optimismSepoliaContract = checkEvmArgs(logger, *optimismSepoliaRPC, *optimismSepoliaContract, "optimismSepolia", false) - *holeskyContract = checkEvmArgs(logger, *holeskyRPC, *holeskyContract, "holesky", false) - *polygonSepoliaContract = checkEvmArgs(logger, *polygonSepoliaRPC, *polygonSepoliaContract, "polygonSepolia", false) + *sepoliaContract = checkEvmArgs(logger, *sepoliaRPC, *sepoliaContract, vaa.ChainIDSepolia) + *arbitrumSepoliaContract = checkEvmArgs(logger, *arbitrumSepoliaRPC, *arbitrumSepoliaContract, vaa.ChainIDArbitrumSepolia) + *baseSepoliaContract = checkEvmArgs(logger, *baseSepoliaRPC, *baseSepoliaContract, vaa.ChainIDBaseSepolia) + *optimismSepoliaContract = checkEvmArgs(logger, *optimismSepoliaRPC, *optimismSepoliaContract, vaa.ChainIDOptimismSepolia) + *holeskyContract = checkEvmArgs(logger, *holeskyRPC, *holeskyContract, vaa.ChainIDHolesky) + *polygonSepoliaContract = checkEvmArgs(logger, *polygonSepoliaRPC, *polygonSepoliaContract, vaa.ChainIDPolygonSepolia) if !argsConsistent([]string{*solanaContract, *solanaRPC}) { logger.Fatal("Both --solanaContract and --solanaRPC must be set or both unset") @@ -1812,17 +1812,17 @@ func shouldStart(rpc *string) bool { // checkEvmArgs verifies that the RPC and contract address parameters for an EVM chain make sense, given the environment. // If we are in devnet mode and the contract address is not specified, it returns the deterministic one for tilt. -func checkEvmArgs(logger *zap.Logger, rpc string, contractAddr, chainLabel string, mainnetSupported bool) string { +func checkEvmArgs(logger *zap.Logger, rpc string, contractAddr string, chainID vaa.ChainID) string { if env != common.UnsafeDevNet { // In mainnet / testnet, if either parameter is specified, they must both be specified. if (rpc == "") != (contractAddr == "") { - logger.Fatal(fmt.Sprintf("Both --%sContract and --%sRPC must be set or both unset", chainLabel, chainLabel)) + logger.Fatal(fmt.Sprintf("Both contract and RPC for chain %s must be set or both unset", chainID.String())) } } else { // In devnet, if RPC is set but contract is not set, use the deterministic one for tilt. if rpc == "" { if contractAddr != "" { - logger.Fatal(fmt.Sprintf("If --%sRPC is not set, --%sContract must not be set", chainLabel, chainLabel)) + logger.Fatal(fmt.Sprintf("If RPC is not set for chain %s, contract must not be set", chainID.String())) } } else { if contractAddr == "" { @@ -1830,8 +1830,9 @@ func checkEvmArgs(logger *zap.Logger, rpc string, contractAddr, chainLabel strin } } } + mainnetSupported := evm.SupportedInMainnet(chainID) if contractAddr != "" && !mainnetSupported && env == common.MainNet { - logger.Fatal(fmt.Sprintf("Chain %s not supported in mainnet", chainLabel)) + logger.Fatal(fmt.Sprintf("Chain %s is not supported in mainnet", chainID.String())) } return contractAddr } diff --git a/node/pkg/watchers/evm/chain_config.go b/node/pkg/watchers/evm/chain_config.go new file mode 100644 index 0000000000..b610dbc4d3 --- /dev/null +++ b/node/pkg/watchers/evm/chain_config.go @@ -0,0 +1,274 @@ +package evm + +// This file defines the set of EVM chains supported by the guardian watcher, including their EVM chain IDs and whether they support finalized and / or safe blocks. +// There is data for both Mainnet and Testnet. A chain should only be populated in the tables if the watcher should be allowed in that environment. +// Wherever possible, a public RPC endpoint is included so the verification tool can confirm that the EVM chain IDs specified here match the values of a known public node. + +// NOTE: Whenever changes are made to the config data in this file, you should do the following: +// node/pkg/watcher/evm$ go test +// node/pkg/watcher/evm/verify_chain_config$ go run verify.go + +// TODO: In a future PR we could consider merging the data in `node/hack/repair_eth/repair_eth.go` into here. + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/certusone/wormhole/node/pkg/common" + "github.com/ethereum/go-ethereum/rpc" + "github.com/wormhole-foundation/wormhole/sdk/vaa" + "go.uber.org/zap" +) + +type ( + // EnvEntry specifies the config data for a given chain / environment. + EnvEntry struct { + // Finalized indicates if the chain supports querying for finalized blocks. + Finalized bool + + // Safe indicates if the chain supports querying for safe blocks. + Safe bool + + // EvmChainID is the expected EVM chain ID (what is returned by the `eth_chainId` RPC call). + EvmChainID uint64 + + // PublicRPC is not actually used by the watcher. It's used to verify that the EvmChainID specified here is correct. + PublicRPC string + } + + // EnvMap defines the config data for a given environment (mainet or testnet). + EnvMap map[vaa.ChainID]EnvEntry +) + +var ( + ErrInvalidEnv = errors.New("invalid environment") + ErrNotFound = errors.New("not found") + + // mainnetChainConfig specifies the configuration for all chains enabled in Mainnet. + // NOTE: Only add a chain here if the watcher should allow it in Mainnet! + // NOTE: If you change this data, be sure and run the tests described at the top of this file! + mainnetChainConfig = EnvMap{ + vaa.ChainIDEthereum: {Finalized: true, Safe: true, EvmChainID: 1, PublicRPC: "https://ethereum-rpc.publicnode.com"}, + vaa.ChainIDBSC: {Finalized: true, Safe: true, EvmChainID: 56, PublicRPC: "https://bsc-rpc.publicnode.com"}, + + // Polygon supports polling for finalized but not safe: https://forum.polygon.technology/t/optimizing-decentralized-apps-ux-with-milestones-a-significantly-accelerated-finality-solution/13154 + vaa.ChainIDPolygon: {Finalized: true, Safe: false, EvmChainID: 137, PublicRPC: "https://polygon-bor-rpc.publicnode.com"}, + + vaa.ChainIDAvalanche: {Finalized: false, Safe: false, EvmChainID: 43114, PublicRPC: "https://avalanche-c-chain-rpc.publicnode.com"}, + vaa.ChainIDOasis: {Finalized: false, Safe: false, EvmChainID: 42262, PublicRPC: "https://emerald.oasis.dev/"}, + // vaa.ChainIDAurora: Not supported in the guardian. + vaa.ChainIDFantom: {Finalized: false, Safe: false, EvmChainID: 250, PublicRPC: "https://fantom-rpc.publicnode.com"}, + vaa.ChainIDKarura: {Finalized: true, Safe: true, EvmChainID: 686, PublicRPC: "https://eth-rpc-karura.aca-api.network/"}, + vaa.ChainIDAcala: {Finalized: true, Safe: true, EvmChainID: 787, PublicRPC: "https://eth-rpc-acala.aca-api.network/"}, + vaa.ChainIDKlaytn: {Finalized: false, Safe: false, EvmChainID: 8217}, // As of Feb 2025, can't find a working public node. + vaa.ChainIDCelo: {Finalized: true, Safe: false, EvmChainID: 42220, PublicRPC: "https://celo-rpc.publicnode.com"}, + vaa.ChainIDMoonbeam: {Finalized: true, Safe: true, EvmChainID: 1284, PublicRPC: "https://moonbeam-rpc.publicnode.com"}, + vaa.ChainIDArbitrum: {Finalized: true, Safe: true, EvmChainID: 42161, PublicRPC: "https://arbitrum-one-rpc.publicnode.com"}, + vaa.ChainIDOptimism: {Finalized: true, Safe: true, EvmChainID: 10, PublicRPC: "https://optimism-rpc.publicnode.com"}, + // vaa.ChainIDGnosis: Not supported in the guardian. + // vaa.ChainIDBtc: Not supported in the guardian. + vaa.ChainIDBase: {Finalized: true, Safe: true, EvmChainID: 8453, PublicRPC: "https://base-rpc.publicnode.com"}, + // vaa.ChainIDFileCoin: Not supported in the guardian. + // vaa.ChainIDRootstock: Not supported in the guardian. + + // As of 11/10/2023 Scroll supports polling for finalized but not safe. + vaa.ChainIDScroll: {Finalized: true, Safe: false, EvmChainID: 534352, PublicRPC: "https://scroll-rpc.publicnode.com"}, + + vaa.ChainIDMantle: {Finalized: true, Safe: true, EvmChainID: 5000, PublicRPC: "https://mantle-rpc.publicnode.com"}, + vaa.ChainIDBlast: {Finalized: true, Safe: true, EvmChainID: 81457, PublicRPC: "https://blast-rpc.publicnode.com"}, + vaa.ChainIDXLayer: {Finalized: true, Safe: true, EvmChainID: 196, PublicRPC: "https://xlayerrpc.okx.com"}, + + // As of 9/06/2024 Linea supports polling for finalized but not safe. + vaa.ChainIDLinea: {Finalized: true, Safe: false, EvmChainID: 59144, PublicRPC: "https://linea-rpc.publicnode.com"}, + + vaa.ChainIDBerachain: {Finalized: false, Safe: false, EvmChainID: 80094, PublicRPC: "https://berachain-rpc.publicnode.com"}, + // vaa.ChainIDSeiEVM: Not in Mainnet yet. + // vaa.ChainIDEclipse: Not supported in the guardian. + // vaa.ChainIDBOB: Not supported in the guardian. + vaa.ChainIDSnaxchain: {Finalized: true, Safe: true, EvmChainID: 2192, PublicRPC: "https://mainnet.snaxchain.io"}, + vaa.ChainIDUnichain: {Finalized: true, Safe: true, EvmChainID: 130, PublicRPC: "https://unichain-rpc.publicnode.com"}, + vaa.ChainIDWorldchain: {Finalized: true, Safe: true, EvmChainID: 480, PublicRPC: "https://worldchain-mainnet.g.alchemy.com/public"}, + // vaa.ChainIDInk: Not in Mainnet yet. + // vaa.ChainIDHyperEVM: Not in Mainnet yet. + // vaa.ChainIDMonad: Not in Mainnet yet. + } + + // testnetChainConfig specifies the configuration for all chains enabled in Mainnet. + // NOTE: Only add a chain here if the watcher should allow it in Testnet. + // NOTE: If you change this data, be sure and run the tests described at the top of this file! + testnetChainConfig = EnvMap{ + // For Ethereum testnet we actually use Holeksy since Goerli is deprecated. + vaa.ChainIDEthereum: {Finalized: true, Safe: true, EvmChainID: 17000, PublicRPC: "https://ethereum-holesky-rpc.publicnode.com"}, + vaa.ChainIDBSC: {Finalized: true, Safe: true, EvmChainID: 97, PublicRPC: "https://bsc-testnet-rpc.publicnode.com"}, + + // Polygon supports polling for finalized but not safe: https://forum.polygon.technology/t/optimizing-decentralized-apps-ux-with-milestones-a-significantly-accelerated-finality-solution/13154 + vaa.ChainIDPolygon: {Finalized: true, Safe: false, EvmChainID: 80001}, // Polygon Mumbai is deprecated. + + vaa.ChainIDAvalanche: {Finalized: false, Safe: false, EvmChainID: 43113, PublicRPC: "https://avalanche-fuji-c-chain-rpc.publicnode.com"}, + vaa.ChainIDOasis: {Finalized: false, Safe: false, EvmChainID: 42261, PublicRPC: "https://testnet.emerald.oasis.dev"}, + // vaa.ChainIDAurora: Not supported in the guardian. + vaa.ChainIDFantom: {Finalized: false, Safe: false, EvmChainID: 4002, PublicRPC: "https://fantom-testnet-rpc.publicnode.com"}, + vaa.ChainIDKarura: {Finalized: true, Safe: true, EvmChainID: 596, PublicRPC: "https://eth-rpc-karura-testnet.aca-staging.network"}, + vaa.ChainIDAcala: {Finalized: true, Safe: true, EvmChainID: 597, PublicRPC: "https://eth-rpc-acala-testnet.aca-staging.network"}, + vaa.ChainIDKlaytn: {Finalized: false, Safe: false, EvmChainID: 1001}, // As of Feb 2025, can't find a working public node. + vaa.ChainIDCelo: {Finalized: true, Safe: true, EvmChainID: 44787, PublicRPC: "https://alfajores-forno.celo-testnet.org"}, + vaa.ChainIDMoonbeam: {Finalized: true, Safe: true, EvmChainID: 1287, PublicRPC: "https://rpc.api.moonbase.moonbeam.network"}, + vaa.ChainIDArbitrum: {Finalized: true, Safe: true, EvmChainID: 421613}, // Arbitrum Goerli is deprecated. + vaa.ChainIDOptimism: {Finalized: true, Safe: true, EvmChainID: 420}, // Optimism Goerli is deprecated. + // vaa.ChainIDGnosis: Not supported in the guardian. + // vaa.ChainIDBtc: Not supported in the guardian. + vaa.ChainIDBase: {Finalized: true, Safe: true, EvmChainID: 84531}, // Base Goerli is deprecated. + // vaa.ChainIDFileCoin: Not supported in the guardian. + // vaa.ChainIDRootstock: Not supported in the guardian. + + // As of 11/10/2023 Scroll supports polling for finalized but not safe. + vaa.ChainIDScroll: {Finalized: true, Safe: false, EvmChainID: 534351, PublicRPC: "https://scroll-sepolia-rpc.publicnode.com"}, + + vaa.ChainIDMantle: {Finalized: true, Safe: true, EvmChainID: 5003, PublicRPC: "https://mantle-sepolia.drpc.org"}, + vaa.ChainIDBlast: {Finalized: true, Safe: true, EvmChainID: 168587773, PublicRPC: "https://blast-sepolia.drpc.org"}, + vaa.ChainIDXLayer: {Finalized: true, Safe: true, EvmChainID: 195, PublicRPC: "https://xlayertestrpc.okx.com"}, + + // As of 9/06/2024 Linea supports polling for finalized but not safe. + vaa.ChainIDLinea: {Finalized: true, Safe: false, EvmChainID: 59141, PublicRPC: "https://linea-sepolia-rpc.publicnode.com"}, + + vaa.ChainIDBerachain: {Finalized: false, Safe: false, EvmChainID: 80084, PublicRPC: "https://bartio.rpc.berachain.com"}, + vaa.ChainIDSeiEVM: {Finalized: true, Safe: true, EvmChainID: 1328, PublicRPC: "https://evm-rpc-testnet.sei-apis.com/"}, + // vaa.ChainIDEclipse: Not supported in the guardian. + // vaa.ChainIDBOB: Not supported in the guardian. + vaa.ChainIDSnaxchain: {Finalized: true, Safe: true, EvmChainID: 13001, PublicRPC: "https://testnet.snaxchain.io"}, + vaa.ChainIDUnichain: {Finalized: true, Safe: true, EvmChainID: 1301, PublicRPC: "https://unichain-sepolia-rpc.publicnode.com"}, + vaa.ChainIDWorldchain: {Finalized: true, Safe: true, EvmChainID: 4801, PublicRPC: "https://worldchain-sepolia.g.alchemy.com/public"}, + vaa.ChainIDInk: {Finalized: true, Safe: true, EvmChainID: 763373, PublicRPC: "https://rpc-qnd-sepolia.inkonchain.com"}, + vaa.ChainIDHyperEVM: {Finalized: true, Safe: true, EvmChainID: 998}, // As of Feb 2025, this does not work: "https://api.hyperliquid-testnet.xyz/evm" + vaa.ChainIDMonad: {Finalized: true, Safe: true, EvmChainID: 10143, PublicRPC: "https://testnet-rpc.monad.xyz"}, + vaa.ChainIDSepolia: {Finalized: true, Safe: true, EvmChainID: 11155111, PublicRPC: "https://ethereum-sepolia-rpc.publicnode.com"}, + vaa.ChainIDArbitrumSepolia: {Finalized: true, Safe: true, EvmChainID: 421614, PublicRPC: "https://arbitrum-sepolia-rpc.publicnode.com"}, + vaa.ChainIDBaseSepolia: {Finalized: true, Safe: true, EvmChainID: 84532, PublicRPC: "https://base-sepolia-rpc.publicnode.com"}, + vaa.ChainIDOptimismSepolia: {Finalized: true, Safe: true, EvmChainID: 11155420, PublicRPC: "https://optimism-sepolia-rpc.publicnode.com"}, + vaa.ChainIDHolesky: {Finalized: true, Safe: true, EvmChainID: 17000, PublicRPC: "https://ethereum-holesky-rpc.publicnode.com"}, + vaa.ChainIDPolygonSepolia: {Finalized: true, Safe: false, EvmChainID: 80002, PublicRPC: "https://polygon-amoy-bor-rpc.publicnode.com"}, + } +) + +// SupportedInMainnet returns true if the chain is configured in Mainnet. +func SupportedInMainnet(chainID vaa.ChainID) bool { + _, exists := mainnetChainConfig[chainID] + return exists +} + +// GetFinality returns the finalized and safe flags for the specified environment / chain. These are used to configure the watcher at run time. +func GetFinality(env common.Environment, chainID vaa.ChainID) (finalized bool, safe bool, err error) { + // Tilt supports polling for both finalized and safe. + if env == common.UnsafeDevNet { + return true, true, nil + } + + m, err := GetChainConfigMap(env) + if err != nil { + return false, false, err + } + + entry, exists := m[chainID] + if !exists { + return false, false, ErrNotFound + } + + return entry.Finalized, entry.Safe, nil +} + +// GetEvmChainID returns the configured EVM chain ID for the specified environment / chain. +func GetEvmChainID(env common.Environment, chainID vaa.ChainID) (uint64, error) { + m, err := GetChainConfigMap(env) + if err != nil { + return 0, err + } + + entry, exists := m[chainID] + if !exists { + return 0, ErrNotFound + } + + return entry.EvmChainID, nil +} + +// GetChainConfigMap is a helper that returns the configuration for the specified environment. +// This is public so the chain verify utility can use it. +func GetChainConfigMap(env common.Environment) (EnvMap, error) { + if env == common.MainNet { + return mainnetChainConfig, nil + } + + if env == common.TestNet { + return testnetChainConfig, nil + } + + return EnvMap{}, ErrInvalidEnv +} + +// QueryEvmChainID queries the specified RPC for the EVM chain ID. +func QueryEvmChainID(ctx context.Context, url string) (uint64, error) { + timeout, cancel := context.WithTimeout(ctx, 15*time.Second) + defer cancel() + + c, err := rpc.DialContext(timeout, url) + if err != nil { + return 0, fmt.Errorf("failed to connect to endpoint: %w", err) + } + + var str string + err = c.CallContext(ctx, &str, "eth_chainId") + if err != nil { + return 0, fmt.Errorf("failed to read evm chain id: %w", err) + } + + evmChainID, err := strconv.ParseUint(strings.TrimPrefix(str, "0x"), 16, 64) + if err != nil { + return 0, fmt.Errorf(`eth_chainId returned an invalid int: "%s"`, str) + } + + return evmChainID, 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, url string) error { + // Don't bother to check in tilt. + if w.env == common.UnsafeDevNet { + return nil + } + + timeout, cancel := context.WithTimeout(ctx, 15*time.Second) + defer cancel() + + c, err := rpc.DialContext(timeout, 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) + } + + expectedEvmChainID, err := GetEvmChainID(w.env, w.chainID) + if err != nil { + return fmt.Errorf("failed to look up evm chain id: %w", err) + } + + logger.Info("queried evm chain id", zap.Uint64("expected", expectedEvmChainID), zap.Uint64("actual", evmChainID)) + + if evmChainID != uint64(expectedEvmChainID) { + return fmt.Errorf("evm chain ID miss match, expected %d, received %d", expectedEvmChainID, evmChainID) + } + + return nil +} diff --git a/node/pkg/watchers/evm/chain_config_test.go b/node/pkg/watchers/evm/chain_config_test.go new file mode 100644 index 0000000000..c89e47ffe8 --- /dev/null +++ b/node/pkg/watchers/evm/chain_config_test.go @@ -0,0 +1,183 @@ +package evm + +import ( + "fmt" + "testing" + + "github.com/certusone/wormhole/node/pkg/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/wormhole-foundation/wormhole/sdk/vaa" +) + +func TestSupportedInMainnet(t *testing.T) { + assert.True(t, SupportedInMainnet(vaa.ChainIDEthereum)) + assert.False(t, SupportedInMainnet(vaa.ChainIDSepolia)) +} + +func TestGetEvmChainID(t *testing.T) { + type test struct { + env common.Environment + input vaa.ChainID + output uint64 + err error + } + + // Note: Don't intend to list every chain here, just enough to verify `GetEvmChainID`. + tests := []test{ + {env: common.MainNet, input: vaa.ChainIDUnset, err: ErrNotFound}, + {env: common.MainNet, input: vaa.ChainIDSepolia, err: ErrNotFound}, + {env: common.MainNet, input: vaa.ChainIDEthereum, output: 1}, + {env: common.MainNet, input: vaa.ChainIDBSC, output: 56}, + {env: common.TestNet, input: vaa.ChainIDUnset, err: ErrNotFound}, + {env: common.TestNet, input: vaa.ChainIDSepolia, output: 11155111}, + {env: common.TestNet, input: vaa.ChainIDEthereum, output: 17000}, + {env: common.GoTest, input: vaa.ChainIDEthereum, err: ErrInvalidEnv}, + } + + for _, tc := range tests { + t.Run(string(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 TestGetFinality(t *testing.T) { + type test struct { + env common.Environment + input vaa.ChainID + finalized bool + safe bool + err error + } + + // Note: Don't intend to list every chain here, just enough to verify `GetEvmChainID`. + tests := []test{ + {env: common.MainNet, input: vaa.ChainIDUnset, err: ErrNotFound}, + {env: common.MainNet, input: vaa.ChainIDSepolia, err: ErrNotFound}, + {env: common.MainNet, input: vaa.ChainIDEthereum, finalized: true, safe: true}, + {env: common.MainNet, input: vaa.ChainIDBSC, finalized: true, safe: true}, + {env: common.MainNet, input: vaa.ChainIDScroll, finalized: true, safe: false}, + {env: common.TestNet, input: vaa.ChainIDUnset, err: ErrNotFound}, + {env: common.TestNet, input: vaa.ChainIDSepolia, finalized: true, safe: true}, + {env: common.TestNet, input: vaa.ChainIDEthereum, finalized: true, safe: true}, + {env: common.GoTest, input: vaa.ChainIDEthereum, err: ErrInvalidEnv}, + } + + for _, tc := range tests { + t.Run(string(tc.env)+"-"+tc.input.String(), func(t *testing.T) { + finalized, safe, err := GetFinality(tc.env, tc.input) + if tc.err != nil { + assert.ErrorIs(t, tc.err, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.finalized, finalized) + assert.Equal(t, tc.safe, safe) + } + }) + } +} + +// TODO: Once this code is merged and verified to be stable, this test can be deleted. +func TestFinalityValuesForMainnet(t *testing.T) { + testFinalityValuesForEnvironment(t, common.MainNet) +} + +// TODO: Once this code is merged and verified to be stable, this test can be deleted. +func TestFinalityValuesForTestnet(t *testing.T) { + testFinalityValuesForEnvironment(t, common.TestNet) +} + +// TODO: Once this code is merged and verified to be stable, this function can be deleted. +func testFinalityValuesForEnvironment(t *testing.T, env common.Environment) { + t.Helper() + m, err := GetChainConfigMap(env) + require.NoError(t, err) + + for chainID, entry := range m { + t.Run(chainID.String(), func(t *testing.T) { + finalized, safe, err := getFinalityForTest(env, chainID) + require.NoError(t, err) + assert.Equal(t, finalized, entry.Finalized) + assert.Equal(t, safe, entry.Safe) + }) + } +} + +// getFinalityForTest was lifted from the old `getFinality` watcher function so we could validate our config data. +// TODO: Once this code is merged and verified to be stable, this function can be deleted so we don't have to maintain it. +func getFinalityForTest(env common.Environment, chainID vaa.ChainID) (finalized bool, safe bool, err error) { + // Tilt supports polling for both finalized and safe. + if env == common.UnsafeDevNet { + finalized = true + safe = true + + // The following chains support polling for both finalized and safe. + } else if chainID == vaa.ChainIDAcala || + chainID == vaa.ChainIDArbitrum || + chainID == vaa.ChainIDArbitrumSepolia || + chainID == vaa.ChainIDBase || + chainID == vaa.ChainIDBaseSepolia || + chainID == vaa.ChainIDBlast || + chainID == vaa.ChainIDBSC || + chainID == vaa.ChainIDEthereum || + chainID == vaa.ChainIDHolesky || + chainID == vaa.ChainIDHyperEVM || + chainID == vaa.ChainIDInk || + chainID == vaa.ChainIDKarura || + chainID == vaa.ChainIDMantle || + chainID == vaa.ChainIDMonad || + chainID == vaa.ChainIDMoonbeam || + chainID == vaa.ChainIDOptimism || + chainID == vaa.ChainIDOptimismSepolia || + chainID == vaa.ChainIDSeiEVM || + chainID == vaa.ChainIDSepolia || + chainID == vaa.ChainIDSnaxchain || + chainID == vaa.ChainIDUnichain || + chainID == vaa.ChainIDWorldchain || + chainID == vaa.ChainIDXLayer { + finalized = true + safe = true + + } else if chainID == vaa.ChainIDCelo { + // TODO: Celo testnet now supports finalized and safe. As of January 2025, mainnet doesn't yet support safe. Once Celo mainnet cuts over, Celo can + // be added to the list above. That change won't be super urgent since we'll just continue to publish safe as finalized, which is not a huge deal. + finalized = true + safe = env != common.MainNet + + // Polygon now supports polling for finalized but not safe. + // https://forum.polygon.technology/t/optimizing-decentralized-apps-ux-with-milestones-a-significantly-accelerated-finality-solution/13154 + } else if chainID == vaa.ChainIDPolygon || + chainID == vaa.ChainIDPolygonSepolia { + finalized = true + + // As of 11/10/2023 Scroll supports polling for finalized but not safe. + } else if chainID == vaa.ChainIDScroll { + finalized = true + + // As of 9/06/2024 Linea supports polling for finalized but not safe. + } else if chainID == vaa.ChainIDLinea { + finalized = true + + // The following chains support instant finality. + } else if chainID == vaa.ChainIDAvalanche || + chainID == vaa.ChainIDBerachain || // Berachain supports instant finality: https://docs.berachain.com/faq/ + chainID == vaa.ChainIDOasis || + chainID == vaa.ChainIDAurora || + chainID == vaa.ChainIDFantom || + chainID == vaa.ChainIDKlaytn { + return false, false, nil + + // Anything else is undefined / not supported. + } else { + return false, false, fmt.Errorf("unsupported chain: %s", chainID.String()) + } + + return +} diff --git a/node/pkg/watchers/evm/verify_chain_config/verify.go b/node/pkg/watchers/evm/verify_chain_config/verify.go new file mode 100644 index 0000000000..ba32d2ccf2 --- /dev/null +++ b/node/pkg/watchers/evm/verify_chain_config/verify.go @@ -0,0 +1,88 @@ +package main + +// This is a tool that queries the RPCs to verify that the `EvmChainID` values in the chain config maps are correct as +// compared to the specified well-known public RPC endpoint. This tool should be run whenever either of the chain config maps are updated. + +// Usage: go run verify.go [--env `env``] +// Where `env` may be "mainnet", "testnet" or "both" where the default is "both". + +import ( + "context" + "flag" + "fmt" + "os" + "sort" + + "github.com/certusone/wormhole/node/pkg/common" + "github.com/certusone/wormhole/node/pkg/watchers/evm" + "github.com/wormhole-foundation/wormhole/sdk/vaa" +) + +var ( + envStr = flag.String("env", "both", `Environment to be validated, may be "mainnet", "testnet" or "both", default is "both"`) +) + +func main() { + flag.Parse() + + if *envStr == "both" { + verifyForEnv(common.MainNet) + verifyForEnv(common.TestNet) + } else { + env, err := common.ParseEnvironment(*envStr) + if err != nil || (env != common.TestNet && env != common.MainNet) { + if *envStr == "" { + fmt.Printf("Please specify --env\n") + } else { + fmt.Printf("Invalid value for --env, should be `mainnet`, `testnet` or `both`, is `%s`\n", *envStr) + } + os.Exit(1) + } + + verifyForEnv(env) + } +} + +type ListEntry struct { + ChainID vaa.ChainID + Entry evm.EnvEntry +} + +func verifyForEnv(env common.Environment) { + m, err := evm.GetChainConfigMap(env) + if err != nil { + fmt.Printf("Failed to get chain config map for %snet\n", env) + os.Exit(1) + } + + // The map is ordered by hash. Sort it by chain ID. + orderedList := []ListEntry{} + for chainId, entry := range m { + orderedList = append(orderedList, ListEntry{chainId, entry}) + } + + sort.Slice(orderedList, func(i, j int) bool { + return orderedList[i].ChainID < orderedList[j].ChainID + }) + + ctx := context.Background() + + for _, entry := range orderedList { + if entry.Entry.PublicRPC == "" { + fmt.Printf("Skipping %v %v because the rpc is null\n", env, entry.ChainID) + } else { + evmChainID, err := evm.QueryEvmChainID(ctx, entry.Entry.PublicRPC) + if err != nil { + fmt.Printf("ERROR: Failed to query EVM chain ID for %v %v: %v\n", env, entry.ChainID, err) + os.Exit(1) + } + + if evmChainID != entry.Entry.EvmChainID { + fmt.Printf("ERROR: EVM chain ID mismatch for %v %v: config: %v, actual: %v\n", env, entry.ChainID, entry.Entry.EvmChainID, evmChainID) + os.Exit(1) + } + + fmt.Printf("EVM chain ID match for %v %v: value: %v\n", env, entry.ChainID, evmChainID) + } + } +} diff --git a/node/pkg/watchers/evm/watcher.go b/node/pkg/watchers/evm/watcher.go index 1d72498886..1c67590c37 100644 --- a/node/pkg/watchers/evm/watcher.go +++ b/node/pkg/watchers/evm/watcher.go @@ -5,8 +5,6 @@ import ( "fmt" "math" "math/big" - "strconv" - "strings" "sync" "sync/atomic" "time" @@ -30,7 +28,6 @@ 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" ) @@ -214,7 +211,7 @@ func (w *Watcher) Run(parentCtx context.Context) error { ContractAddress: w.contract.Hex(), }) - if err := w.verifyEvmChainID(ctx, logger); err != nil { + if err := w.verifyEvmChainID(ctx, logger, w.url); err != nil { return fmt.Errorf("failed to verify evm chain id: %w", err) } @@ -700,73 +697,9 @@ func fetchCurrentGuardianSet(ctx context.Context, ethConn connectors.Connector) // getFinality determines if the chain supports "finalized" and "safe". This is hard coded so it requires thought to change something. However, it also reads the RPC // to make sure the node actually supports the expected values, and returns an error if it doesn't. Note that we do not support using safe mode but not finalized mode. func (w *Watcher) getFinality(ctx context.Context) (bool, bool, error) { - finalized := false - safe := false - - // Tilt supports polling for both finalized and safe. - if w.env == common.UnsafeDevNet { - finalized = true - safe = true - - // The following chains support polling for both finalized and safe. - } else if w.chainID == vaa.ChainIDAcala || - w.chainID == vaa.ChainIDArbitrum || - w.chainID == vaa.ChainIDArbitrumSepolia || - w.chainID == vaa.ChainIDBase || - w.chainID == vaa.ChainIDBaseSepolia || - w.chainID == vaa.ChainIDBlast || - w.chainID == vaa.ChainIDBSC || - w.chainID == vaa.ChainIDEthereum || - w.chainID == vaa.ChainIDHolesky || - w.chainID == vaa.ChainIDHyperEVM || - w.chainID == vaa.ChainIDInk || - w.chainID == vaa.ChainIDKarura || - w.chainID == vaa.ChainIDMantle || - w.chainID == vaa.ChainIDMonad || - w.chainID == vaa.ChainIDMoonbeam || - w.chainID == vaa.ChainIDOptimism || - w.chainID == vaa.ChainIDOptimismSepolia || - w.chainID == vaa.ChainIDSeiEVM || - w.chainID == vaa.ChainIDSepolia || - w.chainID == vaa.ChainIDSnaxchain || - w.chainID == vaa.ChainIDUnichain || - w.chainID == vaa.ChainIDWorldchain || - w.chainID == vaa.ChainIDXLayer { - finalized = true - safe = true - - } else if w.chainID == vaa.ChainIDCelo { - // TODO: Celo testnet now supports finalized and safe. As of January 2025, mainnet doesn't yet support safe. Once Celo mainnet cuts over, Celo can - // be added to the list above. That change won't be super urgent since we'll just continue to publish safe as finalized, which is not a huge deal. - finalized = true - safe = w.env != common.MainNet - - // Polygon now supports polling for finalized but not safe. - // https://forum.polygon.technology/t/optimizing-decentralized-apps-ux-with-milestones-a-significantly-accelerated-finality-solution/13154 - } else if w.chainID == vaa.ChainIDPolygon || - w.chainID == vaa.ChainIDPolygonSepolia { - finalized = true - - // As of 11/10/2023 Scroll supports polling for finalized but not safe. - } else if w.chainID == vaa.ChainIDScroll { - finalized = true - - // As of 9/06/2024 Linea supports polling for finalized but not safe. - } else if w.chainID == vaa.ChainIDLinea { - finalized = true - - // The following chains support instant finality. - } else if w.chainID == vaa.ChainIDAvalanche || - w.chainID == vaa.ChainIDBerachain || // Berachain supports instant finality: https://docs.berachain.com/faq/ - w.chainID == vaa.ChainIDOasis || - w.chainID == vaa.ChainIDAurora || - w.chainID == vaa.ChainIDFantom || - w.chainID == vaa.ChainIDKlaytn { - return false, false, nil - - // Anything else is undefined / not supported. - } else { - return false, false, fmt.Errorf("unsupported chain: %s", w.chainID.String()) + finalized, safe, err := GetFinality(w.env, w.chainID) + if err != nil { + return false, false, fmt.Errorf("failed to get finality for %s chain %v: %v", w.env, w.chainID, err) } // If finalized / safe should be supported, read the RPC to make sure they actually are. @@ -800,46 +733,6 @@ 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 diff --git a/sdk/evm_chain_ids.go b/sdk/evm_chain_ids.go deleted file mode 100644 index ad9733cc09..0000000000 --- a/sdk/evm_chain_ids.go +++ /dev/null @@ -1,45 +0,0 @@ -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 -} diff --git a/sdk/evm_chain_ids_test.go b/sdk/evm_chain_ids_test.go deleted file mode 100644 index e6360f5fc1..0000000000 --- a/sdk/evm_chain_ids_test.go +++ /dev/null @@ -1,74 +0,0 @@ -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) - } - }) - } -} diff --git a/sdk/mainnet_consts.go b/sdk/mainnet_consts.go index 1b014b58e5..6fdfcdb7a1 100644 --- a/sdk/mainnet_consts.go +++ b/sdk/mainnet_consts.go @@ -194,41 +194,3 @@ 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, -} diff --git a/sdk/testnet_consts.go b/sdk/testnet_consts.go index 6ebd2ec751..a19e89d336 100644 --- a/sdk/testnet_consts.go +++ b/sdk/testnet_consts.go @@ -102,47 +102,3 @@ 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, -} From c28fd3954b6ee911a51fe28f44d2b49c62f55148 Mon Sep 17 00:00:00 2001 From: Bruce Riley <bruce@wormholelabs.xyz> Date: Thu, 20 Feb 2025 13:36:16 -0500 Subject: [PATCH 3/3] Code review rework --- node/pkg/watchers/evm/chain_config.go | 2 +- node/pkg/watchers/evm/verify_chain_config/verify.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/node/pkg/watchers/evm/chain_config.go b/node/pkg/watchers/evm/chain_config.go index b610dbc4d3..6d655a5a4a 100644 --- a/node/pkg/watchers/evm/chain_config.go +++ b/node/pkg/watchers/evm/chain_config.go @@ -97,7 +97,7 @@ var ( // vaa.ChainIDMonad: Not in Mainnet yet. } - // testnetChainConfig specifies the configuration for all chains enabled in Mainnet. + // testnetChainConfig specifies the configuration for all chains enabled in Testnet. // NOTE: Only add a chain here if the watcher should allow it in Testnet. // NOTE: If you change this data, be sure and run the tests described at the top of this file! testnetChainConfig = EnvMap{ diff --git a/node/pkg/watchers/evm/verify_chain_config/verify.go b/node/pkg/watchers/evm/verify_chain_config/verify.go index ba32d2ccf2..241bcc8e93 100644 --- a/node/pkg/watchers/evm/verify_chain_config/verify.go +++ b/node/pkg/watchers/evm/verify_chain_config/verify.go @@ -55,7 +55,7 @@ func verifyForEnv(env common.Environment) { os.Exit(1) } - // The map is ordered by hash. Sort it by chain ID. + // Create a slice sorted by ChainID that corresponds to the Chain Config Map. orderedList := []ListEntry{} for chainId, entry := range m { orderedList = append(orderedList, ListEntry{chainId, entry})