diff --git a/deployment/keystone/capability_management.go b/deployment/keystone/capability_management.go index 4e15ea897ab..7e502d4f8ea 100644 --- a/deployment/keystone/capability_management.go +++ b/deployment/keystone/capability_management.go @@ -2,65 +2,54 @@ package keystone import ( "fmt" - "strings" + "math/big" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/deployment" kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" ) // AddCapabilities adds the capabilities to the registry -// it tries to add all capabilities in one go, if that fails, it falls back to adding them one by one -func AddCapabilities(lggr logger.Logger, registry *kcr.CapabilitiesRegistry, chain deployment.Chain, capabilities []kcr.CapabilitiesRegistryCapability) error { +func AddCapabilities(lggr logger.Logger, contractSet *ContractSet, chain deployment.Chain, capabilities []kcr.CapabilitiesRegistryCapability, useMCMS bool) (*timelock.BatchChainOperation, error) { if len(capabilities) == 0 { - return nil + return nil, nil } - // dedup capabilities - var deduped []kcr.CapabilitiesRegistryCapability - seen := make(map[string]struct{}) - for _, cap := range capabilities { - if _, ok := seen[CapabilityID(cap)]; !ok { - seen[CapabilityID(cap)] = struct{}{} - deduped = append(deduped, cap) - } + registry := contractSet.CapabilitiesRegistry + deduped, err := dedupCapabilities(registry, capabilities) + if err != nil { + return nil, fmt.Errorf("failed to dedup capabilities: %w", err) } - - tx, err := registry.AddCapabilities(chain.DeployerKey, deduped) + txOpts := chain.DeployerKey + if useMCMS { + txOpts = deployment.SimTransactOpts() + } + tx, err := registry.AddCapabilities(txOpts, deduped) if err != nil { err = DecodeErr(kcr.CapabilitiesRegistryABI, err) - // no typed errors in the abi, so we have to do string matching - // try to add all capabilities in one go, if that fails, fall back to 1-by-1 - if !strings.Contains(err.Error(), "CapabilityAlreadyExists") { - return fmt.Errorf("failed to call AddCapabilities: %w", err) - } - lggr.Warnw("capabilities already exist, falling back to 1-by-1", "capabilities", deduped) - for _, cap := range deduped { - tx, err = registry.AddCapabilities(chain.DeployerKey, []kcr.CapabilitiesRegistryCapability{cap}) - if err != nil { - err = DecodeErr(kcr.CapabilitiesRegistryABI, err) - if strings.Contains(err.Error(), "CapabilityAlreadyExists") { - lggr.Warnw("capability already exists, skipping", "capability", cap) - continue - } - return fmt.Errorf("failed to call AddCapabilities for capability %v: %w", cap, err) - } - // 1-by-1 tx is pending and we need to wait for it to be mined - _, err = chain.Confirm(tx) - if err != nil { - return fmt.Errorf("failed to confirm AddCapabilities confirm transaction %s: %w", tx.Hash().String(), err) - } - lggr.Debugw("registered capability", "capability", cap) - - } - } else { - // the bulk add tx is pending and we need to wait for it to be mined + return nil, fmt.Errorf("failed to add capabilities: %w", err) + } + var batch *timelock.BatchChainOperation + if !useMCMS { _, err = chain.Confirm(tx) if err != nil { - return fmt.Errorf("failed to confirm AddCapabilities confirm transaction %s: %w", tx.Hash().String(), err) + return nil, fmt.Errorf("failed to confirm AddCapabilities confirm transaction %s: %w", tx.Hash().String(), err) } lggr.Info("registered capabilities", "capabilities", deduped) + } else { + batch = &timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(chain.Selector), + Batch: []mcms.Operation{ + { + To: registry.Address(), + Data: tx.Data(), + Value: big.NewInt(0), + }, + }, + } } - return nil + return batch, nil } // CapabilityID returns a unique id for the capability @@ -68,3 +57,36 @@ func AddCapabilities(lggr logger.Logger, registry *kcr.CapabilitiesRegistry, cha func CapabilityID(c kcr.CapabilitiesRegistryCapability) string { return fmt.Sprintf("%s@%s", c.LabelledName, c.Version) } + +// dedupCapabilities deduplicates the capabilities +// dedup capabilities with respect to the registry +// contract reverts on adding the same capability twice and that would cause the whole transaction to revert +// which is very bad for us for mcms +func dedupCapabilities(registry *kcr.CapabilitiesRegistry, capabilities []kcr.CapabilitiesRegistryCapability) ([]kcr.CapabilitiesRegistryCapability, error) { + var out []kcr.CapabilitiesRegistryCapability + existing, err := registry.GetCapabilities(nil) + if err != nil { + return nil, fmt.Errorf("failed to call GetCapabilities: %w", err) + } + existingByID := make(map[[32]byte]struct{}) + for _, cap := range existing { + existingByID[cap.HashedId] = struct{}{} + } + seen := make(map[string]struct{}) + for _, candidate := range capabilities { + h, err := registry.GetHashedCapabilityId(nil, candidate.LabelledName, candidate.Version) + if err != nil { + return nil, fmt.Errorf("failed to call GetHashedCapabilityId: %w", err) + } + // dedup input capabilities + if _, exists := seen[CapabilityID(candidate)]; exists { + continue + } + seen[CapabilityID(candidate)] = struct{}{} + // dedup with respect to the registry + if _, exists := existingByID[h]; !exists { + out = append(out, candidate) + } + } + return out, nil +} diff --git a/deployment/keystone/changeset/accept_ownership_test.go b/deployment/keystone/changeset/accept_ownership_test.go index f205adda496..70f14c7dc5e 100644 --- a/deployment/keystone/changeset/accept_ownership_test.go +++ b/deployment/keystone/changeset/accept_ownership_test.go @@ -38,6 +38,10 @@ func TestAcceptAllOwnership(t *testing.T) { Changeset: commonchangeset.WrapChangeSet(changeset.DeployForwarder), Config: registrySel, }, + { + Changeset: commonchangeset.WrapChangeSet(changeset.DeployFeedsConsumer), + Config: &changeset.DeployFeedsConsumerRequest{ChainSelector: registrySel}, + }, { Changeset: commonchangeset.WrapChangeSet(commonchangeset.DeployMCMSWithTimelock), Config: map[uint64]types.MCMSWithTimelockConfig{ diff --git a/deployment/keystone/changeset/append_node_capbilities.go b/deployment/keystone/changeset/append_node_capbilities.go index 974c4970c51..7601b1bb206 100644 --- a/deployment/keystone/changeset/append_node_capbilities.go +++ b/deployment/keystone/changeset/append_node_capbilities.go @@ -3,7 +3,11 @@ package changeset import ( "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" kslib "github.com/smartcontractkit/chainlink/deployment/keystone" "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal" ) @@ -16,15 +20,39 @@ type AppendNodeCapabilitiesRequest = MutateNodeCapabilitiesRequest // AppendNodeCapabilities adds any new capabilities to the registry, merges the new capabilities with the existing capabilities // of the node, and updates the nodes in the registry host the union of the new and existing capabilities. func AppendNodeCapabilities(env deployment.Environment, req *AppendNodeCapabilitiesRequest) (deployment.ChangesetOutput, error) { - cfg, err := req.convert(env) + c, err := req.convert(env) if err != nil { return deployment.ChangesetOutput{}, err } - _, err = internal.AppendNodeCapabilitiesImpl(env.Logger, cfg) + r, err := internal.AppendNodeCapabilitiesImpl(env.Logger, c) if err != nil { return deployment.ChangesetOutput{}, err } - return deployment.ChangesetOutput{}, nil + out := deployment.ChangesetOutput{} + if req.UseMCMS { + if r.Ops == nil { + return out, fmt.Errorf("expected MCMS operation to be non-nil") + } + timelocksPerChain := map[uint64]common.Address{ + c.Chain.Selector: c.ContractSet.Timelock.Address(), + } + proposerMCMSes := map[uint64]*gethwrappers.ManyChainMultiSig{ + c.Chain.Selector: c.ContractSet.ProposerMcm, + } + + proposal, err := proposalutils.BuildProposalFromBatches( + timelocksPerChain, + proposerMCMSes, + []timelock.BatchChainOperation{*r.Ops}, + "proposal to set update node capabilities", + 0, + ) + if err != nil { + return out, fmt.Errorf("failed to build proposal: %w", err) + } + out.Proposals = []timelock.MCMSWithTimelockProposal{*proposal} + } + return out, nil } func (req *AppendNodeCapabilitiesRequest) convert(e deployment.Environment) (*internal.AppendNodeCapabilitiesRequest, error) { @@ -35,21 +63,20 @@ func (req *AppendNodeCapabilitiesRequest) convert(e deployment.Environment) (*in if !ok { return nil, fmt.Errorf("registry chain selector %d does not exist in environment", req.RegistryChainSel) } - contracts, err := kslib.GetContractSets(e.Logger, &kslib.GetContractSetsRequest{ + resp, err := kslib.GetContractSets(e.Logger, &kslib.GetContractSetsRequest{ Chains: map[uint64]deployment.Chain{req.RegistryChainSel: registryChain}, - AddressBook: req.AddressBook, + AddressBook: e.ExistingAddresses, }) if err != nil { return nil, fmt.Errorf("failed to get contract sets: %w", err) } - registry := contracts.ContractSets[req.RegistryChainSel].CapabilitiesRegistry - if registry == nil { - return nil, fmt.Errorf("capabilities registry not found for chain %d", req.RegistryChainSel) - } + contracts := resp.ContractSets[req.RegistryChainSel] return &internal.AppendNodeCapabilitiesRequest{ Chain: registryChain, - Registry: registry, + Registry: contracts.CapabilitiesRegistry, + ContractSet: &contracts, P2pToCapabilities: req.P2pToCapabilities, + UseMCMS: req.UseMCMS, }, nil } diff --git a/deployment/keystone/changeset/append_node_capbilities_test.go b/deployment/keystone/changeset/append_node_capbilities_test.go new file mode 100644 index 00000000000..592f2d6d00d --- /dev/null +++ b/deployment/keystone/changeset/append_node_capbilities_test.go @@ -0,0 +1,129 @@ +package changeset_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + + "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" +) + +func TestAppendNodeCapabilities(t *testing.T) { + t.Parallel() + + var ( + capA = kcr.CapabilitiesRegistryCapability{ + LabelledName: "capA", + Version: "0.4.2", + } + capB = kcr.CapabilitiesRegistryCapability{ + LabelledName: "capB", + Version: "3.16.0", + } + caps = []kcr.CapabilitiesRegistryCapability{capA, capB} + ) + t.Run("no mcms", func(t *testing.T) { + te := SetupTestEnv(t, TestConfig{ + WFDonConfig: DonConfig{N: 4}, + AssetDonConfig: DonConfig{N: 4}, + WriterDonConfig: DonConfig{N: 4}, + NumChains: 1, + }) + + newCapabilities := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + for id, _ := range te.WFNodes { + k, err := p2pkey.MakePeerID(id) + require.NoError(t, err) + newCapabilities[k] = caps + } + + t.Run("succeeds if existing capabilities not explicit", func(t *testing.T) { + cfg := changeset.AppendNodeCapabilitiesRequest{ + RegistryChainSel: te.RegistrySelector, + P2pToCapabilities: newCapabilities, + } + + csOut, err := changeset.AppendNodeCapabilities(te.Env, &cfg) + require.NoError(t, err) + require.Len(t, csOut.Proposals, 0) + require.Nil(t, csOut.AddressBook) + + validateCapabilityAppends(t, te, newCapabilities) + }) + }) + t.Run("with mcms", func(t *testing.T) { + te := SetupTestEnv(t, TestConfig{ + WFDonConfig: DonConfig{N: 4}, + AssetDonConfig: DonConfig{N: 4}, + WriterDonConfig: DonConfig{N: 4}, + NumChains: 1, + UseMCMS: true, + }) + + newCapabilities := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + for id, _ := range te.WFNodes { + k, err := p2pkey.MakePeerID(id) + require.NoError(t, err) + newCapabilities[k] = caps + } + + cfg := changeset.AppendNodeCapabilitiesRequest{ + RegistryChainSel: te.RegistrySelector, + P2pToCapabilities: newCapabilities, + UseMCMS: true, + } + + csOut, err := changeset.AppendNodeCapabilities(te.Env, &cfg) + require.NoError(t, err) + require.Len(t, csOut.Proposals, 1) + require.Len(t, csOut.Proposals[0].Transactions, 1) + require.Len(t, csOut.Proposals[0].Transactions[0].Batch, 2) // add capabilities, update nodes + require.Nil(t, csOut.AddressBook) + + // now apply the changeset such that the proposal is signed and execed + contracts := te.ContractSets()[te.RegistrySelector] + timelocks := map[uint64]*gethwrappers.RBACTimelock{ + te.RegistrySelector: contracts.Timelock, + } + _, err = commonchangeset.ApplyChangesets(t, te.Env, timelocks, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.AppendNodeCapabilities), + Config: &cfg, + }, + }) + require.NoError(t, err) + validateCapabilityAppends(t, te, newCapabilities) + }) + +} + +// validateUpdate checks reads nodes from the registry and checks they have the expected updates +func validateCapabilityAppends(t *testing.T, te TestEnv, appended map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) { + registry := te.ContractSets()[te.RegistrySelector].CapabilitiesRegistry + wfP2PIDs := p2pIDs(t, maps.Keys(te.WFNodes)) + nodes, err := registry.GetNodesByP2PIds(nil, wfP2PIDs) + require.NoError(t, err) + require.Len(t, nodes, len(wfP2PIDs)) + for _, node := range nodes { + want := appended[node.P2pId] + require.NotNil(t, want) + assertContainsCapabilities(t, registry, want, node) + } +} + +func assertContainsCapabilities(t *testing.T, registry *kcr.CapabilitiesRegistry, want []kcr.CapabilitiesRegistryCapability, got kcr.INodeInfoProviderNodeInfo) { + wantHashes := make([][32]byte, len(want)) + for i, c := range want { + h, err := registry.GetHashedCapabilityId(nil, c.LabelledName, c.Version) + require.NoError(t, err) + wantHashes[i] = h + assert.Contains(t, got.HashedCapabilityIds, h, "missing capability %v", c) + } + assert.LessOrEqual(t, len(want), len(got.HashedCapabilityIds)) +} diff --git a/deployment/keystone/changeset/deploy_ocr3_test.go b/deployment/keystone/changeset/deploy_ocr3_test.go index 0d49af68823..868fa5d4ebe 100644 --- a/deployment/keystone/changeset/deploy_ocr3_test.go +++ b/deployment/keystone/changeset/deploy_ocr3_test.go @@ -46,7 +46,6 @@ func TestDeployOCR3(t *testing.T) { func TestConfigureOCR3(t *testing.T) { t.Parallel() - lggr := logger.Test(t) c := kslib.OracleConfigWithSecrets{ OracleConfig: kslib.OracleConfig{ @@ -122,13 +121,9 @@ func TestConfigureOCR3(t *testing.T) { assert.NotNil(t, csOut.Proposals) t.Logf("got: %v", csOut.Proposals[0]) - contractSetsResp, err := kslib.GetContractSets(lggr, &kslib.GetContractSetsRequest{ - Chains: te.Env.Chains, - AddressBook: te.Env.ExistingAddresses, - }) - require.NoError(t, err) + contracts := te.ContractSets()[te.RegistrySelector] var timelocks = map[uint64]*gethwrappers.RBACTimelock{ - te.RegistrySelector: contractSetsResp.ContractSets[te.RegistrySelector].Timelock, + te.RegistrySelector: contracts.Timelock, } // now apply the changeset such that the proposal is signed and execed w2 := &bytes.Buffer{} diff --git a/deployment/keystone/changeset/helpers_test.go b/deployment/keystone/changeset/helpers_test.go index d4435d8f7a6..4d39c905f0d 100644 --- a/deployment/keystone/changeset/helpers_test.go +++ b/deployment/keystone/changeset/helpers_test.go @@ -115,6 +115,7 @@ func (te TestEnv) ContractSets() map[uint64]kslib.ContractSet { } // SetupTestEnv sets up a keystone test environment with the given configuration +// TODO: make more configurable; eg many tests don't need all the nodes (like when testing a registry change) func SetupTestEnv(t *testing.T, c TestConfig) TestEnv { require.NoError(t, c.Validate()) lggr := logger.Test(t) @@ -223,15 +224,13 @@ func SetupTestEnv(t *testing.T, c TestConfig) TestEnv { } var allDons = []keystone.DonCapabilities{wfDon, cwDon, assetDon} - _, err = kschangeset.ConfigureInitialContractsChangeset(env, kschangeset.InitialContractsCfg{ + csOut, err := kschangeset.ConfigureInitialContractsChangeset(env, kschangeset.InitialContractsCfg{ RegistryChainSel: registryChainSel, Dons: allDons, OCR3Config: &ocr3Config, }) require.NoError(t, err) - // TODO: KS-rm_deploy_opt - //require.Nil(t, csOut.AddressBook, "no new addresses should be created in configure initial contracts") - //require.NoError(t, env.ExistingAddresses.Merge(csOut.AddressBook)) + require.Nil(t, csOut.AddressBook, "no new addresses should be created in configure initial contracts") req := &keystone.GetContractSetsRequest{ Chains: env.Chains, @@ -259,8 +258,7 @@ func SetupTestEnv(t *testing.T, c TestConfig) TestEnv { validateDon(t, gotRegistry, assetNodes, assetDon) if c.UseMCMS { - // TODO: mcms on all the chains, currently only on the registry chain. need to fix this for forwarders - // deploy, configure and xfer ownership of MCMS + // deploy, configure and xfer ownership of MCMS on all chains timelockCfgs := make(map[uint64]commontypes.MCMSWithTimelockConfig) for sel := range env.Chains { t.Logf("Enabling MCMS on chain %d", sel) diff --git a/deployment/keystone/changeset/internal/append_node_capabilities.go b/deployment/keystone/changeset/internal/append_node_capabilities.go index cb28c03c6f5..ced741d416a 100644 --- a/deployment/keystone/changeset/internal/append_node_capabilities.go +++ b/deployment/keystone/changeset/internal/append_node_capabilities.go @@ -14,7 +14,9 @@ type AppendNodeCapabilitiesRequest struct { Chain deployment.Chain Registry *kcr.CapabilitiesRegistry + ContractSet *kslib.ContractSet P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability + UseMCMS bool } func (req *AppendNodeCapabilitiesRequest) Validate() error { @@ -31,15 +33,6 @@ func AppendNodeCapabilitiesImpl(lggr logger.Logger, req *AppendNodeCapabilitiesR if err := req.Validate(); err != nil { return nil, fmt.Errorf("failed to validate request: %w", err) } - // collect all the capabilities and add them to the registry - var capabilities []kcr.CapabilitiesRegistryCapability - for _, cap := range req.P2pToCapabilities { - capabilities = append(capabilities, cap...) - } - err := kslib.AddCapabilities(lggr, req.Registry, req.Chain, capabilities) - if err != nil { - return nil, fmt.Errorf("failed to add capabilities: %w", err) - } // for each node, merge the new capabilities with the existing ones and update the node updatesByPeer := make(map[p2pkey.PeerID]NodeUpdate) @@ -51,10 +44,23 @@ func AppendNodeCapabilitiesImpl(lggr logger.Logger, req *AppendNodeCapabilitiesR updatesByPeer[p2pID] = NodeUpdate{Capabilities: caps[p2pID]} } + // collect all the capabilities and add them to the registry + var capabilities []kcr.CapabilitiesRegistryCapability + for _, cap := range req.P2pToCapabilities { + capabilities = append(capabilities, cap...) + } + op, err := kslib.AddCapabilities(lggr, req.ContractSet, req.Chain, capabilities, req.UseMCMS) + if err != nil { + return nil, fmt.Errorf("failed to add capabilities: %w", err) + } + updateNodesReq := &UpdateNodesRequest{ Chain: req.Chain, Registry: req.Registry, + ContractSet: req.ContractSet, P2pToUpdates: updatesByPeer, + UseMCMS: req.UseMCMS, + Ops: op, } resp, err := UpdateNodes(lggr, updateNodesReq) if err != nil { diff --git a/deployment/keystone/changeset/internal/test/utils.go b/deployment/keystone/changeset/internal/test/utils.go index 6fe4a8f4a2e..a7aed2c9cb1 100644 --- a/deployment/keystone/changeset/internal/test/utils.go +++ b/deployment/keystone/changeset/internal/test/utils.go @@ -33,6 +33,7 @@ type SetupTestRegistryRequest struct { P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability NopToNodes map[kcr.CapabilitiesRegistryNodeOperator][]*internal.P2PSignerEnc Dons []Don + // TODO maybe add support for MCMS at this level } type SetupTestRegistryResponse struct { diff --git a/deployment/keystone/changeset/internal/update_don.go b/deployment/keystone/changeset/internal/update_don.go index 4883368dc4d..d56f77c1c78 100644 --- a/deployment/keystone/changeset/internal/update_don.go +++ b/deployment/keystone/changeset/internal/update_don.go @@ -9,6 +9,7 @@ import ( "sort" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" @@ -31,6 +32,8 @@ type UpdateDonRequest struct { P2PIDs []p2pkey.PeerID // this is the unique identifier for the don CapabilityConfigs []CapabilityConfig // if Config subfield is nil, a default config is used + + UseMCMS bool } func (r *UpdateDonRequest) appendNodeCapabilitiesRequest() *AppendNodeCapabilitiesRequest { @@ -38,6 +41,7 @@ func (r *UpdateDonRequest) appendNodeCapabilitiesRequest() *AppendNodeCapabiliti Chain: r.Chain, Registry: r.Registry, P2pToCapabilities: make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability), + UseMCMS: r.UseMCMS, } for _, p2pid := range r.P2PIDs { if _, exists := out.P2pToCapabilities[p2pid]; !exists { @@ -61,7 +65,8 @@ func (r *UpdateDonRequest) Validate() error { } type UpdateDonResponse struct { - DonInfo kcr.CapabilitiesRegistryDONInfo + DonInfo kcr.CapabilitiesRegistryDONInfo + Proposals []timelock.MCMSWithTimelockProposal } func UpdateDon(lggr logger.Logger, req *UpdateDonRequest) (*UpdateDonResponse, error) { diff --git a/deployment/keystone/changeset/internal/update_node_capabilities.go b/deployment/keystone/changeset/internal/update_node_capabilities.go index 0420c46f27d..d060f47d810 100644 --- a/deployment/keystone/changeset/internal/update_node_capabilities.go +++ b/deployment/keystone/changeset/internal/update_node_capabilities.go @@ -11,10 +11,12 @@ import ( ) type UpdateNodeCapabilitiesImplRequest struct { - Chain deployment.Chain - Registry *kcr.CapabilitiesRegistry - + Chain deployment.Chain + Registry *kcr.CapabilitiesRegistry + ContractSet *kslib.ContractSet P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability + + UseMCMS bool } func (req *UpdateNodeCapabilitiesImplRequest) Validate() error { @@ -37,7 +39,7 @@ func UpdateNodeCapabilitiesImpl(lggr logger.Logger, req *UpdateNodeCapabilitiesI for _, cap := range req.P2pToCapabilities { capabilities = append(capabilities, cap...) } - err := kslib.AddCapabilities(lggr, req.Registry, req.Chain, capabilities) + op, err := kslib.AddCapabilities(lggr, req.ContractSet, req.Chain, capabilities, req.UseMCMS) if err != nil { return nil, fmt.Errorf("failed to add capabilities: %w", err) } @@ -51,6 +53,9 @@ func UpdateNodeCapabilitiesImpl(lggr logger.Logger, req *UpdateNodeCapabilitiesI Chain: req.Chain, Registry: req.Registry, P2pToUpdates: p2pToUpdates, + ContractSet: req.ContractSet, + Ops: op, + UseMCMS: req.UseMCMS, } resp, err := UpdateNodes(lggr, updateNodesReq) if err != nil { diff --git a/deployment/keystone/changeset/internal/update_nodes.go b/deployment/keystone/changeset/internal/update_nodes.go index b8a08c37e50..51382c3745d 100644 --- a/deployment/keystone/changeset/internal/update_nodes.go +++ b/deployment/keystone/changeset/internal/update_nodes.go @@ -5,10 +5,13 @@ import ( "encoding/hex" "errors" "fmt" + "math/big" "sort" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" "github.com/smartcontractkit/chainlink-common/pkg/logger" kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" @@ -30,6 +33,10 @@ type UpdateNodesRequest struct { Registry *kcr.CapabilitiesRegistry P2pToUpdates map[p2pkey.PeerID]NodeUpdate + + ContractSet *kslib.ContractSet // contract set for the given chain + Ops *timelock.BatchChainOperation + UseMCMS bool } func (req *UpdateNodesRequest) NodeParams() ([]kcr.CapabilitiesRegistryNodeParams, error) { @@ -80,10 +87,13 @@ func (req *UpdateNodesRequest) Validate() error { type UpdateNodesResponse struct { NodeParams []kcr.CapabilitiesRegistryNodeParams + //Proposals []timelock.MCMSWithTimelockProposal + Ops *timelock.BatchChainOperation } // UpdateNodes updates the nodes in the registry -// the update sets the signer and capabilities for each node. it does not append capabilities to the existing ones +// the update sets the signer and capabilities for each node. +// The nodes and capabilities must already exist in the registry. func UpdateNodes(lggr logger.Logger, req *UpdateNodesRequest) (*UpdateNodesResponse, error) { if err := req.Validate(); err != nil { return nil, fmt.Errorf("failed to validate request: %w", err) @@ -94,17 +104,42 @@ func UpdateNodes(lggr logger.Logger, req *UpdateNodesRequest) (*UpdateNodesRespo err = kslib.DecodeErr(kcr.CapabilitiesRegistryABI, err) return nil, fmt.Errorf("failed to make node params: %w", err) } - tx, err := req.Registry.UpdateNodes(req.Chain.DeployerKey, params) + txOpts := req.Chain.DeployerKey + if req.UseMCMS { + txOpts = deployment.SimTransactOpts() + } + tx, err := req.Registry.UpdateNodes(txOpts, params) if err != nil { err = kslib.DecodeErr(kcr.CapabilitiesRegistryABI, err) return nil, fmt.Errorf("failed to call UpdateNodes: %w", err) } - _, err = req.Chain.Confirm(tx) - if err != nil { - return nil, fmt.Errorf("failed to confirm UpdateNodes confirm transaction %s: %w", tx.Hash().String(), err) + ops := req.Ops + if !req.UseMCMS { + _, err = req.Chain.Confirm(tx) + if err != nil { + return nil, fmt.Errorf("failed to confirm UpdateNodes confirm transaction %s: %w", tx.Hash().String(), err) + } + } else { + op := mcms.Operation{ + To: req.Registry.Address(), + Data: tx.Data(), + Value: big.NewInt(0), + } + + if ops == nil { + ops = &timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(req.Chain.Selector), + Batch: []mcms.Operation{ + op, + }, + } + } else { + ops.Batch = append(ops.Batch, op) + } } - return &UpdateNodesResponse{NodeParams: params}, nil + + return &UpdateNodesResponse{NodeParams: params, Ops: ops}, nil } // AppendCapabilities appends the capabilities to the existing capabilities of the nodes listed in p2pIds in the registry diff --git a/deployment/keystone/changeset/update_node_capabilities.go b/deployment/keystone/changeset/update_node_capabilities.go index 1d6dde6af5a..67bd8ac39cd 100644 --- a/deployment/keystone/changeset/update_node_capabilities.go +++ b/deployment/keystone/changeset/update_node_capabilities.go @@ -1,13 +1,16 @@ package changeset import ( - "encoding/json" "fmt" "strconv" + "github.com/ethereum/go-ethereum/common" chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" kslib "github.com/smartcontractkit/chainlink/deployment/keystone" "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal" @@ -50,16 +53,13 @@ type UpdateNodeCapabilitiesRequest = MutateNodeCapabilitiesRequest // MutateNodeCapabilitiesRequest is a request to change the capabilities of nodes in the registry type MutateNodeCapabilitiesRequest struct { - AddressBook deployment.AddressBook RegistryChainSel uint64 P2pToCapabilities map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability + UseMCMS bool } func (req *MutateNodeCapabilitiesRequest) Validate() error { - if req.AddressBook == nil { - return fmt.Errorf("address book is nil") - } if len(req.P2pToCapabilities) == 0 { return fmt.Errorf("p2pToCapabilities is empty") } @@ -79,38 +79,62 @@ func (req *MutateNodeCapabilitiesRequest) updateNodeCapabilitiesImplRequest(e de if !ok { return nil, fmt.Errorf("registry chain selector %d does not exist in environment", req.RegistryChainSel) } - contracts, err := kslib.GetContractSets(e.Logger, &kslib.GetContractSetsRequest{ + resp, err := kslib.GetContractSets(e.Logger, &kslib.GetContractSetsRequest{ Chains: map[uint64]deployment.Chain{req.RegistryChainSel: registryChain}, - AddressBook: req.AddressBook, + AddressBook: e.ExistingAddresses, }) if err != nil { return nil, fmt.Errorf("failed to get contract sets: %w", err) } - registry := contracts.ContractSets[req.RegistryChainSel].CapabilitiesRegistry - if registry == nil { - return nil, fmt.Errorf("capabilities registry not found for chain %d", req.RegistryChainSel) + contractSet, exists := resp.ContractSets[req.RegistryChainSel] + if !exists { + return nil, fmt.Errorf("contract set not found for chain %d", req.RegistryChainSel) } return &internal.UpdateNodeCapabilitiesImplRequest{ Chain: registryChain, - Registry: registry, + Registry: contractSet.CapabilitiesRegistry, P2pToCapabilities: req.P2pToCapabilities, + ContractSet: &contractSet, + UseMCMS: req.UseMCMS, }, nil } // UpdateNodeCapabilities updates the capabilities of nodes in the registry -func UpdateNodeCapabilities(env deployment.Environment, req *MutateNodeCapabilitiesRequest) (deployment.ChangesetOutput, error) { +func UpdateNodeCapabilities(env deployment.Environment, req *UpdateNodeCapabilitiesRequest) (deployment.ChangesetOutput, error) { c, err := req.updateNodeCapabilitiesImplRequest(env) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to convert request: %w", err) } r, err := internal.UpdateNodeCapabilitiesImpl(env.Logger, c) - if err == nil { - b, err2 := json.Marshal(r) - if err2 != nil { - env.Logger.Debugf("Updated node capabilities '%s'", b) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to update nodes: %w", err) + } + + out := deployment.ChangesetOutput{} + if req.UseMCMS { + if r.Ops == nil { + return out, fmt.Errorf("expected MCMS operation to be non-nil") + } + timelocksPerChain := map[uint64]common.Address{ + c.Chain.Selector: c.ContractSet.Timelock.Address(), + } + proposerMCMSes := map[uint64]*gethwrappers.ManyChainMultiSig{ + c.Chain.Selector: c.ContractSet.ProposerMcm, + } + + proposal, err := proposalutils.BuildProposalFromBatches( + timelocksPerChain, + proposerMCMSes, + []timelock.BatchChainOperation{*r.Ops}, + "proposal to set update node capabilities", + 0, + ) + if err != nil { + return out, fmt.Errorf("failed to build proposal: %w", err) } + out.Proposals = []timelock.MCMSWithTimelockProposal{*proposal} } - return deployment.ChangesetOutput{}, err + return out, nil } diff --git a/deployment/keystone/changeset/update_node_capabilities_test.go b/deployment/keystone/changeset/update_node_capabilities_test.go new file mode 100644 index 00000000000..41333e183fb --- /dev/null +++ b/deployment/keystone/changeset/update_node_capabilities_test.go @@ -0,0 +1,203 @@ +package changeset_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + + "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" + kcr "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/capabilities_registry" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" +) + +func TestUpdateNodeCapabilities(t *testing.T) { + t.Parallel() + + var ( + capA = kcr.CapabilitiesRegistryCapability{ + LabelledName: "capA", + Version: "0.4.2", + } + capB = kcr.CapabilitiesRegistryCapability{ + LabelledName: "capB", + Version: "3.16.0", + } + caps = []kcr.CapabilitiesRegistryCapability{capA, capB} + ) + t.Run("no mcms", func(t *testing.T) { + te := SetupTestEnv(t, TestConfig{ + WFDonConfig: DonConfig{N: 4}, + AssetDonConfig: DonConfig{N: 4}, + WriterDonConfig: DonConfig{N: 4}, + NumChains: 1, + }) + + // contract set is already deployed with capabilities + // we have to keep track of the existing capabilities to add to the new ones + var p2pIDs []p2pkey.PeerID + newCapabilities := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + for id, _ := range te.WFNodes { + k, err := p2pkey.MakePeerID(id) + require.NoError(t, err) + p2pIDs = append(p2pIDs, k) + newCapabilities[k] = caps + } + + t.Run("fails if update drops existing capabilities", func(t *testing.T) { + + cfg := changeset.UpdateNodeCapabilitiesRequest{ + RegistryChainSel: te.RegistrySelector, + P2pToCapabilities: newCapabilities, + } + + _, err := changeset.UpdateNodeCapabilities(te.Env, &cfg) + require.Error(t, err) + assert.Contains(t, err.Error(), "CapabilityRequiredByDON") + }) + t.Run("succeeds if update sets new and existing capabilities", func(t *testing.T) { + existing := getNodeCapabilities(te.ContractSets()[te.RegistrySelector].CapabilitiesRegistry, p2pIDs) + + capabiltiesToSet := existing + for k, v := range newCapabilities { + capabiltiesToSet[k] = append(capabiltiesToSet[k], v...) + } + cfg := changeset.UpdateNodeCapabilitiesRequest{ + RegistryChainSel: te.RegistrySelector, + P2pToCapabilities: capabiltiesToSet, + } + + csOut, err := changeset.UpdateNodeCapabilities(te.Env, &cfg) + require.NoError(t, err) + require.Len(t, csOut.Proposals, 0) + require.Nil(t, csOut.AddressBook) + + validateCapabilityUpdates(t, te, capabiltiesToSet) + }) + }) + t.Run("with mcms", func(t *testing.T) { + te := SetupTestEnv(t, TestConfig{ + WFDonConfig: DonConfig{N: 4}, + AssetDonConfig: DonConfig{N: 4}, + WriterDonConfig: DonConfig{N: 4}, + NumChains: 1, + UseMCMS: true, + }) + + // contract set is already deployed with capabilities + // we have to keep track of the existing capabilities to add to the new ones + var p2pIDs []p2pkey.PeerID + newCapabilities := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + for id, _ := range te.WFNodes { + k, err := p2pkey.MakePeerID(id) + require.NoError(t, err) + p2pIDs = append(p2pIDs, k) + newCapabilities[k] = caps + } + + existing := getNodeCapabilities(te.ContractSets()[te.RegistrySelector].CapabilitiesRegistry, p2pIDs) + + capabiltiesToSet := existing + for k, v := range newCapabilities { + capabiltiesToSet[k] = append(capabiltiesToSet[k], v...) + } + cfg := changeset.UpdateNodeCapabilitiesRequest{ + RegistryChainSel: te.RegistrySelector, + P2pToCapabilities: capabiltiesToSet, + UseMCMS: true, + } + + csOut, err := changeset.UpdateNodeCapabilities(te.Env, &cfg) + require.NoError(t, err) + require.Len(t, csOut.Proposals, 1) + require.Len(t, csOut.Proposals[0].Transactions, 1) + require.Len(t, csOut.Proposals[0].Transactions[0].Batch, 2) // add capabilities, update nodes + require.Nil(t, csOut.AddressBook) + + // now apply the changeset such that the proposal is signed and execed + contracts := te.ContractSets()[te.RegistrySelector] + timelocks := map[uint64]*gethwrappers.RBACTimelock{ + te.RegistrySelector: contracts.Timelock, + } + _, err = commonchangeset.ApplyChangesets(t, te.Env, timelocks, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.UpdateNodeCapabilities), + Config: &cfg, + }, + }) + require.NoError(t, err) + validateCapabilityUpdates(t, te, capabiltiesToSet) + + }) + +} + +// validateUpdate checks reads nodes from the registry and checks they have the expected updates +func validateCapabilityUpdates(t *testing.T, te TestEnv, expected map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) { + registry := te.ContractSets()[te.RegistrySelector].CapabilitiesRegistry + wfP2PIDs := p2pIDs(t, maps.Keys(te.WFNodes)) + nodes, err := registry.GetNodesByP2PIds(nil, wfP2PIDs) + require.NoError(t, err) + require.Len(t, nodes, len(wfP2PIDs)) + for _, node := range nodes { + want := expected[node.P2pId] + require.NotNil(t, want) + assertEqualCapabilities(t, registry, want, node) + } +} + +func assertEqualCapabilities(t *testing.T, registry *kcr.CapabilitiesRegistry, want []kcr.CapabilitiesRegistryCapability, got kcr.INodeInfoProviderNodeInfo) { + wantHashes := make([][32]byte, len(want)) + for i, c := range want { + h, err := registry.GetHashedCapabilityId(nil, c.LabelledName, c.Version) + require.NoError(t, err) + wantHashes[i] = h + } + assert.Equal(t, len(want), len(got.HashedCapabilityIds)) + assert.ElementsMatch(t, wantHashes, got.HashedCapabilityIds) +} + +func getNodeCapabilities(registry *kcr.CapabilitiesRegistry, p2pIDs []p2pkey.PeerID) map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability { + m := make(map[p2pkey.PeerID][]kcr.CapabilitiesRegistryCapability) + caps, err := registry.GetCapabilities(nil) + if err != nil { + panic(err) + } + var capMap = make(map[[32]byte]kcr.CapabilitiesRegistryCapability) + for _, c := range caps { + capMap[c.HashedId] = kcr.CapabilitiesRegistryCapability{ + LabelledName: c.LabelledName, + Version: c.Version, + CapabilityType: c.CapabilityType, + ResponseType: c.ResponseType, + ConfigurationContract: c.ConfigurationContract, + } + } + nodes, err := registry.GetNodesByP2PIds(nil, peerIDsToBytes(p2pIDs)) + if err != nil { + panic(err) + } + for _, n := range nodes { + caps := make([]kcr.CapabilitiesRegistryCapability, len(n.HashedCapabilityIds)) + for i, h := range n.HashedCapabilityIds { + c, ok := capMap[h] + if !ok { + panic("capability not found") + } + caps[i] = c + } + m[n.P2pId] = caps + } + return m +} + +func peerIDsToBytes(p2pIDs []p2pkey.PeerID) [][32]byte { + bs := make([][32]byte, len(p2pIDs)) + for i, p := range p2pIDs { + bs[i] = p + } + return bs +} diff --git a/deployment/keystone/changeset/update_nodes.go b/deployment/keystone/changeset/update_nodes.go index 7e436160d2e..d07ffe5b025 100644 --- a/deployment/keystone/changeset/update_nodes.go +++ b/deployment/keystone/changeset/update_nodes.go @@ -3,22 +3,82 @@ package changeset import ( "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" "github.com/smartcontractkit/chainlink/deployment" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" + + kslib "github.com/smartcontractkit/chainlink/deployment/keystone" "github.com/smartcontractkit/chainlink/deployment/keystone/changeset/internal" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" ) var _ deployment.ChangeSet[*UpdateNodesRequest] = UpdateNodes -type UpdateNodesRequest = internal.UpdateNodesRequest +type UpdateNodesRequest struct { + RegistryChainSel uint64 + P2pToUpdates map[p2pkey.PeerID]NodeUpdate + + UseMCMS bool +} type NodeUpdate = internal.NodeUpdate // UpdateNodes updates the a set of nodes. -// This a complex action in practice that involves registering missing capabilities, adding the nodes, and updating -// the capabilities of the DON +// The nodes and capabilities in the request must already exist in the registry contract. func UpdateNodes(env deployment.Environment, req *UpdateNodesRequest) (deployment.ChangesetOutput, error) { - _, err := internal.UpdateNodes(env.Logger, req) + // extract the registry contract and chain from the environment + registryChain, ok := env.Chains[req.RegistryChainSel] + if !ok { + return deployment.ChangesetOutput{}, fmt.Errorf("registry chain selector %d does not exist in environment", req.RegistryChainSel) + } + cresp, err := kslib.GetContractSets(env.Logger, &kslib.GetContractSetsRequest{ + Chains: env.Chains, + AddressBook: env.ExistingAddresses, + }) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get contract sets: %w", err) + } + contracts, exists := cresp.ContractSets[req.RegistryChainSel] + if !exists { + return deployment.ChangesetOutput{}, fmt.Errorf("contract set not found for chain %d", req.RegistryChainSel) + } + + resp, err := internal.UpdateNodes(env.Logger, &internal.UpdateNodesRequest{ + Chain: registryChain, + Registry: contracts.CapabilitiesRegistry, + ContractSet: &contracts, + P2pToUpdates: req.P2pToUpdates, + UseMCMS: req.UseMCMS, + }) if err != nil { return deployment.ChangesetOutput{}, fmt.Errorf("failed to update don: %w", err) } - return deployment.ChangesetOutput{}, nil + + out := deployment.ChangesetOutput{} + if req.UseMCMS { + if resp.Ops == nil { + return out, fmt.Errorf("expected MCMS operation to be non-nil") + } + timelocksPerChain := map[uint64]common.Address{ + req.RegistryChainSel: contracts.Timelock.Address(), + } + proposerMCMSes := map[uint64]*gethwrappers.ManyChainMultiSig{ + req.RegistryChainSel: contracts.ProposerMcm, + } + + proposal, err := proposalutils.BuildProposalFromBatches( + timelocksPerChain, + proposerMCMSes, + []timelock.BatchChainOperation{*resp.Ops}, + "proposal to set update nodes", + 0, + ) + if err != nil { + return out, fmt.Errorf("failed to build proposal: %w", err) + } + out.Proposals = []timelock.MCMSWithTimelockProposal{*proposal} + } + + return out, nil } diff --git a/deployment/keystone/changeset/update_nodes_test.go b/deployment/keystone/changeset/update_nodes_test.go new file mode 100644 index 00000000000..10c08333d22 --- /dev/null +++ b/deployment/keystone/changeset/update_nodes_test.go @@ -0,0 +1,125 @@ +package changeset_test + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + + "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/p2pkey" +) + +func TestUpdateNodes(t *testing.T) { + t.Parallel() + + t.Run("no mcms", func(t *testing.T) { + te := SetupTestEnv(t, TestConfig{ + WFDonConfig: DonConfig{N: 4}, + AssetDonConfig: DonConfig{N: 4}, + WriterDonConfig: DonConfig{N: 4}, + NumChains: 1, + }) + + updates := make(map[p2pkey.PeerID]changeset.NodeUpdate) + i := uint8(0) + for id, _ := range te.WFNodes { + k, err := p2pkey.MakePeerID(id) + require.NoError(t, err) + pubKey := [32]byte{31: i + 1} + // don't set capabilities or nop b/c those must already exist in the contract + // those ops must be a different proposal when using MCMS + updates[k] = changeset.NodeUpdate{ + EncryptionPublicKey: hex.EncodeToString(pubKey[:]), + Signer: [32]byte{0: i + 1}, + } + i++ + } + + cfg := changeset.UpdateNodesRequest{ + RegistryChainSel: te.RegistrySelector, + P2pToUpdates: updates, + } + + csOut, err := changeset.UpdateNodes(te.Env, &cfg) + require.NoError(t, err) + require.Len(t, csOut.Proposals, 0) + require.Nil(t, csOut.AddressBook) + + validateUpdate(t, te, updates) + }) + + t.Run("with mcms", func(t *testing.T) { + te := SetupTestEnv(t, TestConfig{ + WFDonConfig: DonConfig{N: 4}, + AssetDonConfig: DonConfig{N: 4}, + WriterDonConfig: DonConfig{N: 4}, + NumChains: 1, + UseMCMS: true, + }) + + updates := make(map[p2pkey.PeerID]changeset.NodeUpdate) + i := uint8(0) + for id, _ := range te.WFNodes { + k, err := p2pkey.MakePeerID(id) + require.NoError(t, err) + pubKey := [32]byte{31: i + 1} + // don't set capabilities or nop b/c those must already exist in the contract + // those ops must be a different proposal when using MCMS + updates[k] = changeset.NodeUpdate{ + EncryptionPublicKey: hex.EncodeToString(pubKey[:]), + Signer: [32]byte{0: i + 1}, + } + i++ + } + + cfg := changeset.UpdateNodesRequest{ + RegistryChainSel: te.RegistrySelector, + P2pToUpdates: updates, + UseMCMS: true, + } + + csOut, err := changeset.UpdateNodes(te.Env, &cfg) + require.NoError(t, err) + require.Len(t, csOut.Proposals, 1) + require.Nil(t, csOut.AddressBook) + + // now apply the changeset such that the proposal is signed and execed + contracts := te.ContractSets()[te.RegistrySelector] + timelocks := map[uint64]*gethwrappers.RBACTimelock{ + te.RegistrySelector: contracts.Timelock, + } + _, err = commonchangeset.ApplyChangesets(t, te.Env, timelocks, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(changeset.UpdateNodes), + Config: &changeset.UpdateNodesRequest{ + RegistryChainSel: te.RegistrySelector, + P2pToUpdates: updates, + UseMCMS: true, + }, + }, + }) + require.NoError(t, err) + + validateUpdate(t, te, updates) + }) + +} + +// validateUpdate checks reads nodes from the registry and checks they have the expected updates +func validateUpdate(t *testing.T, te TestEnv, expected map[p2pkey.PeerID]changeset.NodeUpdate) { + registry := te.ContractSets()[te.RegistrySelector].CapabilitiesRegistry + wfP2PIDs := p2pIDs(t, maps.Keys(te.WFNodes)) + nodes, err := registry.GetNodesByP2PIds(nil, wfP2PIDs) + require.NoError(t, err) + require.Len(t, nodes, len(wfP2PIDs)) + for _, node := range nodes { + // only check the fields that were updated + assert.Equal(t, expected[node.P2pId].EncryptionPublicKey, hex.EncodeToString(node.EncryptionPublicKey[:])) + assert.Equal(t, expected[node.P2pId].Signer, node.Signer) + } +} diff --git a/deployment/keystone/deploy.go b/deployment/keystone/deploy.go index da277bc3497..b896d042b70 100644 --- a/deployment/keystone/deploy.go +++ b/deployment/keystone/deploy.go @@ -21,7 +21,6 @@ import ( "github.com/smartcontractkit/ccip-owner-contracts/pkg/gethwrappers" "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/mcms" "github.com/smartcontractkit/ccip-owner-contracts/pkg/proposal/timelock" - "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" @@ -424,30 +423,19 @@ type RegisteredCapability struct { ID [32]byte } -func FromCapabilitiesRegistryCapability(cap *kcr.CapabilitiesRegistryCapability, e deployment.Environment, registryChainSelector uint64) (*RegisteredCapability, error) { - registry, _, err := GetRegistryContract(&e, registryChainSelector, e.ExistingAddresses) - if err != nil { - return nil, fmt.Errorf("failed to get registry: %w", err) - } - id, err := registry.GetHashedCapabilityId(&bind.CallOpts{}, cap.LabelledName, cap.Version) - if err != nil { - return nil, fmt.Errorf("failed to call GetHashedCapabilityId for capability %v: %w", cap, err) - } - return &RegisteredCapability{ - CapabilitiesRegistryCapability: *cap, - ID: id, - }, nil -} - // RegisterCapabilities add computes the capability id, adds it to the registry and associates the registered capabilities with appropriate don(s) func RegisterCapabilities(lggr logger.Logger, req RegisterCapabilitiesRequest) (*RegisterCapabilitiesResponse, error) { if len(req.DonToCapabilities) == 0 { return nil, fmt.Errorf("no capabilities to register") } - registry, registryChain, err := GetRegistryContract(req.Env, req.RegistryChainSelector, req.Env.ExistingAddresses) - if err != nil { - return nil, fmt.Errorf("failed to get registry: %w", err) - } + cresp, err := GetContractSets(req.Env.Logger, &GetContractSetsRequest{ + Chains: req.Env.Chains, + AddressBook: req.Env.ExistingAddresses, + }) + contracts := cresp.ContractSets[req.RegistryChainSelector] + registry := contracts.CapabilitiesRegistry + registryChain := req.Env.Chains[req.RegistryChainSelector] + lggr.Infow("registering capabilities...", "len", len(req.DonToCapabilities)) resp := &RegisterCapabilitiesResponse{ DonToCapabilities: make(map[string][]RegisteredCapability), @@ -481,8 +469,8 @@ func RegisterCapabilities(lggr logger.Logger, req RegisterCapabilitiesRequest) ( for cap := range uniqueCaps { capabilities = append(capabilities, cap) } - - err = AddCapabilities(lggr, registry, registryChain, capabilities) + // not using mcms; ignore proposals + _, err = AddCapabilities(lggr, &contracts, registryChain, capabilities, false) if err != nil { return nil, fmt.Errorf("failed to add capabilities: %w", err) }