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
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ func (a *EVMIndexerConfigAdapter) ResolveVerifierAddresses(
)

if len(refs) == 0 {
return nil, fmt.Errorf("no %s verifier addresses found for chain %d with qualifier %q", kind, chainSelector, qualifier)
return nil, &ccvadapters.MissingIndexerVerifierAddressesError{
Kind: kind,
ChainSelector: chainSelector,
Qualifier: qualifier,
}
}

addresses := make([]string, 0, len(refs))
Expand Down
21 changes: 21 additions & 0 deletions deployment/v1_7_0/adapters/indexer_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ const (
LombardVerifierKind VerifierKind = "lombard"
)

type MissingIndexerVerifierAddressesError struct {
Kind VerifierKind
ChainSelector uint64
Qualifier string
}

func (e *MissingIndexerVerifierAddressesError) Error() string {
return fmt.Sprintf(
"no %s verifier addresses found for chain %d with qualifier %q",
e.Kind,
e.ChainSelector,
e.Qualifier,
)
}

type IndexerConfigAdapter interface {
ResolveVerifierAddresses(ds datastore.DataStore, chainSelector uint64, qualifier string, kind VerifierKind) ([]string, error)
}
Expand Down Expand Up @@ -75,3 +90,9 @@ func (r *IndexerConfigRegistry) GetByChain(chainSelector uint64) (IndexerConfigA
}
return adapter, nil
}

func (r *IndexerConfigRegistry) HasAdapters() bool {
r.mu.Lock()
defer r.mu.Unlock()
return len(r.adapters) > 0
}
47 changes: 35 additions & 12 deletions deployment/v1_7_0/changesets/generate_aggregator_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
type GenerateAggregatorConfigInput struct {
ServiceIdentifier string
CommitteeQualifier string
ChainSelectors []uint64
Topology *offchain.EnvironmentTopology
}

func GenerateAggregatorConfig(registry *adapters.AggregatorConfigRegistry) deployment.ChangeSetV2[GenerateAggregatorConfigInput] {
Expand All @@ -27,19 +27,14 @@ func GenerateAggregatorConfig(registry *adapters.AggregatorConfigRegistry) deplo
if cfg.CommitteeQualifier == "" {
return fmt.Errorf("committee qualifier is required")
}
envSelectors := e.BlockChains.ListChainSelectors()
for _, s := range cfg.ChainSelectors {
if !slices.Contains(envSelectors, s) {
return fmt.Errorf("selector %d is not available in environment", s)
}
}
return nil
_, err := resolveAggregatorChainSelectors(e, cfg)
return err
}

apply := func(e deployment.Environment, cfg GenerateAggregatorConfigInput) (deployment.ChangesetOutput, error) {
chainSelectors := cfg.ChainSelectors
if len(chainSelectors) == 0 {
chainSelectors = e.BlockChains.ListChainSelectors()
chainSelectors, err := resolveAggregatorChainSelectors(e, cfg)
if err != nil {
return deployment.ChangesetOutput{}, err
}

committee, err := buildAggregatorCommittee(e, registry, cfg.CommitteeQualifier, chainSelectors)
Expand All @@ -66,6 +61,34 @@ func GenerateAggregatorConfig(registry *adapters.AggregatorConfigRegistry) deplo
return deployment.CreateChangeSet(apply, validate)
}

func resolveAggregatorChainSelectors(e deployment.Environment, cfg GenerateAggregatorConfigInput) ([]uint64, error) {
if cfg.Topology == nil {
return nil, fmt.Errorf("topology is required")
}
if cfg.Topology.NOPTopology == nil {
return nil, fmt.Errorf("NOP topology is required")
}

committee, ok := cfg.Topology.NOPTopology.Committees[cfg.CommitteeQualifier]
if !ok {
return nil, fmt.Errorf("committee %q not found in topology", cfg.CommitteeQualifier)
}

chainSelectors, err := getCommitteeChainSelectors(committee)
if err != nil {
return nil, err
}

envSelectors := e.BlockChains.ListChainSelectors()
for _, s := range chainSelectors {
if !slices.Contains(envSelectors, s) {
return nil, fmt.Errorf("committee %q references chain selector %d which is not available in environment", cfg.CommitteeQualifier, s)
}
}

return chainSelectors, nil
}

func buildAggregatorCommittee(
e deployment.Environment,
registry *adapters.AggregatorConfigRegistry,
Expand Down Expand Up @@ -105,7 +128,7 @@ func buildAggregatorCommittee(

committeeStates, ok := allCommittees[committeeQualifier]
if !ok || len(committeeStates) == 0 {
return nil, fmt.Errorf("committee %q not found in on-chain topology", committeeQualifier)
return nil, fmt.Errorf("committee %q not found in deployed verifier state", committeeQualifier)
}

quorumConfigs, err := buildQuorumConfigs(e.DataStore, registry, committeeStates, committeeQualifier, chainSelectors)
Expand Down
71 changes: 59 additions & 12 deletions deployment/v1_7_0/changesets/generate_aggregator_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ func newAggregatorConfigRegistry() *adapters.AggregatorConfigRegistry {
return &adapters.AggregatorConfigRegistry{}
}

func newTopologyForCommittee(qualifier string, selectors []uint64) *offchain.EnvironmentTopology {
chainConfigs := make(map[string]offchain.ChainCommitteeConfig, len(selectors))
for _, sel := range selectors {
chainConfigs[strconv.FormatUint(sel, 10)] = offchain.ChainCommitteeConfig{}
}

return &offchain.EnvironmentTopology{
NOPTopology: &offchain.NOPTopology{
Committees: map[string]offchain.CommitteeConfig{
qualifier: {
Qualifier: qualifier,
ChainConfigs: chainConfigs,
},
},
},
}
}

func TestGenerateAggregatorConfig_Validation(t *testing.T) {
sel1 := chainsel.TEST_90000001.Selector
sel2 := chainsel.TEST_90000002.Selector
Expand Down Expand Up @@ -86,13 +104,41 @@ func TestGenerateAggregatorConfig_Validation(t *testing.T) {
errContains: "committee qualifier is required",
},
{
name: "unknown chain selector",
name: "missing topology",
input: changesets.GenerateAggregatorConfigInput{
ServiceIdentifier: "agg",
CommitteeQualifier: "default",
},
errContains: "topology is required",
},
{
name: "missing nop topology",
input: changesets.GenerateAggregatorConfigInput{
ServiceIdentifier: "agg",
CommitteeQualifier: "default",
ChainSelectors: []uint64{9999},
Topology: &offchain.EnvironmentTopology{},
},
errContains: "selector 9999 is not available in environment",
errContains: "NOP topology is required",
},
{
name: "missing committee in topology",
input: changesets.GenerateAggregatorConfigInput{
ServiceIdentifier: "agg",
CommitteeQualifier: "default",
Topology: &offchain.EnvironmentTopology{
NOPTopology: &offchain.NOPTopology{Committees: map[string]offchain.CommitteeConfig{}},
},
},
errContains: `committee "default" not found in topology`,
},
{
name: "topology chain missing from environment",
input: changesets.GenerateAggregatorConfigInput{
ServiceIdentifier: "agg",
CommitteeQualifier: "default",
Topology: newTopologyForCommittee("default", []uint64{sel1, 9999}),
},
errContains: `committee "default" references chain selector 9999 which is not available in environment`,
},
}

Expand All @@ -112,6 +158,7 @@ func TestGenerateAggregatorConfig_Validation(t *testing.T) {
func TestGenerateAggregatorConfig_BuildsCorrectConfig(t *testing.T) {
sel1 := chainsel.TEST_90000001.Selector
sel2 := chainsel.TEST_90000002.Selector
sel3 := chainsel.TEST_90000003.Selector

sel1Str := strconv.FormatUint(sel1, 10)
sel2Str := strconv.FormatUint(sel2, 10)
Expand Down Expand Up @@ -157,14 +204,14 @@ func TestGenerateAggregatorConfig_BuildsCorrectConfig(t *testing.T) {
ds := datastore.NewMemoryDataStore()
env := deployment.Environment{
DataStore: ds.Seal(),
BlockChains: newTestBlockChains([]uint64{sel1, sel2}),
BlockChains: newTestBlockChains([]uint64{sel1, sel2, sel3}),
}

cs := changesets.GenerateAggregatorConfig(registry)
output, err := cs.Apply(env, changesets.GenerateAggregatorConfigInput{
ServiceIdentifier: "test-aggregator",
CommitteeQualifier: qualifier,
ChainSelectors: []uint64{sel1, sel2},
Topology: newTopologyForCommittee(qualifier, []uint64{sel1, sel2}),
})
require.NoError(t, err)
require.NotNil(t, output.DataStore)
Expand Down Expand Up @@ -210,7 +257,7 @@ func TestGenerateAggregatorConfig_ScanError(t *testing.T) {
_, err := cs.Apply(env, changesets.GenerateAggregatorConfigInput{
ServiceIdentifier: "test-aggregator",
CommitteeQualifier: "default",
ChainSelectors: []uint64{sel1},
Topology: newTopologyForCommittee("default", []uint64{sel1}),
})
require.Error(t, err)
assert.Contains(t, err.Error(), "scan failed")
Expand Down Expand Up @@ -247,7 +294,7 @@ func TestGenerateAggregatorConfig_ResolveAddressError(t *testing.T) {
_, err := cs.Apply(env, changesets.GenerateAggregatorConfigInput{
ServiceIdentifier: "test-aggregator",
CommitteeQualifier: qualifier,
ChainSelectors: []uint64{sel1},
Topology: newTopologyForCommittee(qualifier, []uint64{sel1}),
})
require.Error(t, err)
assert.Contains(t, err.Error(), "address not found")
Expand All @@ -271,10 +318,10 @@ func TestGenerateAggregatorConfig_CommitteeNotFound(t *testing.T) {
_, err := cs.Apply(env, changesets.GenerateAggregatorConfigInput{
ServiceIdentifier: "test-aggregator",
CommitteeQualifier: "default",
ChainSelectors: []uint64{sel1},
Topology: newTopologyForCommittee("default", []uint64{sel1}),
})
require.Error(t, err)
assert.Contains(t, err.Error(), `committee "default" not found`)
assert.Contains(t, err.Error(), `committee "default" not found in deployed verifier state`)
}

func TestGenerateAggregatorConfig_DuplicateSourceChainAcrossDestinationsSucceeds(t *testing.T) {
Expand Down Expand Up @@ -336,7 +383,7 @@ func TestGenerateAggregatorConfig_DuplicateSourceChainAcrossDestinationsSucceeds
output, err := cs.Apply(env, changesets.GenerateAggregatorConfigInput{
ServiceIdentifier: "test-aggregator",
CommitteeQualifier: qualifier,
ChainSelectors: []uint64{destChainA, destChainB, sourceChain},
Topology: newTopologyForCommittee(qualifier, []uint64{destChainA, destChainB, sourceChain}),
})
require.NoError(t, err)
require.NotNil(t, output.DataStore)
Expand Down Expand Up @@ -389,7 +436,7 @@ func TestGenerateAggregatorConfig_DuplicateQualifierOnSameChainFails(t *testing.
_, err := cs.Apply(env, changesets.GenerateAggregatorConfigInput{
ServiceIdentifier: "test-aggregator",
CommitteeQualifier: qualifier,
ChainSelectors: []uint64{sel1},
Topology: newTopologyForCommittee(qualifier, []uint64{sel1}),
})
require.Error(t, err)
assert.Contains(t, err.Error(), "multiple committee verifiers with qualifier")
Expand All @@ -408,7 +455,7 @@ func TestGenerateAggregatorConfig_MissingAdapterForFamily(t *testing.T) {
_, err := cs.Apply(env, changesets.GenerateAggregatorConfigInput{
ServiceIdentifier: "test-aggregator",
CommitteeQualifier: "default",
ChainSelectors: []uint64{sel1},
Topology: newTopologyForCommittee("default", []uint64{sel1}),
})
require.Error(t, err)
assert.Contains(t, err.Error(), "no offchain config adapter registered")
Expand Down
28 changes: 14 additions & 14 deletions deployment/v1_7_0/changesets/generate_indexer_config.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package changesets

import (
"errors"
"fmt"
"slices"
"sort"

"github.com/smartcontractkit/chainlink-deployments-framework/datastore"
Expand All @@ -17,7 +17,6 @@ type GenerateIndexerConfigInput struct {
CommitteeVerifierNameToQualifier map[string]string
CCTPVerifierNameToQualifier map[string]string
LombardVerifierNameToQualifier map[string]string
ChainSelectors []uint64
}

func GenerateIndexerConfig(registry *adapters.IndexerConfigRegistry) deployment.ChangeSetV2[GenerateIndexerConfigInput] {
Expand All @@ -30,22 +29,11 @@ func GenerateIndexerConfig(registry *adapters.IndexerConfigRegistry) deployment.
len(cfg.LombardVerifierNameToQualifier) == 0 {
return fmt.Errorf("at least one verifier name to qualifier mapping is required")
}
envSelectors := e.BlockChains.ListChainSelectors()
for _, s := range cfg.ChainSelectors {
if !slices.Contains(envSelectors, s) {
return fmt.Errorf("selector %d is not available in environment", s)
}
}
return nil
}

apply := func(e deployment.Environment, cfg GenerateIndexerConfigInput) (deployment.ChangesetOutput, error) {
selectors := cfg.ChainSelectors
if len(selectors) == 0 {
selectors = e.BlockChains.ListChainSelectors()
}

verifierMap, err := buildIndexerVerifierMap(e.DataStore, registry, selectors, cfg)
verifierMap, err := buildIndexerVerifierMap(e.DataStore, registry, e.BlockChains.ListChainSelectors(), cfg)
if err != nil {
return deployment.ChangesetOutput{}, err
}
Expand Down Expand Up @@ -108,6 +96,10 @@ func collectVerifierAddresses(
qualifier string,
kind adapters.VerifierKind,
) ([]string, error) {
if !registry.HasAdapters() {
return nil, fmt.Errorf("no indexer config adapter registered")
}

seen := make(map[string]bool)
var addresses []string

Expand All @@ -119,6 +111,10 @@ func collectVerifierAddresses(

addrs, err := adapter.ResolveVerifierAddresses(ds, sel, qualifier, kind)
if err != nil {
var missingErr *adapters.MissingIndexerVerifierAddressesError
if errors.As(err, &missingErr) {
Comment on lines 111 to +115
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because GenerateIndexerConfig now always uses env.BlockChains.ListChainSelectors(), environments containing chain families without a registered IndexerConfigAdapter will fail hard at registry.GetByChain(sel). This is a regression from being able to restrict selectors via input. Consider treating “no adapter registered for chain family” as a non-fatal condition (skip that selector), ideally via a typed error from GetByChain / a HasAdapterForChain helper, so mixed-family environments can still generate indexer config for supported chains.

Copilot uses AI. Check for mistakes.
continue
}
Comment on lines 112 to +117
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new behavior that ignores adapters.MissingIndexerVerifierAddressesError via errors.As isn’t exercised by the current tests (the mock adapter returns (nil, nil) on missing data). Add a test case where the adapter returns MissingIndexerVerifierAddressesError for one selector to ensure the changeset correctly skips that chain and still succeeds when other chains provide addresses.

Copilot uses AI. Check for mistakes.
return nil, fmt.Errorf("chain %d: %w", sel, err)
}

Expand All @@ -130,6 +126,10 @@ func collectVerifierAddresses(
}
}

if len(addresses) == 0 {
return nil, fmt.Errorf("no deployed %s verifier addresses found for qualifier %q", kind, qualifier)
}

return addresses, nil
}

Expand Down
Loading
Loading