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 @@ -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"
Expand All @@ -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.
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,71 +74,57 @@ 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)

// Valid (multiple DONs)
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)
}
Expand All @@ -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,
}

Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"slices"

"github.com/Masterminds/semver/v3"
Expand All @@ -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.
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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).
Expand Down
Loading