Skip to content
Merged
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
137 changes: 96 additions & 41 deletions chains/evm/deployment/v1_5_0/adapters/configimport.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")) {
Comment on lines +149 to +150
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
}

Expand All @@ -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] {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
})
}
Expand Down
51 changes: 51 additions & 0 deletions chains/evm/deployment/v1_5_0/adapters/configimport_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
80 changes: 44 additions & 36 deletions chains/evm/deployment/v1_6_0/adapters/configimport.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
}
Comment on lines +121 to 130
}
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] {
Expand Down
Loading
Loading