diff --git a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go index a761a412856..acb8e833a09 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go +++ b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities.go @@ -3,7 +3,6 @@ package changeset import ( "errors" "fmt" - "slices" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/operations" @@ -22,10 +21,10 @@ type AddCapabilitiesInput struct { RegistryChainSel uint64 `json:"registryChainSel" yaml:"registryChainSel"` RegistryQualifier string `json:"registryQualifier" yaml:"registryQualifier"` - MCMSConfig *crecontracts.MCMSConfig `json:"mcmsConfig" yaml:"mcmsConfig"` - DonName string `json:"donName" yaml:"donName"` // optional if DonNames is set; for backward compatibility - DonNames []string `json:"donNames" yaml:"donNames"` // multiple DONs to update - CapabilityConfigs []contracts.CapabilityConfig `json:"capabilityConfigs" yaml:"capabilityConfigs"` + MCMSConfig *crecontracts.MCMSConfig `json:"mcmsConfig" yaml:"mcmsConfig"` + + // DonCapabilityConfigs maps DON name to the list of capability configs for that DON. + DonCapabilityConfigs map[string][]contracts.CapabilityConfig `json:"donCapabilityConfigs" yaml:"donCapabilityConfigs"` // Force indicates whether to force the update even if we cannot validate that all forwarder contracts are ready to accept the new configure version. // This is very dangerous, and could break the whole platform if the forwarders are not ready. Be very careful with this option. @@ -35,29 +34,16 @@ type AddCapabilitiesInput struct { type AddCapabilities struct{} func (u AddCapabilities) VerifyPreconditions(_ cldf.Environment, config AddCapabilitiesInput) error { - if config.DonName != "" && len(config.DonNames) > 0 { - return errors.New("cannot specify both donName and donNames") - } - donNames := u.donNames(config) - if len(donNames) == 0 { - return errors.New("must specify donName or donNames") - } - if slices.Contains(donNames, "") { - return errors.New("donName or donNames cannot contain an empty string") - } - if len(config.CapabilityConfigs) == 0 { - return errors.New("capabilityConfigs is required") - } - return nil -} - -// donNames returns the list of DON names to update (from DonNames or single DonName for backward compatibility). -func (u AddCapabilities) donNames(config AddCapabilitiesInput) []string { - if len(config.DonNames) > 0 { - return config.DonNames + if len(config.DonCapabilityConfigs) == 0 { + return errors.New("donCapabilityConfigs must contain at least one DON entry") } - if config.DonName != "" { - return []string{config.DonName} + for donName, configs := range config.DonCapabilityConfigs { + if donName == "" { + return errors.New("donCapabilityConfigs keys cannot be empty strings") + } + if len(configs) == 0 { + return fmt.Errorf("donCapabilityConfigs[%q] must contain at least one capability config", donName) + } } return nil } @@ -79,11 +65,10 @@ func (u AddCapabilities) Apply(e cldf.Environment, config AddCapabilitiesInput) sequences.AddCapabilities, sequences.AddCapabilitiesDeps{Env: &e, MCMSContracts: mcmsContracts}, sequences.AddCapabilitiesInput{ - RegistryRef: registryRef, - DonNames: u.donNames(config), - CapabilityConfigs: config.CapabilityConfigs, - Force: config.Force, - MCMSConfig: config.MCMSConfig, + RegistryRef: registryRef, + DonCapabilityConfigs: config.DonCapabilityConfigs, + Force: config.Force, + MCMSConfig: config.MCMSConfig, }, ) if err != nil { diff --git a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go index 14f5c419c3f..37f11fa52d1 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go +++ b/deployment/cre/capabilities_registry/v2/changeset/add_capabilities_test.go @@ -74,62 +74,46 @@ func TestAddCapabilities_VerifyPreconditions(t *testing.T) { env := test.SetupEnvV2(t, false) chainSelector := env.RegistrySelector - // Missing donName and donNames - err := cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ - RegistryChainSel: chainSelector, - RegistryQualifier: "qual", - DonNames: nil, - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}}}, - }) - require.Error(t, err) - assert.Contains(t, err.Error(), "must specify donName or donNames") + capCfg := []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}} - // Both donName and donNames set - err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ - RegistryChainSel: chainSelector, - RegistryQualifier: "qual", - DonName: "don-1", - DonNames: []string{"don-2"}, - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}}}, + // Empty map + err := cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ + RegistryChainSel: chainSelector, + RegistryQualifier: "qual", + DonCapabilityConfigs: nil, }) require.Error(t, err) - assert.Contains(t, err.Error(), "cannot specify both donName and donNames") + assert.Contains(t, err.Error(), "donCapabilityConfigs must contain at least one DON entry") - // donNames with empty string + // Empty DON name key err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ RegistryChainSel: chainSelector, RegistryQualifier: "qual", - DonNames: []string{"don-1", ""}, - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}}}, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + "": capCfg, + }, }) require.Error(t, err) - assert.Contains(t, err.Error(), "cannot contain an empty string") + assert.Contains(t, err.Error(), "cannot be empty strings") - // Missing capability configs + // Empty config list for a DON err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ RegistryChainSel: chainSelector, RegistryQualifier: "qual", - DonNames: []string{"don-1"}, - CapabilityConfigs: nil, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + "don-1": {}, + }, }) require.Error(t, err) - assert.Contains(t, err.Error(), "capabilityConfigs") + assert.Contains(t, err.Error(), "at least one capability config") - // Valid (single DON via donNames) + // Valid (single DON) err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ RegistryChainSel: chainSelector, RegistryQualifier: "qual", - DonNames: []string{"don-1"}, - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}}, - }) - require.NoError(t, err) - - // Valid (single DON via donName - backward compatibility) - err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ - RegistryChainSel: chainSelector, - RegistryQualifier: "qual", - DonName: "don-1", - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}}, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + "don-1": capCfg, + }, }) require.NoError(t, err) @@ -137,8 +121,10 @@ func TestAddCapabilities_VerifyPreconditions(t *testing.T) { err = cs.VerifyPreconditions(*env.Env, changeset.AddCapabilitiesInput{ RegistryChainSel: chainSelector, RegistryQualifier: "qual", - DonNames: []string{"don-1", "don-2"}, - CapabilityConfigs: []contracts.CapabilityConfig{{Capability: contracts.Capability{CapabilityID: "cap@1.0.0"}, Config: map[string]any{"k": "v"}}}, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + "don-1": capCfg, + "don-2": capCfg, + }, }) require.NoError(t, err) } @@ -147,15 +133,16 @@ func addNewCapability(t *testing.T, fixture *test.EnvWrapperV2, capID string) { input := changeset.AddCapabilitiesInput{ RegistryChainSel: fixture.RegistrySelector, RegistryQualifier: test.RegistryQualifier, - DonNames: []string{test.DONName}, - CapabilityConfigs: []contracts.CapabilityConfig{{ - Capability: contracts.Capability{ - CapabilityID: capID, - ConfigurationContract: common.Address{}, - Metadata: newCapMetadata, - }, - Config: newCapConfig, - }}, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + test.DONName: {{ + Capability: contracts.Capability{ + CapabilityID: capID, + ConfigurationContract: common.Address{}, + Metadata: newCapMetadata, + }, + Config: newCapConfig, + }}, + }, Force: true, } @@ -246,15 +233,16 @@ func TestAddCapabilities_Apply_MCMS(t *testing.T) { input := changeset.AddCapabilitiesInput{ RegistryChainSel: fixture.RegistrySelector, RegistryQualifier: test.RegistryQualifier, - DonNames: []string{test.DONName}, - CapabilityConfigs: []contracts.CapabilityConfig{{ - Capability: contracts.Capability{ - CapabilityID: newCapID, - ConfigurationContract: common.Address{}, - Metadata: newCapMetadata, - }, - Config: newCapConfig, - }}, + DonCapabilityConfigs: map[string][]contracts.CapabilityConfig{ + test.DONName: {{ + Capability: contracts.Capability{ + CapabilityID: newCapID, + ConfigurationContract: common.Address{}, + Metadata: newCapMetadata, + }, + Config: newCapConfig, + }}, + }, Force: true, MCMSConfig: &crecontracts.MCMSConfig{ MinDelay: 1 * time.Second, diff --git a/deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go b/deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go index 206b08a15ab..8239a6439ee 100644 --- a/deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go +++ b/deployment/cre/capabilities_registry/v2/changeset/sequences/add_capabilities.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "slices" "github.com/Masterminds/semver/v3" @@ -30,10 +31,8 @@ type AddCapabilitiesDeps struct { } type AddCapabilitiesInput struct { - CapabilityConfigs []contracts.CapabilityConfig // if Config subfield is nil, a default config is used - - // DonNames are the DONs to update. At least one is required. - DonNames []string + // DonCapabilityConfigs maps DON name to the list of capability configs for that DON. + DonCapabilityConfigs map[string][]contracts.CapabilityConfig // Force indicates whether to force the update even if we cannot validate that all forwarder contracts are ready to accept the new configure version. // This is very dangerous, and could break the whole platform if the forwarders are not ready. Be very careful with this option. @@ -44,14 +43,16 @@ type AddCapabilitiesInput struct { } func (i *AddCapabilitiesInput) Validate() error { - if len(i.DonNames) == 0 { - return errors.New("must specify at least one DON name") - } - if slices.Contains(i.DonNames, "") { - return errors.New("donNames cannot contain an empty string") + if len(i.DonCapabilityConfigs) == 0 { + return errors.New("donCapabilityConfigs must contain at least one DON entry") } - if len(i.CapabilityConfigs) == 0 { - return errors.New("capabilityConfigs is required") + for donName, configs := range i.DonCapabilityConfigs { + if donName == "" { + return errors.New("donCapabilityConfigs keys cannot be empty strings") + } + if len(configs) == 0 { + return fmt.Errorf("donCapabilityConfigs[%q] must contain at least one capability config", donName) + } } return nil } @@ -90,8 +91,8 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti return AddCapabilitiesOutput{}, fmt.Errorf("failed to create CapabilitiesRegistry: %w", err) } - // Build capabilities list once (registry-level; same for all DONs). - capabilities, err := buildCapabilitiesFromConfigs(input.CapabilityConfigs) + // Build capabilities list once (registry-level; union across all DONs). + capabilities, err := buildCapabilitiesFromAllDONConfigs(input.DonCapabilityConfigs) if err != nil { return AddCapabilitiesOutput{}, err } @@ -137,7 +138,7 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti var allUpdatedNodes []capabilities_registry_v2.CapabilitiesRegistryNodeParams // Update each DON: get nodes, update node configs, update DON. - for _, donName := range input.DonNames { + for donName, donCapConfigs := range input.DonCapabilityConfigs { don, nodes, err := GetDonNodes(donName, capReg) if err != nil { return AddCapabilitiesOutput{}, fmt.Errorf("failed to get DON %s nodes: %w", donName, err) @@ -148,7 +149,7 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti p2pIDs = append(p2pIDs, node.P2pId) } - nodeUpdates, err := buildNodeUpdatesForDON(p2pIDs, input.CapabilityConfigs) + nodeUpdates, err := buildNodeUpdatesForDON(p2pIDs, donCapConfigs) if err != nil { return AddCapabilitiesOutput{}, fmt.Errorf("failed to build node updates for DON %s: %w", donName, err) } @@ -182,7 +183,7 @@ var AddCapabilities = operations.NewSequence[AddCapabilitiesInput, AddCapabiliti contracts.UpdateDONInput{ ChainSelector: chainSel, P2PIDs: p2pIDs, - CapabilityConfigs: input.CapabilityConfigs, + CapabilityConfigs: donCapConfigs, MergeCapabilityConfigsWithOnChain: true, DonName: donName, F: don.F, @@ -233,17 +234,23 @@ func toOpsSlice(opPtrs ...*types.BatchOperation) []types.BatchOperation { return result } -// buildCapabilitiesFromConfigs builds the capability list for RegisterCapabilities (registry-level, no DON). -func buildCapabilitiesFromConfigs(configs []contracts.CapabilityConfig) ([]contracts.RegisterableCapability, error) { - out := make([]contracts.RegisterableCapability, len(configs)) - for i, cfg := range configs { - out[i] = contracts.RegisterableCapability{ - Metadata: cfg.Capability.Metadata, - CapabilityID: cfg.Capability.CapabilityID, - ConfigurationContract: cfg.Capability.ConfigurationContract, +// buildCapabilitiesFromAllDONConfigs collects the unique capabilities across all DONs' configs +// for registry-level registration. +func buildCapabilitiesFromAllDONConfigs(donConfigs map[string][]contracts.CapabilityConfig) ([]contracts.RegisterableCapability, error) { + uniqueCaps := make(map[string]contracts.RegisterableCapability) + for _, configs := range donConfigs { + for _, cfg := range configs { + if _, ok := uniqueCaps[cfg.Capability.CapabilityID]; ok { + continue + } + uniqueCaps[cfg.Capability.CapabilityID] = contracts.RegisterableCapability{ + Metadata: cfg.Capability.Metadata, + CapabilityID: cfg.Capability.CapabilityID, + ConfigurationContract: cfg.Capability.ConfigurationContract, + } } } - return out, nil + return slices.Collect(maps.Values(uniqueCaps)), nil } // buildNodeUpdatesForDON builds node config updates for a DON's nodes (adds the new capabilities to each node).