diff --git a/chains/evm/deployment/v1_5_0/adapters/configimport.go b/chains/evm/deployment/v1_5_0/adapters/configimport.go index 83177bf9e..286861d91 100644 --- a/chains/evm/deployment/v1_5_0/adapters/configimport.go +++ b/chains/evm/deployment/v1_5_0/adapters/configimport.go @@ -16,8 +16,7 @@ import ( cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" "golang.org/x/sync/errgroup" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_2_0/router" - + adapters1_2 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/adapters" "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_0/evm_2_evm_offramp" "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_0/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_5_0/token_admin_registry" @@ -48,6 +47,12 @@ type ConfigImportAdapter struct { TokenAdminReg common.Address PriceRegistry common.Address Router common.Address + + // connectedChainsCache memoizes the result of ConnectedChains per chain selector + // to avoid duplicate (potentially expensive) RPC work when the method is called + // multiple times for the same chain within the same adapter instance. + connectedChainsCache map[uint64][]uint64 + connectedChainsMu sync.Mutex } func (ci *ConfigImportAdapter) InitializeAdapter(e cldf.Environment, sel uint64) error { @@ -121,28 +126,42 @@ func (ci *ConfigImportAdapter) InitializeAdapter(e cldf.Environment, sel uint64) } func (ci *ConfigImportAdapter) ConnectedChains(e cldf.Environment, chainsel uint64) ([]uint64, error) { - var connected []uint64 - // to ensure deduplication in case there are multiple onramps addresses in datastore for the same remote chain selector - var mapConnectedChains = make(map[uint64]bool) - chain, ok := e.BlockChains.EVMChains()[chainsel] - if !ok { - return nil, fmt.Errorf("chain with selector %d not found in environment", chainsel) + // Fast path: return cached result if available to avoid duplicate RPC work. + ci.connectedChainsMu.Lock() + if ci.connectedChainsCache == nil { + ci.connectedChainsCache = make(map[uint64][]uint64) + } + if cached, ok := ci.connectedChainsCache[chainsel]; ok { + // Return a copy to prevent callers from mutating the cached slice. + result := make([]uint64, len(cached)) + copy(result, cached) + ci.connectedChainsMu.Unlock() + return result, nil } - routerC, err := router.NewRouter(ci.Router, chain.Client) + ci.connectedChainsMu.Unlock() + + var connected []uint64 + laneResolver := adapters1_2.LaneVersionResolver{} + remoteChainToVersionMap, _, err := laneResolver.DeriveLaneVersionsForChain(e, chainsel) if err != nil { - return nil, fmt.Errorf("failed to instantiate router contract at %s on chain %d: %w", ci.Router.String(), chain.Selector, err) + return nil, fmt.Errorf("failed to derive lane versions for chain %d: %w", chainsel, err) } - for destSel, onrampForDest := range ci.OnRamp { - onRamp, err := routerC.GetOnRamp(nil, destSel) - if err != nil { - return nil, fmt.Errorf("failed to get onramp for dest chain %d from router at %s on chain %d: %w", destSel, ci.Router.String(), chain.Selector, err) - } - // if the onramp address from the router doesn't match the onramp address we have, then this chain is not actually connected with 1.5 - if onRamp == onrampForDest && !mapConnectedChains[destSel] { + for destSel, version := range remoteChainToVersionMap { + if version.Equal(semver.MustParse("1.5.0")) { connected = append(connected, destSel) - mapConnectedChains[destSel] = true } } + + // Cache the computed result for subsequent calls. + ci.connectedChainsMu.Lock() + if ci.connectedChainsCache == nil { + ci.connectedChainsCache = make(map[uint64][]uint64) + } + cached := make([]uint64, len(connected)) + copy(cached, connected) + ci.connectedChainsCache[chainsel] = cached + ci.connectedChainsMu.Unlock() + return connected, nil } @@ -151,8 +170,12 @@ func (ci *ConfigImportAdapter) SupportedTokensPerRemoteChain(e cldf.Environment, if !ok { return nil, fmt.Errorf("chain with selector %d not found in environment", chainsel) } + remoteChains, err := ci.ConnectedChains(e, chainsel) + if err != nil { + return nil, fmt.Errorf("failed to get connected chains for chain %d: %w", chainsel, err) + } // get all supported tokens from token admin registry - return GetSupportedTokensPerRemoteChain(e.GetContext(), e.Logger, ci.TokenAdminReg, chain) + return GetSupportedTokensPerRemoteChain(e.GetContext(), e.Logger, ci.TokenAdminReg, chain, remoteChains) } func (ci *ConfigImportAdapter) SequenceImportConfig() *cldf_ops.Sequence[api.ImportConfigPerChainInput, sequences.OnChainOutput, cldf_chain.BlockChains] { @@ -192,7 +215,7 @@ func (ci *ConfigImportAdapter) SequenceImportConfig() *cldf_ops.Sequence[api.Imp }) } -func GetSupportedTokensPerRemoteChain(ctx context.Context, l logger.Logger, tokenAdminRegAddr common.Address, chain evm.Chain) (map[uint64][]common.Address, error) { +func GetSupportedTokensPerRemoteChain(ctx context.Context, l logger.Logger, tokenAdminRegAddr common.Address, chain evm.Chain, remoteChains []uint64) (map[uint64][]common.Address, error) { // get all supported tokens from token admin registry tokenAdminRegC, err := token_admin_registry.NewTokenAdminRegistry(tokenAdminRegAddr, chain.Client) if err != nil { @@ -230,29 +253,61 @@ func GetSupportedTokensPerRemoteChain(ctx context.Context, l logger.Logger, toke if err != nil { return fmt.Errorf("failed to instantiate token pool contract at %s on chain %d: %w", poolAddr.String(), chain.Selector, err) } - chains, err := tokenPoolC.GetSupportedChains(&bind.CallOpts{ - Context: grpCtx, - }) - if err != nil { - // if we fail to get the supported chains for a pool, we skip it and move on to avoid failing the entire config import - // since it's possible for some pools do not support the getSupportedChains function - l.Warnf("failed to get supported chains for token pool at %s on chain %d: %v", poolAddr.String(), chain.Selector, err) - return nil - } - tokenAddr, err := tokenPoolC.GetToken(&bind.CallOpts{ - Context: grpCtx, - }) - if err != nil { - // if we fail to get the token address for a pool, we skip it and move on to avoid failing the entire config import - // since it's possible for some pools do not support the getToken function - l.Warnf("failed to get token address for token pool at %s on chain %d: %v", poolAddr.String(), chain.Selector, err) - return nil - } - mu.Lock() - for _, remoteChain := range chains { + + // Cache the token address per pool so we only fetch it once, and + // track when certain pool methods appear to be unsupported so we + // can avoid repeated failed calls and warning spam. + var ( + tokenAddr common.Address + tokenFetched bool + isSupportedChainUnsupported bool + getTokenUnsupported bool + ) + + for _, remoteChain := range remoteChains { + // If we've already determined that IsSupportedChain or GetToken + // are unsupported for this pool, stop checking further chains. + if isSupportedChainUnsupported || getTokenUnsupported { + break + } + + supported, err := tokenPoolC.IsSupportedChain(&bind.CallOpts{ + Context: grpCtx, + }, remoteChain) + if err != nil { + // If we fail to check if the pool supports a remote chain, + // assume this method isn't supported by this pool, log once, + // and short-circuit to avoid failing the entire import and + // spamming warnings for every remote chain. + l.Warnf("failed to check if token pool at %s on chain %d supports remote chain %d: %v", poolAddr.String(), chain.Selector, remoteChain, err) + isSupportedChainUnsupported = true + break + } + if !supported { + continue + } + + // Fetch the token address at most once per pool. + if !tokenFetched { + tokenAddr, err = tokenPoolC.GetToken(&bind.CallOpts{ + Context: grpCtx, + }) + if err != nil { + // If we fail to get the token address for a pool, assume + // this method isn't supported or is consistently failing + // for this pool. Log once and short-circuit further + // attempts for this pool to avoid warning spam. + l.Warnf("failed to get token address for token pool at %s on chain %d: %v", poolAddr.String(), chain.Selector, err) + getTokenUnsupported = true + break + } + tokenFetched = true + } + + mu.Lock() tokensPerRemoteChain[remoteChain] = append(tokensPerRemoteChain[remoteChain], tokenAddr) + mu.Unlock() } - mu.Unlock() return nil }) } diff --git a/chains/evm/deployment/v1_5_0/adapters/configimport_test.go b/chains/evm/deployment/v1_5_0/adapters/configimport_test.go new file mode 100644 index 000000000..529fbcd7a --- /dev/null +++ b/chains/evm/deployment/v1_5_0/adapters/configimport_test.go @@ -0,0 +1,51 @@ +package adapters + +import ( + "testing" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/stretchr/testify/require" +) + +func TestConfigImportAdapter_ConnectedChains_cacheHitReturnsCopy(t *testing.T) { + t.Parallel() + ci := &ConfigImportAdapter{} + chainSel := uint64(123) + cachedChains := []uint64{456, 789} + ci.connectedChainsCache = map[uint64][]uint64{ + chainSel: cachedChains, + } + + var e cldf.Environment + + first, err := ci.ConnectedChains(e, chainSel) + require.NoError(t, err) + require.Equal(t, []uint64{456, 789}, first) + + // Mutate the returned slice; cache should be unaffected. + first[0] = 999 + first = append(first, 111) + + second, err := ci.ConnectedChains(e, chainSel) + require.NoError(t, err) + require.Equal(t, []uint64{456, 789}, second, "cache should return a copy; mutating first result must not affect second call") +} + +func TestConfigImportAdapter_ConnectedChains_cachePerChainSelector(t *testing.T) { + t.Parallel() + ci := &ConfigImportAdapter{} + ci.connectedChainsCache = map[uint64][]uint64{ + 100: {200, 201}, + 300: {301}, + } + + var e cldf.Environment + + got100, err := ci.ConnectedChains(e, 100) + require.NoError(t, err) + require.Equal(t, []uint64{200, 201}, got100) + + got300, err := ci.ConnectedChains(e, 300) + require.NoError(t, err) + require.Equal(t, []uint64{301}, got300) +} diff --git a/chains/evm/deployment/v1_6_0/adapters/configimport.go b/chains/evm/deployment/v1_6_0/adapters/configimport.go index 34adab580..52dfe45a4 100644 --- a/chains/evm/deployment/v1_6_0/adapters/configimport.go +++ b/chains/evm/deployment/v1_6_0/adapters/configimport.go @@ -2,18 +2,17 @@ package adapters import ( "fmt" + "sync" "github.com/Masterminds/semver/v3" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" cldf_ops "github.com/smartcontractkit/chainlink-deployments-framework/operations" - "github.com/smartcontractkit/chainlink-ccip/chains/evm/gobindings/generated/v1_2_0/router" - evm_datastore_utils "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/utils/datastore" + adapters1_2 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/adapters" routerops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_2_0/operations/router" adapters1_5 "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/adapters" tokenadminops "github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment/v1_5_0/operations/token_admin_registry" @@ -32,6 +31,12 @@ type ConfigImportAdapter struct { OffRamp common.Address Router common.Address TokenAdminReg common.Address + + // connectedChainsCache memoizes the result of ConnectedChains per chain selector + // to avoid duplicate (potentially expensive) RPC work when the method is called + // multiple times for the same chain within the same adapter instance. + connectedChainsCache map[uint64][]uint64 + connectedChainsMu sync.Mutex } func (ci *ConfigImportAdapter) InitializeAdapter(e cldf.Environment, chainSelector uint64) error { @@ -90,49 +95,52 @@ func (ci *ConfigImportAdapter) SupportedTokensPerRemoteChain(e cldf.Environment, if !ok { return nil, fmt.Errorf("chain with selector %d not found in environment", chainsel) } + remoteChains, err := ci.ConnectedChains(e, chainsel) + if err != nil { + return nil, fmt.Errorf("failed to get connected chains for chain %d: %w", chainsel, err) + } // get all supported tokens from token admin registry - return adapters1_5.GetSupportedTokensPerRemoteChain(e.GetContext(), e.Logger, ci.TokenAdminReg, chain) + return adapters1_5.GetSupportedTokensPerRemoteChain(e.GetContext(), e.Logger, ci.TokenAdminReg, chain, remoteChains) } func (ci *ConfigImportAdapter) ConnectedChains(e cldf.Environment, chainsel uint64) ([]uint64, error) { - chain, ok := e.BlockChains.EVMChains()[chainsel] - if !ok { - return nil, fmt.Errorf("chain with selector %d not found in environment", chainsel) - } - routerAddr := ci.Router - if routerAddr == (common.Address{}) { - return nil, fmt.Errorf("router address not initialized for chain %d", chainsel) + // Fast path: return cached result if available to avoid duplicate RPC work. + ci.connectedChainsMu.Lock() + if ci.connectedChainsCache == nil { + ci.connectedChainsCache = make(map[uint64][]uint64) } - // get all offRamps from router to find connected chains - routerC, err := router.NewRouter(ci.Router, chain.Client) - if err != nil { - return nil, fmt.Errorf("failed to instantiate router contract at %s on chain %d: %w", routerAddr.String(), chain.Selector, err) + if cached, ok := ci.connectedChainsCache[chainsel]; ok { + // Return a copy to prevent callers from mutating the cached slice. + result := make([]uint64, len(cached)) + copy(result, cached) + ci.connectedChainsMu.Unlock() + return result, nil } - offRamps, err := routerC.GetOffRamps(&bind.CallOpts{ - Context: e.GetContext(), - }) + ci.connectedChainsMu.Unlock() + + var connected []uint64 + laneResolver := adapters1_2.LaneVersionResolver{} + remoteChainToVersionMap, _, err := laneResolver.DeriveLaneVersionsForChain(e, chainsel) if err != nil { - return nil, fmt.Errorf("failed to get off ramps from router at %s on chain %d: %w", routerAddr.String(), chain.Selector, err) + return nil, fmt.Errorf("failed to derive lane versions for chain %d: %w", chainsel, err) } - connectedChains := make([]uint64, 0) - for _, offRamp := range offRamps { - // if the offramp's address matches our offramp, then we are connected to the source chain via 1.6 - if offRamp.OffRamp == ci.OffRamp { - // get the onRamp on router for the source chain and check if it matches our onRamp, if it does then we are connected to that chain - // lanes are always bi-directional so source and destination chain selectors are interchangeable for the purpose of finding connected chains - onRamp, err := routerC.GetOnRamp(&bind.CallOpts{ - Context: e.GetContext(), - }, offRamp.SourceChainSelector) - if err != nil { - return nil, fmt.Errorf("failed to get on ramp for source chain selector %d from router at %s on chain %d: %w", offRamp.SourceChainSelector, routerAddr.String(), chain.Selector, err) - } - if onRamp != ci.OnRamp { - continue - } - connectedChains = append(connectedChains, offRamp.SourceChainSelector) + for destSel, version := range remoteChainToVersionMap { + if version.Equal(semver.MustParse("1.6.0")) { + connected = append(connected, destSel) } } - return connectedChains, nil + + // Cache the computed result for subsequent calls. + ci.connectedChainsMu.Lock() + if ci.connectedChainsCache == nil { + ci.connectedChainsCache = make(map[uint64][]uint64) + } + cached := make([]uint64, len(connected)) + copy(cached, connected) + ci.connectedChainsCache[chainsel] = cached + ci.connectedChainsMu.Unlock() + + return connected, nil } func (ci *ConfigImportAdapter) SequenceImportConfig() *cldf_ops.Sequence[api.ImportConfigPerChainInput, sequences.OnChainOutput, cldf_chain.BlockChains] { diff --git a/chains/evm/deployment/v1_6_0/adapters/configimport_test.go b/chains/evm/deployment/v1_6_0/adapters/configimport_test.go new file mode 100644 index 000000000..529fbcd7a --- /dev/null +++ b/chains/evm/deployment/v1_6_0/adapters/configimport_test.go @@ -0,0 +1,51 @@ +package adapters + +import ( + "testing" + + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + "github.com/stretchr/testify/require" +) + +func TestConfigImportAdapter_ConnectedChains_cacheHitReturnsCopy(t *testing.T) { + t.Parallel() + ci := &ConfigImportAdapter{} + chainSel := uint64(123) + cachedChains := []uint64{456, 789} + ci.connectedChainsCache = map[uint64][]uint64{ + chainSel: cachedChains, + } + + var e cldf.Environment + + first, err := ci.ConnectedChains(e, chainSel) + require.NoError(t, err) + require.Equal(t, []uint64{456, 789}, first) + + // Mutate the returned slice; cache should be unaffected. + first[0] = 999 + first = append(first, 111) + + second, err := ci.ConnectedChains(e, chainSel) + require.NoError(t, err) + require.Equal(t, []uint64{456, 789}, second, "cache should return a copy; mutating first result must not affect second call") +} + +func TestConfigImportAdapter_ConnectedChains_cachePerChainSelector(t *testing.T) { + t.Parallel() + ci := &ConfigImportAdapter{} + ci.connectedChainsCache = map[uint64][]uint64{ + 100: {200, 201}, + 300: {301}, + } + + var e cldf.Environment + + got100, err := ci.ConnectedChains(e, 100) + require.NoError(t, err) + require.Equal(t, []uint64{200, 201}, got100) + + got300, err := ci.ConnectedChains(e, 300) + require.NoError(t, err) + require.Equal(t, []uint64{301}, got300) +}