diff --git a/deployment/v2_0_0/changesets/build_lanes_cross_family.go b/deployment/v2_0_0/changesets/build_lanes_cross_family.go index 03f7bd0ee..10f55f82b 100644 --- a/deployment/v2_0_0/changesets/build_lanes_cross_family.go +++ b/deployment/v2_0_0/changesets/build_lanes_cross_family.go @@ -61,6 +61,28 @@ type ConfigureChainsForLanesFromTopologyConfig struct { BuildLanesCrossFamilyConfig } +// committeeQualifiersForLaneLeg returns the committee qualifiers that apply to the leg +// from local → remote. A committee applies only if both the local and remote chain +// selectors appear in its ChainConfigs — meaning the committee is configured for both +// sides of the lane. When committees is nil, the single default qualifier is returned. +func committeeQualifiersForLaneLeg(local, remote uint64, committees map[string]offchain.CommitteeConfig) ([]string, error) { + if committees == nil { + return []string{defaultQualifier}, nil + } + localKey := strconv.FormatUint(local, 10) + remoteKey := strconv.FormatUint(remote, 10) + qualifiers := make([]string, 0, len(committees)) + for q, committee := range committees { + _, hasLocal := committee.ChainConfigs[localKey] + _, hasRemote := committee.ChainConfigs[remoteKey] + if hasLocal && hasRemote { + qualifiers = append(qualifiers, q) + } + } + sort.Strings(qualifiers) + return qualifiers, nil +} + // expandLanesToPartialChainConfigs converts bidirectional lane pairs into the internal // chain-centric representation used by the changeset apply path. func expandLanesToPartialChainConfigs(lanes []CrossFamilyLanePair, committees map[string]offchain.CommitteeConfig) ([]partialChainConfig, error) { @@ -77,35 +99,14 @@ func expandLanesToPartialChainConfigs(lanes []CrossFamilyLanePair, committees ma return nil, fmt.Errorf("lane %d: chainA and chainB must differ", i) } var qualifiers []string - remoteKey := strconv.FormatUint(lane.ChainB, 10) - for _, cvCfg := range committees { - if _, exists := cvCfg.ChainConfigs[remoteKey]; exists { - qualifiers = append(qualifiers, cvCfg.Qualifier) - } - } - sort.Strings(qualifiers) - if len(qualifiers) == 0 { - if len(committees) == 0 { - qualifiers = []string{defaultQualifier} - } else { - return nil, fmt.Errorf("lane %d: no committees have chain_config for remote chain %d", i, lane.ChainB) - } + qualifiers, err := committeeQualifiersForLaneLeg(lane.ChainA, lane.ChainB, committees) + if err != nil { + return nil, fmt.Errorf("lane %d: %w", i, err) } mergeLaneLeg(byChain, lane.ChainA, lane.ChainB, qualifiers, lane.ChainAOverrides) - qualifiers = nil - remoteKey = strconv.FormatUint(lane.ChainA, 10) - for _, cvCfg := range committees { - if _, exists := cvCfg.ChainConfigs[remoteKey]; exists { - qualifiers = append(qualifiers, cvCfg.Qualifier) - } - } - sort.Strings(qualifiers) - if len(qualifiers) == 0 { - if len(committees) == 0 { - qualifiers = []string{defaultQualifier} - } else { - return nil, fmt.Errorf("lane %d: no committees have chain_config for remote chain %d", i, lane.ChainA) - } + qualifiers, err = committeeQualifiersForLaneLeg(lane.ChainB, lane.ChainA, committees) + if err != nil { + return nil, fmt.Errorf("lane %d: %w", i, err) } mergeLaneLeg(byChain, lane.ChainB, lane.ChainA, qualifiers, lane.ChainBOverrides) } diff --git a/deployment/v2_0_0/changesets/build_lanes_cross_family_internal_test.go b/deployment/v2_0_0/changesets/build_lanes_cross_family_internal_test.go index 50286503c..bb7620554 100644 --- a/deployment/v2_0_0/changesets/build_lanes_cross_family_internal_test.go +++ b/deployment/v2_0_0/changesets/build_lanes_cross_family_internal_test.go @@ -128,11 +128,12 @@ func TestExpandLanesToPartialChainConfigs_MultipleCommittees(t *testing.T) { assert.Contains(t, cfgB.CommitteeVerifiers[1].RemoteChains, chainA) } -func TestExpandLanesToPartialChainConfigs_FiltersCommitteeQualifiersPerRemoteChain(t *testing.T) { +func TestExpandLanesToPartialChainConfigs_FiltersCommitteeQualifiersPerLaneLeg(t *testing.T) { chainA := uint64(100) chainB := uint64(200) // alpha is on both chains; beta only on chainA; gamma only on chainB. + // Only alpha covers both sides of the lane, so it is the only qualifier for either leg. committees := map[string]offchain.CommitteeConfig{ "alpha": { Qualifier: "alpha", @@ -165,21 +166,17 @@ func TestExpandLanesToPartialChainConfigs_FiltersCommitteeQualifiersPerRemoteCha bySel[c.ChainSelector] = c } + // beta (chainA only) and gamma (chainB only) are excluded from both legs; + // only alpha spans both chains. cfgA := bySel[chainA] - require.Len(t, cfgA.CommitteeVerifiers, 2) + require.Len(t, cfgA.CommitteeVerifiers, 1) assert.Equal(t, "alpha", cfgA.CommitteeVerifiers[0].CommitteeQualifier) - assert.Equal(t, "gamma", cfgA.CommitteeVerifiers[1].CommitteeQualifier) - for _, cv := range cfgA.CommitteeVerifiers { - assert.Contains(t, cv.RemoteChains, chainB) - } + assert.Contains(t, cfgA.CommitteeVerifiers[0].RemoteChains, chainB) cfgB := bySel[chainB] - require.Len(t, cfgB.CommitteeVerifiers, 2) + require.Len(t, cfgB.CommitteeVerifiers, 1) assert.Equal(t, "alpha", cfgB.CommitteeVerifiers[0].CommitteeQualifier) - assert.Equal(t, "beta", cfgB.CommitteeVerifiers[1].CommitteeQualifier) - for _, cv := range cfgB.CommitteeVerifiers { - assert.Contains(t, cv.RemoteChains, chainA) - } + assert.Contains(t, cfgB.CommitteeVerifiers[0].RemoteChains, chainA) } func TestExpandLanesToPartialChainConfigs_ChainOverridesCommitteeVerifierFinalityConfig(t *testing.T) { @@ -224,12 +221,12 @@ func TestExpandLanesToPartialChainConfigs_ChainOverridesCommitteeVerifierFinalit cfg = c } } - require.Len(t, cfg.CommitteeVerifiers, 2) + // Only alpha covers both chainA and chainB; beta/gamma are single-chain. + require.Len(t, cfg.CommitteeVerifiers, 1) for _, cv := range cfg.CommitteeVerifiers { require.NotNil(t, cv.AllowedFinalityConfig) require.Equal(t, *finalityCfg, *cv.AllowedFinalityConfig) require.Contains(t, cv.RemoteChains, chainB) - } }