diff --git a/build/devenv/common/common.go b/build/devenv/common/common.go index d7d3caedc..5cc750e88 100644 --- a/build/devenv/common/common.go +++ b/build/devenv/common/common.go @@ -397,9 +397,9 @@ func qualifiersAvailable(qualifiers []string, topology *offchain.EnvironmentTopo } // FilterTokenCombinations returns only the token combinations whose CCV qualifiers -// all exist as committees in the topology, and when ds is non-nil, whose local and -// remote pool address refs exist in ds for every selector (each chain deploys both -// pools across bidirectional transfer configs). +// all exist as committees in the topology. When ds is non-nil, selectors[0] is treated +// as the local chain and selectors[1:] as candidate remotes, and the filter keeps only +// combinations whose declared local->remote orientation exists in the datastore. // Pass ds nil to skip the datastore check. func FilterTokenCombinations(combos []TokenCombination, topology *offchain.EnvironmentTopology, ds datastore.DataStore, selectors []uint64) []TokenCombination { filtered := make([]TokenCombination, 0, len(combos)) @@ -417,14 +417,21 @@ func FilterTokenCombinations(combos []TokenCombination, topology *offchain.Envir } func tokenCombinationPoolsExistInDataStore(ds datastore.DataStore, selectors []uint64, combo TokenCombination) bool { - local := combo.LocalPoolAddressRef() - remote := combo.RemotePoolAddressRef() - for _, sel := range selectors { - if !dataStoreHasAddressRef(ds, sel, local) || !dataStoreHasAddressRef(ds, sel, remote) { - return false + if len(selectors) == 0 { + return true + } + if !dataStoreHasAddressRef(ds, selectors[0], combo.LocalPoolAddressRef()) { + return false + } + // A combo is usable for a selector if that selector has the local pool and at + // least one remote selector has the corresponding remote pool. Requiring the + // same remote pool to exist on every remote selector drops valid mixed lanes. + for _, sel := range selectors[1:] { + if dataStoreHasAddressRef(ds, sel, combo.RemotePoolAddressRef()) { + return true } } - return true + return len(selectors) == 1 } func dataStoreHasAddressRef(ds datastore.DataStore, chainSelector uint64, ref datastore.AddressRef) bool { diff --git a/build/devenv/evm/impl.go b/build/devenv/evm/impl.go index d90ae4c23..e2ea1d486 100644 --- a/build/devenv/evm/impl.go +++ b/build/devenv/evm/impl.go @@ -1321,25 +1321,38 @@ func (m *CCIP17EVMConfig) GetTokenTransferConfigs( applicableCombos := devenvcommon.FilterTokenCombinations( devenvcommon.AllTokenCombinations(), topology, env.DataStore, append([]uint64{selector}, remoteSelectors...), ) + hasAddressRef := func(chainSelector uint64, ref datastore.AddressRef) bool { + _, err := env.DataStore.Addresses().Get(datastore.NewAddressRefKey( + chainSelector, + ref.Type, + ref.Version, + ref.Qualifier, + )) + return err == nil + } merged := make(map[string]tokenscore.TokenTransferConfig) for _, combo := range applicableCombos { - for _, pair := range []struct { - local, remote datastore.AddressRef - ccvQuals []string - }{ - {combo.LocalPoolAddressRef(), combo.RemotePoolAddressRef(), combo.LocalPoolCCVQualifiers()}, - {combo.RemotePoolAddressRef(), combo.LocalPoolAddressRef(), combo.RemotePoolCCVQualifiers()}, - } { - cfg := m.buildEVMTokenTransferConfig(selector, remoteSelectors, pair.local, pair.remote, pair.ccvQuals) - key := string(cfg.TokenPoolRef.Type) + "\x00" + cfg.TokenPoolRef.Version.String() + "\x00" + cfg.TokenPoolRef.Qualifier - if existing, ok := merged[key]; ok { - maps.Copy(existing.RemoteChains, cfg.RemoteChains) - merged[key] = existing - } else { - merged[key] = cfg + eligibleRemoteSelectors := make([]uint64, 0, len(remoteSelectors)) + // Emit only the remote selectors that actually have the pool required by + // this combo. ConfigureTokensForTransfers expects every advertised remote + // to have a reciprocal config on the other side. + for _, rs := range remoteSelectors { + if hasAddressRef(rs, combo.RemotePoolAddressRef()) { + eligibleRemoteSelectors = append(eligibleRemoteSelectors, rs) } } + if len(eligibleRemoteSelectors) == 0 { + continue + } + cfg := m.buildEVMTokenTransferConfig(selector, eligibleRemoteSelectors, combo.LocalPoolAddressRef(), combo.RemotePoolAddressRef(), combo.LocalPoolCCVQualifiers()) + key := string(cfg.TokenPoolRef.Type) + "\x00" + cfg.TokenPoolRef.Version.String() + "\x00" + cfg.TokenPoolRef.Qualifier + if existing, ok := merged[key]; ok { + maps.Copy(existing.RemoteChains, cfg.RemoteChains) + merged[key] = existing + } else { + merged[key] = cfg + } } configs := make([]tokenscore.TokenTransferConfig, 0, len(merged)) @@ -1357,16 +1370,15 @@ func (m *CCIP17EVMConfig) buildEVMTokenTransferConfig( ccvQualifiers []string, ) tokenscore.TokenTransferConfig { remoteChains := make(map[uint64]tokenscore.RemoteChainConfig[*datastore.AddressRef, datastore.AddressRef]) + ccvRefs := make([]datastore.AddressRef, 0, len(ccvQualifiers)) + for _, qualifier := range ccvQualifiers { + ccvRefs = append(ccvRefs, datastore.AddressRef{ + Type: datastore.ContractType(versioned_verifier_resolver.CommitteeVerifierResolverType), + Version: versioned_verifier_resolver.Version, + Qualifier: qualifier, + }) + } for _, rs := range remoteSelectors { - ccvRefs := make([]datastore.AddressRef, 0, len(ccvQualifiers)) - for _, qualifier := range ccvQualifiers { - ccvRefs = append(ccvRefs, datastore.AddressRef{ - Type: datastore.ContractType(versioned_verifier_resolver.CommitteeVerifierResolverType), - Version: versioned_verifier_resolver.Version, - Qualifier: qualifier, - }) - } - remoteChains[rs] = tokenscore.RemoteChainConfig[*datastore.AddressRef, datastore.AddressRef]{ RemotePool: &remoteRef, DefaultFinalityInboundRateLimiterConfig: tokenscore.RateLimiterConfigFloatInput{}, @@ -1381,6 +1393,11 @@ func (m *CCIP17EVMConfig) buildEVMTokenTransferConfig( return tokenscore.TokenTransferConfig{ ChainSelector: selector, TokenPoolRef: localRef, + TokenRef: datastore.AddressRef{ + Type: datastore.ContractType(bnm_drip_v1_0.ContractType), + Version: semver.MustParse(bnm_drip_v1_0.Deploy.Version()), + Qualifier: localRef.Qualifier, + }, RegistryRef: datastore.AddressRef{ Type: datastore.ContractType(token_admin_registry.ContractType), Version: semver.MustParse(token_admin_registry.Deploy.Version()), diff --git a/build/devenv/implcommon.go b/build/devenv/implcommon.go index 012a30e0b..bdfe2f049 100644 --- a/build/devenv/implcommon.go +++ b/build/devenv/implcommon.go @@ -3,6 +3,7 @@ package ccv import ( "context" "fmt" + "maps" "sort" "github.com/Masterminds/semver/v3" @@ -526,17 +527,30 @@ func ConfigureAllTokenTransfers( env *deployment.Environment, topology *ccipOffchain.EnvironmentTopology, ) error { - // poolIdentityKey returns a key that groups configs across chains for the - // same pool type+version+qualifier. - poolIdentityKey := func(cfg *tokenscore.TokenTransferConfig) string { + refKey := func(ref datastore.AddressRef) string { v := "" - if cfg.TokenPoolRef.Version != nil { - v = cfg.TokenPoolRef.Version.String() + if ref.Version != nil { + v = ref.Version.String() } - return string(cfg.TokenPoolRef.Type) + "+" + v + "+" + cfg.TokenPoolRef.Qualifier + return string(ref.Type) + "+" + v + "+" + ref.Qualifier } - byPoolIdentity := make(map[string][]tokenscore.TokenTransferConfig) + // laneKey groups reciprocal configs for the same selector pair by the local + // pool identity each selector contributes. The selector ordering is stable, + // so A(local burn)->B(remote lock) and B(local lock)->A(remote burn) land in + // the same bucket, while the opposite orientation on the same selector pair + // stays distinct. + laneKey := func(local datastore.AddressRef, localSelector uint64, remote datastore.AddressRef, remoteSelector uint64) string { + leftSelector, leftRef := localSelector, local + rightSelector, rightRef := remoteSelector, remote + if leftSelector > rightSelector { + leftSelector, rightSelector = rightSelector, leftSelector + leftRef, rightRef = rightRef, leftRef + } + return fmt.Sprintf("%d:%s<->%d:%s", leftSelector, refKey(leftRef), rightSelector, refKey(rightRef)) + } + + byLane := make(map[string]map[uint64]tokenscore.TokenTransferConfig) for i, impl := range impls { tcp, ok := impl.(cciptestinterfaces.TokenConfigProvider) @@ -555,18 +569,53 @@ func ConfigureAllTokenTransfers( return fmt.Errorf("get token transfer configs for selector %d: %w", selectors[i], err) } for _, cfg := range cfgs { - key := poolIdentityKey(&cfg) - byPoolIdentity[key] = append(byPoolIdentity[key], cfg) + for remoteSelector, remoteCfg := range cfg.RemoteChains { + if remoteCfg.RemotePool == nil { + continue + } + + key := laneKey(cfg.TokenPoolRef, cfg.ChainSelector, *remoteCfg.RemotePool, remoteSelector) + splitCfg := cfg + splitCfg.RemoteChains = map[uint64]tokenscore.RemoteChainConfig[*datastore.AddressRef, datastore.AddressRef]{ + remoteSelector: remoteCfg, + } + + if byLane[key] == nil { + byLane[key] = make(map[uint64]tokenscore.TokenTransferConfig) + } + + if existing, ok := byLane[key][cfg.ChainSelector]; ok { + if refKey(existing.TokenPoolRef) != refKey(splitCfg.TokenPoolRef) { + return fmt.Errorf( + "selector %d produced conflicting local pool configs for lane %s: %s/%s vs %s/%s", + cfg.ChainSelector, + key, + existing.TokenPoolRef.Type, + existing.TokenPoolRef.Qualifier, + splitCfg.TokenPoolRef.Type, + splitCfg.TokenPoolRef.Qualifier, + ) + } + maps.Copy(existing.RemoteChains, splitCfg.RemoteChains) + byLane[key][cfg.ChainSelector] = existing + } else { + byLane[key][cfg.ChainSelector] = splitCfg + } + } } } - if len(byPoolIdentity) == 0 { + if len(byLane) == 0 { return nil } tokenAdapterRegistry := tokenscore.GetTokenAdapterRegistry() mcmsReaderRegistry := changesetscore.GetRegistry() - for _, group := range byPoolIdentity { + for _, groupedBySelector := range byLane { + group := make([]tokenscore.TokenTransferConfig, 0, len(groupedBySelector)) + for _, cfg := range groupedBySelector { + group = append(group, cfg) + } _, err := tokenscore.ConfigureTokensForTransfers(tokenAdapterRegistry, mcmsReaderRegistry).Apply(*env, tokenscore.ConfigureTokensForTransfersConfig{ Tokens: group, })