Skip to content

Commit 38fa56e

Browse files
authored
Remove nodes from Capability Registry changeset (smartcontractkit#16734)
* RemoveNodes Changeset * fix lint
1 parent 074bd35 commit 38fa56e

File tree

2 files changed

+260
-2
lines changed

2 files changed

+260
-2
lines changed

deployment/ccip/changeset/v1_6/cs_home_chain.go

+139-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,16 @@ import (
2929
p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types"
3030
)
3131

32-
var _ deployment.ChangeSet[DeployHomeChainConfig] = DeployHomeChainChangeset
32+
var (
33+
_ deployment.ChangeSet[DeployHomeChainConfig] = DeployHomeChainChangeset
34+
// RemoveNodesFromCapRegChangeset is a changeset that removes nodes from the CapabilitiesRegistry contract.
35+
// It fails validation
36+
// - if the changeset is executed neither by CapabilitiesRegistry contract owner nor by the node operator admin.
37+
// - if node is not already present in the CapabilitiesRegistry contract.
38+
// - if node is part of CapabilitiesDON
39+
// - if node is part of WorkflowDON
40+
RemoveNodesFromCapRegChangeset = deployment.CreateChangeSet(removeNodesLogic, removeNodesPrecondition)
41+
)
3342

3443
// DeployHomeChainChangeset is a separate changeset because it is a standalone deployment performed once in home chain for the entire CCIP deployment.
3544
func DeployHomeChainChangeset(env deployment.Environment, cfg DeployHomeChainConfig) (deployment.ChangesetOutput, error) {
@@ -433,6 +442,7 @@ func addNodes(
433442
lggr.Infow("No new nodes to add")
434443
return nil
435444
}
445+
lggr.Infow("Adding nodes", "chain", chain.String(), "nodes", p2pIDsByNodeOpId)
436446
tx, err := capReg.AddNodes(chain.DeployerKey, nodeParams)
437447
if err != nil {
438448
lggr.Errorw("Failed to add nodes", "chain", chain.String(),
@@ -494,7 +504,7 @@ func RemoveDONs(e deployment.Environment, cfg RemoveDONsConfig) (deployment.Chan
494504
return deployment.ChangesetOutput{}, err
495505
}
496506
if cfg.MCMS == nil {
497-
_, err = homeChain.Confirm(tx)
507+
_, err = deployment.ConfirmIfNoErrorWithABI(homeChain, tx, capabilities_registry.CapabilitiesRegistryABI, err)
498508
if err != nil {
499509
return deployment.ChangesetOutput{}, err
500510
}
@@ -529,3 +539,130 @@ func RemoveDONs(e deployment.Environment, cfg RemoveDONsConfig) (deployment.Chan
529539
e.Logger.Infof("Created proposal to remove dons")
530540
return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil
531541
}
542+
543+
type RemoveNodesConfig struct {
544+
HomeChainSel uint64
545+
P2PIDsToRemove [][32]byte
546+
MCMSCfg *changeset.MCMSConfig
547+
}
548+
549+
func removeNodesPrecondition(env deployment.Environment, c RemoveNodesConfig) error {
550+
state, err := changeset.LoadOnchainState(env)
551+
if err != nil {
552+
return err
553+
}
554+
if err := changeset.ValidateChain(env, state, c.HomeChainSel, c.MCMSCfg); err != nil {
555+
return err
556+
}
557+
if len(c.P2PIDsToRemove) == 0 {
558+
return errors.New("p2p ids to remove must be set")
559+
}
560+
for _, p2pID := range c.P2PIDsToRemove {
561+
if bytes.Equal(p2pID[:], make([]byte, 32)) {
562+
return errors.New("empty p2p id")
563+
}
564+
}
565+
566+
// Cap reg must exist
567+
if state.Chains[c.HomeChainSel].CapabilityRegistry == nil {
568+
return fmt.Errorf("cap reg does not exist for home chain %d", c.HomeChainSel)
569+
}
570+
capReg := state.Chains[c.HomeChainSel].CapabilityRegistry
571+
nodeInfos, err := capReg.GetNodes(&bind.CallOpts{
572+
Context: env.GetContext(),
573+
})
574+
if err != nil {
575+
return fmt.Errorf("failed to get nodes from Capreg %s: %w", capReg.Address().String(), err)
576+
}
577+
capRegOwner, err := capReg.Owner(&bind.CallOpts{
578+
Context: env.GetContext(),
579+
})
580+
if err != nil {
581+
return fmt.Errorf("failed to get owner of Capreg %s: %w", capReg.Address().String(), err)
582+
}
583+
txSender := env.Chains[c.HomeChainSel].DeployerKey.From
584+
if c.MCMSCfg != nil {
585+
txSender = state.Chains[c.HomeChainSel].Timelock.Address()
586+
}
587+
existingP2PIDs := make(map[[32]byte]capabilities_registry.INodeInfoProviderNodeInfo)
588+
for _, nodeInfo := range nodeInfos {
589+
existingP2PIDs[nodeInfo.P2pId] = nodeInfo
590+
}
591+
for _, p2pID := range c.P2PIDsToRemove {
592+
info, exists := existingP2PIDs[p2pID]
593+
if !exists {
594+
return fmt.Errorf("p2p id %x does not exist in Capreg %s", p2pID[:], capReg.Address().String())
595+
}
596+
nop, err := capReg.GetNodeOperator(nil, info.NodeOperatorId)
597+
if err != nil {
598+
return fmt.Errorf("failed to get node operator %d for node %x: %w", info.NodeOperatorId, p2pID[:], err)
599+
}
600+
if txSender != capRegOwner && txSender != nop.Admin {
601+
return fmt.Errorf("tx sender %s is not the owner %s of Capreg %s or admin %s for node %x",
602+
txSender.String(), capRegOwner.String(), capReg.Address().String(), nop.Admin.String(), p2pID[:])
603+
}
604+
if len(info.CapabilitiesDONIds) > 0 {
605+
return fmt.Errorf("p2p id %x is part of CapabilitiesDON, cannot remove", p2pID[:])
606+
}
607+
if info.WorkflowDONId != 0 {
608+
return fmt.Errorf("p2p id %x is part of WorkflowDON, cannot remove", p2pID[:])
609+
}
610+
}
611+
612+
return nil
613+
}
614+
615+
func removeNodesLogic(env deployment.Environment, c RemoveNodesConfig) (deployment.ChangesetOutput, error) {
616+
state, err := changeset.LoadOnchainState(env)
617+
if err != nil {
618+
return deployment.ChangesetOutput{}, err
619+
}
620+
homeChainState := state.Chains[c.HomeChainSel]
621+
homeChain := env.Chains[c.HomeChainSel]
622+
txOpts := homeChain.DeployerKey
623+
if c.MCMSCfg != nil {
624+
txOpts = deployment.SimTransactOpts()
625+
}
626+
tx, err := homeChainState.CapabilityRegistry.RemoveNodes(txOpts, c.P2PIDsToRemove)
627+
if c.MCMSCfg == nil {
628+
_, err = deployment.ConfirmIfNoErrorWithABI(homeChain, tx, capabilities_registry.CapabilitiesRegistryABI, err)
629+
if err != nil {
630+
return deployment.ChangesetOutput{}, fmt.Errorf("failed to remove nodes from capreg %s: %w",
631+
homeChainState.CapabilityRegistry.Address().String(), err)
632+
}
633+
env.Logger.Infof("Removed nodes using deployer key tx %s", tx.Hash().String())
634+
return deployment.ChangesetOutput{}, nil
635+
}
636+
if err != nil {
637+
return deployment.ChangesetOutput{}, err
638+
}
639+
batchOperation, err := proposalutils.BatchOperationForChain(c.HomeChainSel,
640+
homeChainState.CapabilityRegistry.Address().Hex(), tx.Data(), big.NewInt(0),
641+
string(changeset.CapabilitiesRegistry), []string{})
642+
if err != nil {
643+
return deployment.ChangesetOutput{}, fmt.Errorf("failed to create batch operation for home chain: %w", err)
644+
}
645+
646+
timelocks := changeset.BuildTimelockAddressPerChain(env, state)
647+
proposerMcms := changeset.BuildProposerMcmAddressesPerChain(env, state)
648+
inspectors := make(map[uint64]mcmssdk.Inspector)
649+
inspectors[c.HomeChainSel], err = proposalutils.McmsInspectorForChain(env, c.HomeChainSel)
650+
if err != nil {
651+
return deployment.ChangesetOutput{}, fmt.Errorf("failed to get mcms inspector for chain %s: %w", homeChain.String(), err)
652+
}
653+
proposal, err := proposalutils.BuildProposalFromBatchesV2(
654+
env,
655+
timelocks,
656+
proposerMcms,
657+
inspectors,
658+
[]mcmstypes.BatchOperation{batchOperation},
659+
"Remove Nodes from CapabilitiesRegistry",
660+
c.MCMSCfg.MinDelay,
661+
)
662+
if err != nil {
663+
return deployment.ChangesetOutput{}, err
664+
}
665+
666+
env.Logger.Infof("Created proposal to remove nodes")
667+
return deployment.ChangesetOutput{MCMSTimelockProposals: []mcmslib.TimelockProposal{*proposal}}, nil
668+
}

deployment/ccip/changeset/v1_6/cs_home_chain_test.go

+121
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ package v1_6_test
33
import (
44
"testing"
55

6+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
67
"github.com/ethereum/go-ethereum/common"
78
"github.com/stretchr/testify/require"
89
"go.uber.org/zap/zapcore"
910

11+
"github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext"
12+
1013
"github.com/smartcontractkit/chainlink-integrations/evm/utils"
1114

1215
"github.com/smartcontractkit/chainlink/deployment"
@@ -269,3 +272,121 @@ func TestAddDonAfterRemoveDons(t *testing.T) {
269272
)
270273
require.NoError(t, err)
271274
}
275+
276+
func TestRemoveNodes(t *testing.T) {
277+
for _, tc := range []struct {
278+
name string
279+
mcmsEnabled bool
280+
}{
281+
{
282+
name: "MCMS enabled",
283+
mcmsEnabled: true,
284+
},
285+
{
286+
name: "MCMS disabled",
287+
mcmsEnabled: false,
288+
},
289+
} {
290+
t.Run(tc.name, func(t *testing.T) {
291+
ctx := testcontext.Get(t)
292+
e, tEnv := testhelpers.NewMemoryEnvironment(t, testhelpers.WithPrerequisiteDeploymentOnly(nil))
293+
nodes, err := deployment.NodeInfo(e.Env.NodeIDs, e.Env.Offchain)
294+
require.NoError(t, err)
295+
// apply the DeployHomeChain changeset, and timelock
296+
e.Env, err = commoncs.ApplyChangesets(t, e.Env, nil, []commoncs.ConfiguredChangeSet{
297+
commoncs.Configure(
298+
deployment.CreateLegacyChangeSet(v1_6.DeployHomeChainChangeset),
299+
v1_6.DeployHomeChainConfig{
300+
HomeChainSel: e.HomeChainSel,
301+
RMNDynamicConfig: testhelpers.NewTestRMNDynamicConfig(),
302+
RMNStaticConfig: testhelpers.NewTestRMNStaticConfig(),
303+
NodeOperators: testhelpers.NewTestNodeOperator(e.Env.Chains[e.HomeChainSel].DeployerKey.From),
304+
NodeP2PIDsPerNodeOpAdmin: map[string][][32]byte{
305+
testhelpers.TestNodeOperator: nodes.NonBootstraps().PeerIDs(),
306+
},
307+
},
308+
),
309+
})
310+
require.NoError(t, err)
311+
312+
s, err := changeset.LoadOnchainState(e.Env)
313+
require.NoError(t, err)
314+
state, err := changeset.LoadOnchainState(e.Env)
315+
require.NoError(t, err)
316+
homeChain := s.Chains[e.HomeChainSel]
317+
allChains := e.Env.AllChainSelectors()
318+
319+
var mcmsConfig *changeset.MCMSConfig
320+
if tc.mcmsEnabled {
321+
mcmsConfig = &changeset.MCMSConfig{
322+
MinDelay: 0,
323+
}
324+
}
325+
if tc.mcmsEnabled {
326+
// Transfer ownership to timelock so that we can promote the zero digest later down the line.
327+
_, err := commoncs.Apply(t, e.Env,
328+
map[uint64]*proposalutils.TimelockExecutionContracts{
329+
e.HomeChainSel: {
330+
Timelock: state.Chains[e.HomeChainSel].Timelock,
331+
CallProxy: state.Chains[e.HomeChainSel].CallProxy,
332+
},
333+
},
334+
commoncs.Configure(
335+
deployment.CreateLegacyChangeSet(commoncs.TransferToMCMSWithTimelock),
336+
commoncs.TransferToMCMSWithTimelockConfig{
337+
ContractsByChain: map[uint64][]common.Address{
338+
e.HomeChainSel: {homeChain.CapabilityRegistry.Address()},
339+
},
340+
},
341+
),
342+
)
343+
require.NoError(t, err)
344+
owner, err := homeChain.CapabilityRegistry.Owner(&bind.CallOpts{
345+
Context: ctx,
346+
})
347+
require.NoError(t, err)
348+
require.Equal(t, state.Chains[e.HomeChainSel].Timelock.Address(), owner)
349+
}
350+
e.Env, err = commoncs.Apply(t, e.Env,
351+
map[uint64]*proposalutils.TimelockExecutionContracts{
352+
e.HomeChainSel: {
353+
Timelock: state.Chains[e.HomeChainSel].Timelock,
354+
CallProxy: state.Chains[e.HomeChainSel].CallProxy,
355+
},
356+
},
357+
commoncs.Configure(v1_6.RemoveNodesFromCapRegChangeset,
358+
v1_6.RemoveNodesConfig{
359+
HomeChainSel: e.HomeChainSel,
360+
P2PIDsToRemove: nodes.NonBootstraps().PeerIDs(),
361+
MCMSCfg: mcmsConfig,
362+
}))
363+
require.NoError(t, err)
364+
365+
// get all nodes
366+
nodesAfterCS, err := homeChain.CapabilityRegistry.GetNodes(&bind.CallOpts{
367+
Context: ctx,
368+
})
369+
require.NoError(t, err)
370+
require.Empty(t, nodesAfterCS)
371+
// currently DeployHomeChainChangeset only applies with MCMS disabled
372+
// check if the nodes are readded to the cap reg following rest of the deployment process
373+
if !tc.mcmsEnabled {
374+
tEnv.UpdateDeployedEnvironment(e)
375+
testhelpers.AddCCIPContractsToEnvironment(t, allChains, tEnv, false)
376+
nodesNow, err := homeChain.CapabilityRegistry.GetNodes(&bind.CallOpts{
377+
Context: ctx,
378+
})
379+
require.NoError(t, err)
380+
require.Len(t, nodesNow, len(nodes.NonBootstraps().PeerIDs()))
381+
nodeP2pKeys := make(map[[32]byte]struct{})
382+
for _, node := range nodesNow {
383+
nodeP2pKeys[node.P2pId] = struct{}{}
384+
}
385+
for _, p2pID := range nodes.NonBootstraps().PeerIDs() {
386+
_, ok := nodeP2pKeys[p2pID]
387+
require.True(t, ok)
388+
}
389+
}
390+
})
391+
}
392+
}

0 commit comments

Comments
 (0)