From f9cc5147878cc5f454e2f36ce9e7da3035af04d3 Mon Sep 17 00:00:00 2001 From: "Simon B.Robert" Date: Wed, 15 Jan 2025 14:28:18 -0500 Subject: [PATCH] New rmn curse changeset (#15868) * Initial commit * Add unit tests * Cleanup * Add uncurse changeset * Address linting issues * Fix more linting issues * Use changeset in e2e test * Address PR feedback * Fix linting issue and bug with subject * Remove nolint * Use error group * Use global curse only instead of CurseChain since it curse all connected chains * Address PR comments * Fix linting issue * Merge develop * Use fmnt.Errorf * Fix linting issue * Fix linting issue * Additional comments and renaming * Enhance idempotency checks in RMN curse and uncurse operations * Fix build error * Address PR comments * Fix build error * Fix test name * Add more test for deployer group * Fix linting issue --- .../ccip/changeset/cs_rmn_curse_uncurse.go | 334 +++++++++++++++ .../changeset/cs_rmn_curse_uncurse_test.go | 379 ++++++++++++++++++ .../ccip/changeset/cs_update_rmn_config.go | 29 -- deployment/ccip/changeset/deployer_group.go | 194 +++++++++ .../ccip/changeset/deployer_group_test.go | 191 +++++++++ integration-tests/smoke/ccip/ccip_rmn_test.go | 98 +++-- 6 files changed, 1155 insertions(+), 70 deletions(-) create mode 100644 deployment/ccip/changeset/cs_rmn_curse_uncurse.go create mode 100644 deployment/ccip/changeset/cs_rmn_curse_uncurse_test.go create mode 100644 deployment/ccip/changeset/deployer_group.go create mode 100644 deployment/ccip/changeset/deployer_group_test.go diff --git a/deployment/ccip/changeset/cs_rmn_curse_uncurse.go b/deployment/ccip/changeset/cs_rmn_curse_uncurse.go new file mode 100644 index 00000000000..1f3222014e3 --- /dev/null +++ b/deployment/ccip/changeset/cs_rmn_curse_uncurse.go @@ -0,0 +1,334 @@ +package changeset + +import ( + "encoding/binary" + "errors" + "fmt" + + "github.com/smartcontractkit/chainlink/deployment" + commoncs "github.com/smartcontractkit/chainlink/deployment/common/changeset" +) + +// GlobalCurseSubject as defined here: https://github.com/smartcontractkit/chainlink/blob/new-rmn-curse-changeset/contracts/src/v0.8/ccip/rmn/RMNRemote.sol#L15 +func GlobalCurseSubject() Subject { + return Subject{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01} +} + +// RMNCurseAction represent a curse action to be applied on a chain (ChainSelector) with a specific subject (SubjectToCurse) +// The curse action will by applied by calling the Curse method on the RMNRemote contract on the chain (ChainSelector) +type RMNCurseAction struct { + ChainSelector uint64 + SubjectToCurse Subject +} + +// CurseAction is a function that returns a list of RMNCurseAction to be applied on a chain +// CurseChain, CurseLane, CurseGloballyOnlyOnSource are examples of function implementing CurseAction +type CurseAction func(e deployment.Environment) []RMNCurseAction + +type RMNCurseConfig struct { + MCMS *MCMSConfig + CurseActions []CurseAction + Reason string +} + +func (c RMNCurseConfig) Validate(e deployment.Environment) error { + state, err := LoadOnchainState(e) + + if err != nil { + return fmt.Errorf("failed to load onchain state: %w", err) + } + + if len(c.CurseActions) == 0 { + return errors.New("curse actions are required") + } + + if c.Reason == "" { + return errors.New("reason is required") + } + + validSubjects := map[Subject]struct{}{ + GlobalCurseSubject(): {}, + } + for _, selector := range e.AllChainSelectors() { + validSubjects[SelectorToSubject(selector)] = struct{}{} + } + + for _, curseAction := range c.CurseActions { + result := curseAction(e) + for _, action := range result { + targetChain := e.Chains[action.ChainSelector] + targetChainState, ok := state.Chains[action.ChainSelector] + if !ok { + return fmt.Errorf("chain %s not found in onchain state", targetChain.String()) + } + + if err := commoncs.ValidateOwnership(e.GetContext(), c.MCMS != nil, targetChain.DeployerKey.From, targetChainState.Timelock.Address(), targetChainState.RMNRemote); err != nil { + return fmt.Errorf("chain %s: %w", targetChain.String(), err) + } + + if err = deployment.IsValidChainSelector(action.ChainSelector); err != nil { + return fmt.Errorf("invalid chain selector %d for chain %s", action.ChainSelector, targetChain.String()) + } + + if _, ok := validSubjects[action.SubjectToCurse]; !ok { + return fmt.Errorf("invalid subject %x for chain %s", action.SubjectToCurse, targetChain.String()) + } + } + } + + return nil +} + +type Subject = [16]byte + +func SelectorToSubject(selector uint64) Subject { + var b Subject + binary.BigEndian.PutUint64(b[8:], selector) + return b +} + +// CurseLaneOnlyOnSource curses a lane only on the source chain +// This will prevent message from source to destination to be initiated +// One noteworthy behaviour is that this means that message can be sent from destination to source but will not be executed on the source +// Given 3 chains A, B, C +// CurseLaneOnlyOnSource(A, B) will curse A with the curse subject of B +func CurseLaneOnlyOnSource(sourceSelector uint64, destinationSelector uint64) CurseAction { + // Curse from source to destination + return func(e deployment.Environment) []RMNCurseAction { + return []RMNCurseAction{ + { + ChainSelector: sourceSelector, + SubjectToCurse: SelectorToSubject(destinationSelector), + }, + } + } +} + +// CurseGloballyOnlyOnChain curses a chain globally only on the source chain +// Given 3 chains A, B, C +// CurseGloballyOnlyOnChain(A) will curse a with the global curse subject only +func CurseGloballyOnlyOnChain(selector uint64) CurseAction { + return func(e deployment.Environment) []RMNCurseAction { + return []RMNCurseAction{ + { + ChainSelector: selector, + SubjectToCurse: GlobalCurseSubject(), + }, + } + } +} + +// Call Curse on both RMNRemote from source and destination to prevent message from source to destination and vice versa +// Given 3 chains A, B, C +// CurseLaneBidirectionally(A, B) will curse A with the curse subject of B and B with the curse subject of A +func CurseLaneBidirectionally(sourceSelector uint64, destinationSelector uint64) CurseAction { + // Bidirectional curse between two chains + return func(e deployment.Environment) []RMNCurseAction { + return append( + CurseLaneOnlyOnSource(sourceSelector, destinationSelector)(e), + CurseLaneOnlyOnSource(destinationSelector, sourceSelector)(e)..., + ) + } +} + +// CurseChain do a global curse on chainSelector and curse chainSelector on all other chains +// Given 3 chains A, B, C +// CurseChain(A) will curse A with the global curse subject and curse B and C with the curse subject of A +func CurseChain(chainSelector uint64) CurseAction { + return func(e deployment.Environment) []RMNCurseAction { + chainSelectors := e.AllChainSelectors() + + // Curse all other chains to prevent onramp from sending message to the cursed chain + var curseActions []RMNCurseAction + for _, otherChainSelector := range chainSelectors { + if otherChainSelector != chainSelector { + curseActions = append(curseActions, RMNCurseAction{ + ChainSelector: otherChainSelector, + SubjectToCurse: SelectorToSubject(chainSelector), + }) + } + } + + // Curse the chain with a global curse to prevent any onramp or offramp message from send message in and out of the chain + curseActions = append(curseActions, CurseGloballyOnlyOnChain(chainSelector)(e)...) + + return curseActions + } +} + +func groupRMNSubjectBySelector(rmnSubjects []RMNCurseAction, avoidCursingSelf bool, onlyKeepGlobal bool) map[uint64][]Subject { + grouped := make(map[uint64][]Subject) + for _, s := range rmnSubjects { + // Skip self-curse if needed + if s.SubjectToCurse == SelectorToSubject(s.ChainSelector) && avoidCursingSelf { + continue + } + // Initialize slice for this chain if needed + if _, ok := grouped[s.ChainSelector]; !ok { + grouped[s.ChainSelector] = []Subject{} + } + // If global is already set and we only keep global, skip + if onlyKeepGlobal && len(grouped[s.ChainSelector]) == 1 && grouped[s.ChainSelector][0] == GlobalCurseSubject() { + continue + } + // If subject is global and we only keep global, reset immediately + if s.SubjectToCurse == GlobalCurseSubject() && onlyKeepGlobal { + grouped[s.ChainSelector] = []Subject{GlobalCurseSubject()} + continue + } + // Ensure uniqueness + duplicate := false + for _, added := range grouped[s.ChainSelector] { + if added == s.SubjectToCurse { + duplicate = true + break + } + } + if !duplicate { + grouped[s.ChainSelector] = append(grouped[s.ChainSelector], s.SubjectToCurse) + } + } + + return grouped +} + +// RMNCurseChangeset creates a new changeset for cursing chains or lanes on RMNRemote contracts. +// Example usage: +// +// cfg := RMNCurseConfig{ +// CurseActions: []CurseAction{ +// CurseChain(SEPOLIA_CHAIN_SELECTOR), +// CurseLane(SEPOLIA_CHAIN_SELECTOR, AVAX_FUJI_CHAIN_SELECTOR), +// }, +// CurseReason: "test curse", +// MCMS: &MCMSConfig{MinDelay: 0}, +// } +// output, err := RMNCurseChangeset(env, cfg) +func RMNCurseChangeset(e deployment.Environment, cfg RMNCurseConfig) (deployment.ChangesetOutput, error) { + err := cfg.Validate(e) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + state, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to load onchain state: %w", err) + } + deployerGroup := NewDeployerGroup(e, state, cfg.MCMS) + + // Generate curse actions + var curseActions []RMNCurseAction + for _, curseAction := range cfg.CurseActions { + curseActions = append(curseActions, curseAction(e)...) + } + // Group curse actions by chain selector + grouped := groupRMNSubjectBySelector(curseActions, true, true) + // For each chain in the environment get the RMNRemote contract and call curse + for selector, chain := range state.Chains { + deployer, err := deployerGroup.getDeployer(selector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get deployer for chain %d: %w", selector, err) + } + if curseSubjects, ok := grouped[selector]; ok { + // Only curse the subjects that are not actually cursed + notAlreadyCursedSubjects := make([]Subject, 0) + for _, subject := range curseSubjects { + cursed, err := chain.RMNRemote.IsCursed(nil, subject) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to check if chain %d is cursed: %w", selector, err) + } + + if !cursed { + notAlreadyCursedSubjects = append(notAlreadyCursedSubjects, subject) + } else { + e.Logger.Warnf("chain %s subject %x is already cursed, ignoring it while cursing", e.Chains[selector].Name(), subject) + } + } + + if len(notAlreadyCursedSubjects) == 0 { + e.Logger.Infof("chain %s is already cursed with all the subjects, skipping", e.Chains[selector].Name()) + continue + } + + _, err := chain.RMNRemote.Curse0(deployer, notAlreadyCursedSubjects) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to curse chain %d: %w", selector, err) + } + e.Logger.Infof("Cursed chain %d with subjects %v", selector, notAlreadyCursedSubjects) + } + } + + return deployerGroup.enact("proposal to curse RMNs: " + cfg.Reason) +} + +// RMNUncurseChangeset creates a new changeset for uncursing chains or lanes on RMNRemote contracts. +// Example usage: +// +// cfg := RMNCurseConfig{ +// CurseActions: []CurseAction{ +// CurseChain(SEPOLIA_CHAIN_SELECTOR), +// CurseLane(SEPOLIA_CHAIN_SELECTOR, AVAX_FUJI_CHAIN_SELECTOR), +// }, +// MCMS: &MCMSConfig{MinDelay: 0}, +// } +// output, err := RMNUncurseChangeset(env, cfg) +// +// Curse actions are reused and reverted instead of applied in this changeset +func RMNUncurseChangeset(e deployment.Environment, cfg RMNCurseConfig) (deployment.ChangesetOutput, error) { + err := cfg.Validate(e) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + state, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to load onchain state: %w", err) + } + deployerGroup := NewDeployerGroup(e, state, cfg.MCMS) + + // Generate curse actions + var curseActions []RMNCurseAction + for _, curseAction := range cfg.CurseActions { + curseActions = append(curseActions, curseAction(e)...) + } + // Group curse actions by chain selector + grouped := groupRMNSubjectBySelector(curseActions, false, false) + + // For each chain in the environement get the RMNRemote contract and call uncurse + for selector, chain := range state.Chains { + deployer, err := deployerGroup.getDeployer(selector) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to get deployer for chain %d: %w", selector, err) + } + + if curseSubjects, ok := grouped[selector]; ok { + // Only keep the subject that are actually cursed + actuallyCursedSubjects := make([]Subject, 0) + for _, subject := range curseSubjects { + cursed, err := chain.RMNRemote.IsCursed(nil, subject) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to check if chain %d is cursed: %w", selector, err) + } + + if cursed { + actuallyCursedSubjects = append(actuallyCursedSubjects, subject) + } else { + e.Logger.Warnf("chain %s subject %x is not cursed, ignoring it while uncursing", e.Chains[selector].Name(), subject) + } + } + + if len(actuallyCursedSubjects) == 0 { + e.Logger.Infof("chain %s is not cursed with any of the subjects, skipping", e.Chains[selector].Name()) + continue + } + + _, err := chain.RMNRemote.Uncurse0(deployer, actuallyCursedSubjects) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to uncurse chain %d: %w", selector, err) + } + e.Logger.Infof("Uncursed chain %d with subjects %v", selector, actuallyCursedSubjects) + } + } + + return deployerGroup.enact("proposal to uncurse RMNs: %s" + cfg.Reason) +} diff --git a/deployment/ccip/changeset/cs_rmn_curse_uncurse_test.go b/deployment/ccip/changeset/cs_rmn_curse_uncurse_test.go new file mode 100644 index 00000000000..2b0a6c07a59 --- /dev/null +++ b/deployment/ccip/changeset/cs_rmn_curse_uncurse_test.go @@ -0,0 +1,379 @@ +package changeset + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" + "github.com/smartcontractkit/chainlink/deployment/common/proposalutils" +) + +type curseAssertion struct { + chainID uint64 + subject uint64 + globalCurse bool + cursed bool +} + +type CurseTestCase struct { + name string + curseActionsBuilder func(mapIDToSelectorFunc) []CurseAction + curseAssertions []curseAssertion +} + +type mapIDToSelectorFunc func(uint64) uint64 + +var testCases = []CurseTestCase{ + { + name: "lane", + curseActionsBuilder: func(mapIDToSelector mapIDToSelectorFunc) []CurseAction { + return []CurseAction{CurseLaneBidirectionally(mapIDToSelector(0), mapIDToSelector(1))} + }, + curseAssertions: []curseAssertion{ + {chainID: 0, subject: 1, cursed: true}, + {chainID: 0, subject: 2, cursed: false}, + {chainID: 1, subject: 0, cursed: true}, + {chainID: 1, subject: 2, cursed: false}, + {chainID: 2, subject: 0, cursed: false}, + {chainID: 2, subject: 1, cursed: false}, + }, + }, + { + name: "lane duplicate", + curseActionsBuilder: func(mapIDToSelector mapIDToSelectorFunc) []CurseAction { + return []CurseAction{CurseLaneBidirectionally(mapIDToSelector(0), mapIDToSelector(1)), CurseLaneBidirectionally(mapIDToSelector(0), mapIDToSelector(1))} + }, + curseAssertions: []curseAssertion{ + {chainID: 0, subject: 1, cursed: true}, + {chainID: 0, subject: 2, cursed: false}, + {chainID: 1, subject: 0, cursed: true}, + {chainID: 1, subject: 2, cursed: false}, + {chainID: 2, subject: 0, cursed: false}, + {chainID: 2, subject: 1, cursed: false}, + }, + }, + { + name: "chain", + curseActionsBuilder: func(mapIDToSelector mapIDToSelectorFunc) []CurseAction { + return []CurseAction{CurseChain(mapIDToSelector(0))} + }, + curseAssertions: []curseAssertion{ + {chainID: 0, globalCurse: true, cursed: true}, + {chainID: 1, subject: 0, cursed: true}, + {chainID: 1, subject: 2, cursed: false}, + {chainID: 2, subject: 0, cursed: true}, + {chainID: 2, subject: 1, cursed: false}, + }, + }, + { + name: "chain duplicate", + curseActionsBuilder: func(mapIDToSelector mapIDToSelectorFunc) []CurseAction { + return []CurseAction{CurseChain(mapIDToSelector(0)), CurseChain(mapIDToSelector(0))} + }, + curseAssertions: []curseAssertion{ + {chainID: 0, globalCurse: true, cursed: true}, + {chainID: 1, subject: 0, cursed: true}, + {chainID: 1, subject: 2, cursed: false}, + {chainID: 2, subject: 0, cursed: true}, + {chainID: 2, subject: 1, cursed: false}, + }, + }, + { + name: "chain and lanes", + curseActionsBuilder: func(mapIDToSelector mapIDToSelectorFunc) []CurseAction { + return []CurseAction{CurseChain(mapIDToSelector(0)), CurseLaneBidirectionally(mapIDToSelector(1), mapIDToSelector(2))} + }, + curseAssertions: []curseAssertion{ + {chainID: 0, globalCurse: true, cursed: true}, + {chainID: 1, subject: 0, cursed: true}, + {chainID: 1, subject: 2, cursed: true}, + {chainID: 2, subject: 0, cursed: true}, + {chainID: 2, subject: 1, cursed: true}, + }, + }, +} + +func TestRMNCurse(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name+"_NO_MCMS", func(t *testing.T) { + runRmnCurseTest(t, tc) + }) + t.Run(tc.name+"_MCMS", func(t *testing.T) { + runRmnCurseMCMSTest(t, tc) + }) + } +} + +func TestRMNCurseIdempotent(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name+"_CURSE_IDEMPOTENT_NO_MCMS", func(t *testing.T) { + runRmnCurseIdempotentTest(t, tc) + }) + } +} + +func TestRMNUncurseIdempotent(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name+"_UNCURESE_IDEMPOTENT_NO_MCMS", func(t *testing.T) { + runRmnUncurseIdempotentTest(t, tc) + }) + } +} + +func TestRMNUncurse(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name+"_UNCURSE", func(t *testing.T) { + runRmnUncurseTest(t, tc) + }) + t.Run(tc.name+"_UNCURSE_MCMS", func(t *testing.T) { + runRmnUncurseMCMSTest(t, tc) + }) + } +} + +func TestRMNCurseConfigValidate(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name+"_VALIDATE", func(t *testing.T) { + runRmnCurseConfigValidateTest(t, tc) + }) + } +} + +func runRmnUncurseTest(t *testing.T, tc CurseTestCase) { + e, _ := NewMemoryEnvironment(t, WithChains(3)) + + mapIDToSelector := func(id uint64) uint64 { + return e.Env.AllChainSelectors()[id] + } + + verifyNoActiveCurseOnAllChains(t, &e) + + config := RMNCurseConfig{ + CurseActions: tc.curseActionsBuilder(mapIDToSelector), + Reason: "test curse", + } + + _, err := RMNCurseChangeset(e.Env, config) + require.NoError(t, err) + + verifyTestCaseAssertions(t, &e, tc, mapIDToSelector) + + _, err = RMNUncurseChangeset(e.Env, config) + require.NoError(t, err) + + verifyNoActiveCurseOnAllChains(t, &e) +} + +func transferRMNContractToMCMS(t *testing.T, e *DeployedEnv, state CCIPOnChainState, timelocksPerChain map[uint64]*proposalutils.TimelockExecutionContracts) { + contractsByChain := make(map[uint64][]common.Address) + rmnRemoteAddressesByChain := buildRMNRemoteAddressPerChain(e.Env, state) + for chainSelector, rmnRemoteAddress := range rmnRemoteAddressesByChain { + contractsByChain[chainSelector] = []common.Address{rmnRemoteAddress} + } + + contractsByChain[e.HomeChainSel] = append(contractsByChain[e.HomeChainSel], state.Chains[e.HomeChainSel].RMNHome.Address()) + + // This is required because RMN Contracts is initially owned by the deployer + _, err := commonchangeset.ApplyChangesets(t, e.Env, timelocksPerChain, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(commonchangeset.TransferToMCMSWithTimelock), + Config: commonchangeset.TransferToMCMSWithTimelockConfig{ + ContractsByChain: contractsByChain, + MinDelay: 0, + }, + }, + }) + require.NoError(t, err) +} + +func runRmnUncurseMCMSTest(t *testing.T, tc CurseTestCase) { + e, _ := NewMemoryEnvironment(t, WithChains(3)) + + mapIDToSelector := func(id uint64) uint64 { + return e.Env.AllChainSelectors()[id] + } + + config := RMNCurseConfig{ + CurseActions: tc.curseActionsBuilder(mapIDToSelector), + Reason: "test curse", + MCMS: &MCMSConfig{MinDelay: 0}, + } + + state, err := LoadOnchainState(e.Env) + require.NoError(t, err) + + verifyNoActiveCurseOnAllChains(t, &e) + + timelocksPerChain := buildTimelockPerChain(e.Env, state) + + transferRMNContractToMCMS(t, &e, state, timelocksPerChain) + + _, err = commonchangeset.ApplyChangesets(t, e.Env, timelocksPerChain, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(RMNCurseChangeset), + Config: config, + }, + }) + require.NoError(t, err) + + verifyTestCaseAssertions(t, &e, tc, mapIDToSelector) + + _, err = commonchangeset.ApplyChangesets(t, e.Env, timelocksPerChain, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(RMNUncurseChangeset), + Config: config, + }, + }) + require.NoError(t, err) + + verifyNoActiveCurseOnAllChains(t, &e) +} + +func runRmnCurseConfigValidateTest(t *testing.T, tc CurseTestCase) { + e, _ := NewMemoryEnvironment(t, WithChains(3)) + + mapIDToSelector := func(id uint64) uint64 { + return e.Env.AllChainSelectors()[id] + } + + config := RMNCurseConfig{ + CurseActions: tc.curseActionsBuilder(mapIDToSelector), + Reason: "test curse", + } + + err := config.Validate(e.Env) + require.NoError(t, err) +} + +func runRmnCurseTest(t *testing.T, tc CurseTestCase) { + e, _ := NewMemoryEnvironment(t, WithChains(3)) + + mapIDToSelector := func(id uint64) uint64 { + return e.Env.AllChainSelectors()[id] + } + + verifyNoActiveCurseOnAllChains(t, &e) + + config := RMNCurseConfig{ + CurseActions: tc.curseActionsBuilder(mapIDToSelector), + Reason: "test curse", + } + + _, err := RMNCurseChangeset(e.Env, config) + require.NoError(t, err) + + verifyTestCaseAssertions(t, &e, tc, mapIDToSelector) +} + +func runRmnCurseIdempotentTest(t *testing.T, tc CurseTestCase) { + e, _ := NewMemoryEnvironment(t, WithChains(3)) + + mapIDToSelector := func(id uint64) uint64 { + return e.Env.AllChainSelectors()[id] + } + + verifyNoActiveCurseOnAllChains(t, &e) + + config := RMNCurseConfig{ + CurseActions: tc.curseActionsBuilder(mapIDToSelector), + Reason: "test curse", + } + + _, err := RMNCurseChangeset(e.Env, config) + require.NoError(t, err) + + _, err = RMNCurseChangeset(e.Env, config) + require.NoError(t, err) + + verifyTestCaseAssertions(t, &e, tc, mapIDToSelector) +} + +func runRmnUncurseIdempotentTest(t *testing.T, tc CurseTestCase) { + e, _ := NewMemoryEnvironment(t, WithChains(3)) + + mapIDToSelector := func(id uint64) uint64 { + return e.Env.AllChainSelectors()[id] + } + + verifyNoActiveCurseOnAllChains(t, &e) + + config := RMNCurseConfig{ + CurseActions: tc.curseActionsBuilder(mapIDToSelector), + Reason: "test curse", + } + + _, err := RMNCurseChangeset(e.Env, config) + require.NoError(t, err) + + verifyTestCaseAssertions(t, &e, tc, mapIDToSelector) + + _, err = RMNUncurseChangeset(e.Env, config) + require.NoError(t, err) + + _, err = RMNUncurseChangeset(e.Env, config) + require.NoError(t, err) + + verifyNoActiveCurseOnAllChains(t, &e) +} + +func runRmnCurseMCMSTest(t *testing.T, tc CurseTestCase) { + e, _ := NewMemoryEnvironment(t, WithChains(3)) + + mapIDToSelector := func(id uint64) uint64 { + return e.Env.AllChainSelectors()[id] + } + + config := RMNCurseConfig{ + CurseActions: tc.curseActionsBuilder(mapIDToSelector), + Reason: "test curse", + MCMS: &MCMSConfig{MinDelay: 0}, + } + + state, err := LoadOnchainState(e.Env) + require.NoError(t, err) + + verifyNoActiveCurseOnAllChains(t, &e) + + timelocksPerChain := buildTimelockPerChain(e.Env, state) + + transferRMNContractToMCMS(t, &e, state, timelocksPerChain) + + _, err = commonchangeset.ApplyChangesets(t, e.Env, timelocksPerChain, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(RMNCurseChangeset), + Config: config, + }, + }) + require.NoError(t, err) + + verifyTestCaseAssertions(t, &e, tc, mapIDToSelector) +} + +func verifyTestCaseAssertions(t *testing.T, e *DeployedEnv, tc CurseTestCase, mapIDToSelector mapIDToSelectorFunc) { + state, err := LoadOnchainState(e.Env) + require.NoError(t, err) + + for _, assertion := range tc.curseAssertions { + cursedSubject := SelectorToSubject(mapIDToSelector(assertion.subject)) + if assertion.globalCurse { + cursedSubject = GlobalCurseSubject() + } + + isCursed, err := state.Chains[mapIDToSelector(assertion.chainID)].RMNRemote.IsCursed(nil, cursedSubject) + require.NoError(t, err) + require.Equal(t, assertion.cursed, isCursed, "chain %d subject %d", assertion.chainID, assertion.subject) + } +} + +func verifyNoActiveCurseOnAllChains(t *testing.T, e *DeployedEnv) { + state, err := LoadOnchainState(e.Env) + require.NoError(t, err) + + for _, chain := range e.Env.Chains { + isCursed, err := state.Chains[chain.Selector].RMNRemote.IsCursed0(nil) + require.NoError(t, err) + require.False(t, isCursed, "chain %d", chain.Selector) + } +} diff --git a/deployment/ccip/changeset/cs_update_rmn_config.go b/deployment/ccip/changeset/cs_update_rmn_config.go index e52ca407b5d..1797d588fca 100644 --- a/deployment/ccip/changeset/cs_update_rmn_config.go +++ b/deployment/ccip/changeset/cs_update_rmn_config.go @@ -9,7 +9,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - "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" @@ -415,34 +414,6 @@ func PromoteCandidateConfigChangeset(e deployment.Environment, config PromoteRMN }, nil } -func buildTimelockPerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]*proposalutils.TimelockExecutionContracts { - timelocksPerChain := make(map[uint64]*proposalutils.TimelockExecutionContracts) - for _, chain := range e.Chains { - timelocksPerChain[chain.Selector] = &proposalutils.TimelockExecutionContracts{ - Timelock: state.Chains[chain.Selector].Timelock, - CallProxy: state.Chains[chain.Selector].CallProxy, - } - } - return timelocksPerChain -} - -func buildTimelockAddressPerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]common.Address { - timelocksPerChain := buildTimelockPerChain(e, state) - timelockAddressPerChain := make(map[uint64]common.Address) - for chain, timelock := range timelocksPerChain { - timelockAddressPerChain[chain] = timelock.Timelock.Address() - } - return timelockAddressPerChain -} - -func buildProposerPerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]*gethwrappers.ManyChainMultiSig { - proposerPerChain := make(map[uint64]*gethwrappers.ManyChainMultiSig) - for _, chain := range e.Chains { - proposerPerChain[chain.Selector] = state.Chains[chain.Selector].ProposerMcm - } - return proposerPerChain -} - func buildRMNRemotePerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]*rmn_remote.RMNRemote { timelocksPerChain := make(map[uint64]*rmn_remote.RMNRemote) for _, chain := range e.Chains { diff --git a/deployment/ccip/changeset/deployer_group.go b/deployment/ccip/changeset/deployer_group.go new file mode 100644 index 00000000000..0c0ff1e5c8e --- /dev/null +++ b/deployment/ccip/changeset/deployer_group.go @@ -0,0 +1,194 @@ +package changeset + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "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" +) + +type DeployerGroup struct { + e deployment.Environment + state CCIPOnChainState + mcmConfig *MCMSConfig + transactions map[uint64][]*types.Transaction +} + +// DeployerGroup is an abstraction that lets developers write their changeset +// without needing to know if it's executed using a DeployerKey or an MCMS proposal. +// +// Example usage: +// +// deployerGroup := NewDeployerGroup(e, state, mcmConfig) +// selector := 0 +// # Get the right deployer key for the chain +// deployer := deployerGroup.getDeployer(selector) +// state.Chains[selector].RMNRemote.Curse() +// # Execute the transaction or create the proposal +// deployerGroup.enact("Curse RMNRemote") +func NewDeployerGroup(e deployment.Environment, state CCIPOnChainState, mcmConfig *MCMSConfig) *DeployerGroup { + return &DeployerGroup{ + e: e, + mcmConfig: mcmConfig, + state: state, + transactions: make(map[uint64][]*types.Transaction), + } +} + +func (d *DeployerGroup) getDeployer(chain uint64) (*bind.TransactOpts, error) { + txOpts := d.e.Chains[chain].DeployerKey + if d.mcmConfig != nil { + txOpts = deployment.SimTransactOpts() + txOpts = &bind.TransactOpts{ + From: d.state.Chains[chain].Timelock.Address(), + Signer: txOpts.Signer, + GasLimit: txOpts.GasLimit, + GasPrice: txOpts.GasPrice, + Nonce: txOpts.Nonce, + Value: txOpts.Value, + GasFeeCap: txOpts.GasFeeCap, + GasTipCap: txOpts.GasTipCap, + Context: txOpts.Context, + AccessList: txOpts.AccessList, + NoSend: txOpts.NoSend, + } + } + sim := &bind.TransactOpts{ + From: txOpts.From, + Signer: txOpts.Signer, + GasLimit: txOpts.GasLimit, + GasPrice: txOpts.GasPrice, + Nonce: txOpts.Nonce, + Value: txOpts.Value, + GasFeeCap: txOpts.GasFeeCap, + GasTipCap: txOpts.GasTipCap, + Context: txOpts.Context, + AccessList: txOpts.AccessList, + NoSend: true, + } + oldSigner := sim.Signer + + var startingNonce *big.Int + if txOpts.Nonce != nil { + startingNonce = new(big.Int).Set(txOpts.Nonce) + } else { + nonce, err := d.e.Chains[chain].Client.PendingNonceAt(context.Background(), txOpts.From) + if err != nil { + return nil, fmt.Errorf("could not get nonce for deployer: %w", err) + } + startingNonce = new(big.Int).SetUint64(nonce) + } + + sim.Signer = func(a common.Address, t *types.Transaction) (*types.Transaction, error) { + // Update the nonce to consider the transactions that have been sent + sim.Nonce = big.NewInt(0).Add(startingNonce, big.NewInt(int64(len(d.transactions[chain]))+1)) + + tx, err := oldSigner(a, t) + if err != nil { + return nil, err + } + d.transactions[chain] = append(d.transactions[chain], tx) + return tx, nil + } + return sim, nil +} + +func (d *DeployerGroup) enact(deploymentDescription string) (deployment.ChangesetOutput, error) { + if d.mcmConfig != nil { + return d.enactMcms(deploymentDescription) + } + + return d.enactDeployer() +} + +func (d *DeployerGroup) enactMcms(deploymentDescription string) (deployment.ChangesetOutput, error) { + batches := make([]timelock.BatchChainOperation, 0) + for selector, txs := range d.transactions { + mcmOps := make([]mcms.Operation, len(txs)) + for i, tx := range txs { + mcmOps[i] = mcms.Operation{ + To: *tx.To(), + Data: tx.Data(), + Value: tx.Value(), + } + } + batches = append(batches, timelock.BatchChainOperation{ + ChainIdentifier: mcms.ChainIdentifier(selector), + Batch: mcmOps, + }) + } + + timelocksPerChain := buildTimelockAddressPerChain(d.e, d.state) + + proposerMCMSes := buildProposerPerChain(d.e, d.state) + + prop, err := proposalutils.BuildProposalFromBatches( + timelocksPerChain, + proposerMCMSes, + batches, + deploymentDescription, + d.mcmConfig.MinDelay, + ) + + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to build proposal %w", err) + } + + return deployment.ChangesetOutput{ + Proposals: []timelock.MCMSWithTimelockProposal{*prop}, + }, nil +} + +func (d *DeployerGroup) enactDeployer() (deployment.ChangesetOutput, error) { + for selector, txs := range d.transactions { + for _, tx := range txs { + err := d.e.Chains[selector].Client.SendTransaction(context.Background(), tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("failed to send transaction: %w", err) + } + + _, err = d.e.Chains[selector].Confirm(tx) + if err != nil { + return deployment.ChangesetOutput{}, fmt.Errorf("waiting for tx to be mined failed: %w", err) + } + } + } + return deployment.ChangesetOutput{}, nil +} + +func buildTimelockPerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]*proposalutils.TimelockExecutionContracts { + timelocksPerChain := make(map[uint64]*proposalutils.TimelockExecutionContracts) + for _, chain := range e.Chains { + timelocksPerChain[chain.Selector] = &proposalutils.TimelockExecutionContracts{ + Timelock: state.Chains[chain.Selector].Timelock, + CallProxy: state.Chains[chain.Selector].CallProxy, + } + } + return timelocksPerChain +} + +func buildTimelockAddressPerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]common.Address { + timelocksPerChain := buildTimelockPerChain(e, state) + timelockAddressPerChain := make(map[uint64]common.Address) + for chain, timelock := range timelocksPerChain { + timelockAddressPerChain[chain] = timelock.Timelock.Address() + } + return timelockAddressPerChain +} + +func buildProposerPerChain(e deployment.Environment, state CCIPOnChainState) map[uint64]*gethwrappers.ManyChainMultiSig { + proposerPerChain := make(map[uint64]*gethwrappers.ManyChainMultiSig) + for _, chain := range e.Chains { + proposerPerChain[chain.Selector] = state.Chains[chain.Selector].ProposerMcm + } + return proposerPerChain +} diff --git a/deployment/ccip/changeset/deployer_group_test.go b/deployment/ccip/changeset/deployer_group_test.go new file mode 100644 index 00000000000..12dcaa9076b --- /dev/null +++ b/deployment/ccip/changeset/deployer_group_test.go @@ -0,0 +1,191 @@ +package changeset + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/deployment" + commonchangeset "github.com/smartcontractkit/chainlink/deployment/common/changeset" +) + +type dummyDeployerGroupChangesetConfig struct { + selector uint64 + address common.Address + mints []*big.Int + MCMS *MCMSConfig +} + +func dummyDeployerGroupGrantMintChangeset(e deployment.Environment, cfg dummyDeployerGroupChangesetConfig) (deployment.ChangesetOutput, error) { + state, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + token := state.Chains[cfg.selector].LinkToken + + group := NewDeployerGroup(e, state, cfg.MCMS) + deployer, err := group.getDeployer(cfg.selector) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + _, err = token.GrantMintRole(deployer, deployer.From) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + return group.enact("Grant mint role") +} + +func dummyDeployerGroupMintChangeset(e deployment.Environment, cfg dummyDeployerGroupChangesetConfig) (deployment.ChangesetOutput, error) { + state, err := LoadOnchainState(e) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + token := state.Chains[cfg.selector].LinkToken + + group := NewDeployerGroup(e, state, cfg.MCMS) + deployer, err := group.getDeployer(cfg.selector) + if err != nil { + return deployment.ChangesetOutput{}, err + } + + for _, mint := range cfg.mints { + _, err = token.Mint(deployer, cfg.address, mint) + if err != nil { + return deployment.ChangesetOutput{}, err + } + } + + return group.enact("Mint tokens") +} + +type deployerGroupTestCase struct { + name string + cfg dummyDeployerGroupChangesetConfig + expectError bool +} + +var deployerGroupTestCases = []deployerGroupTestCase{ + { + name: "happy path", + cfg: dummyDeployerGroupChangesetConfig{ + mints: []*big.Int{big.NewInt(1), big.NewInt(2)}, + address: common.HexToAddress("0x455E5AA18469bC6ccEF49594645666C587A3a71B"), + }, + }, + { + name: "error", + cfg: dummyDeployerGroupChangesetConfig{ + mints: []*big.Int{big.NewInt(-1)}, + address: common.HexToAddress("0x455E5AA18469bC6ccEF49594645666C587A3a71B"), + }, + expectError: true, + }, +} + +func TestDeployerGroup(t *testing.T) { + for _, tc := range deployerGroupTestCases { + t.Run(tc.name, func(t *testing.T) { + e, _ := NewMemoryEnvironment(t, WithChains(2)) + + tc.cfg.selector = e.HomeChainSel + tc.cfg.MCMS = nil + + _, err := dummyDeployerGroupGrantMintChangeset(e.Env, tc.cfg) + require.NoError(t, err) + + _, err = dummyDeployerGroupMintChangeset(e.Env, tc.cfg) + if tc.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + + state, err := LoadOnchainState(e.Env) + require.NoError(t, err) + + token := state.Chains[e.HomeChainSel].LinkToken + + amount, err := token.BalanceOf(nil, tc.cfg.address) + require.NoError(t, err) + + sumOfMints := big.NewInt(0) + for _, mint := range tc.cfg.mints { + sumOfMints = sumOfMints.Add(sumOfMints, mint) + } + + require.Equal(t, sumOfMints, amount) + } + }) + } +} + +func TestDeployerGroupMCMS(t *testing.T) { + for _, tc := range deployerGroupTestCases { + t.Run(tc.name, func(t *testing.T) { + if tc.expectError { + t.Skip("skipping test because it's not possible to verify error when using MCMS since we are explicitly failing the test in ApplyChangesets") + } + + e, _ := NewMemoryEnvironment(t, WithChains(2)) + + tc.cfg.selector = e.HomeChainSel + tc.cfg.MCMS = &MCMSConfig{ + MinDelay: 0, + } + state, err := LoadOnchainState(e.Env) + require.NoError(t, err) + + timelocksPerChain := buildTimelockPerChain(e.Env, state) + + contractsByChain := make(map[uint64][]common.Address) + contractsByChain[e.HomeChainSel] = []common.Address{state.Chains[e.HomeChainSel].LinkToken.Address()} + + _, err = commonchangeset.ApplyChangesets(t, e.Env, timelocksPerChain, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(commonchangeset.TransferToMCMSWithTimelock), + Config: commonchangeset.TransferToMCMSWithTimelockConfig{ + ContractsByChain: contractsByChain, + MinDelay: 0, + }, + }, + }) + require.NoError(t, err) + + _, err = commonchangeset.ApplyChangesets(t, e.Env, timelocksPerChain, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(dummyDeployerGroupGrantMintChangeset), + Config: tc.cfg, + }, + }) + require.NoError(t, err) + + _, err = commonchangeset.ApplyChangesets(t, e.Env, timelocksPerChain, []commonchangeset.ChangesetApplication{ + { + Changeset: commonchangeset.WrapChangeSet(dummyDeployerGroupMintChangeset), + Config: tc.cfg, + }, + }) + require.NoError(t, err) + + state, err = LoadOnchainState(e.Env) + require.NoError(t, err) + + token := state.Chains[e.HomeChainSel].LinkToken + + amount, err := token.BalanceOf(nil, tc.cfg.address) + require.NoError(t, err) + + sumOfMints := big.NewInt(0) + for _, mint := range tc.cfg.mints { + sumOfMints = sumOfMints.Add(sumOfMints, mint) + } + + require.Equal(t, sumOfMints, amount) + }) + } +} diff --git a/integration-tests/smoke/ccip/ccip_rmn_test.go b/integration-tests/smoke/ccip/ccip_rmn_test.go index c655caa9f08..fcf17956dc0 100644 --- a/integration-tests/smoke/ccip/ccip_rmn_test.go +++ b/integration-tests/smoke/ccip/ccip_rmn_test.go @@ -2,7 +2,6 @@ package smoke import ( "context" - "encoding/binary" "errors" "math/big" "os" @@ -17,17 +16,15 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/osutil" "github.com/smartcontractkit/chainlink-testing-framework/lib/utils/testcontext" - "github.com/smartcontractkit/chainlink-ccip/pkg/reader" - "github.com/smartcontractkit/chainlink/deployment/ccip/changeset" "github.com/smartcontractkit/chainlink/deployment/environment/devenv" - "github.com/smartcontractkit/chainlink/deployment" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/rmn_home" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/rmn_remote" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" @@ -329,8 +326,9 @@ func runRmnTestCase(t *testing.T, tc rmnTestCase) { startBlocks, seqNumCommit, seqNumExec := tc.sendMessages(t, onChainState, envWithRMN) t.Logf("Sent all messages, seqNumCommit: %v seqNumExec: %v", seqNumCommit, seqNumExec) + eg := errgroup.Group{} tc.callContractsToCurseChains(ctx, t, onChainState, envWithRMN) - tc.callContractsToCurseAndRevokeCurse(ctx, t, onChainState, envWithRMN) + tc.callContractsToCurseAndRevokeCurse(ctx, &eg, t, onChainState, envWithRMN) tc.enableOracles(ctx, t, envWithRMN, disabledNodes) @@ -390,6 +388,8 @@ func runRmnTestCase(t *testing.T, tc rmnTestCase) { <-commitReportReceived // wait for commit reports t.Logf("✅ Commit report") + require.NoError(t, eg.Wait()) + if tc.waitForExec { t.Logf("⌛ Waiting for exec reports...") changeset.ConfirmExecWithSeqNrsForAll(t, envWithRMN.Env, onChainState, seqNumExec, startBlocks) @@ -614,7 +614,7 @@ func (tc rmnTestCase) callContractsToCurseChains(ctx context.Context, t *testing remoteSel := tc.pf.chainSelectors[remoteCfg.chainIdx] chState, ok := onChainState.Chains[remoteSel] require.True(t, ok) - chain, ok := envWithRMN.Env.Chains[remoteSel] + _, ok = envWithRMN.Env.Chains[remoteSel] require.True(t, ok) cursedSubjects, ok := tc.cursedSubjectsPerChain[remoteCfg.chainIdx] @@ -623,14 +623,19 @@ func (tc rmnTestCase) callContractsToCurseChains(ctx context.Context, t *testing } for _, subjectDescription := range cursedSubjects { - subj := reader.GlobalCurseSubject - if subjectDescription != globalCurse { - subj = chainSelectorToBytes16(tc.pf.chainSelectors[subjectDescription]) + curseActions := make([]changeset.CurseAction, 0) + + if subjectDescription == globalCurse { + curseActions = append(curseActions, changeset.CurseGloballyOnlyOnChain(remoteSel)) + } else { + curseActions = append(curseActions, changeset.CurseLaneOnlyOnSource(remoteSel, tc.pf.chainSelectors[subjectDescription])) } - t.Logf("cursing subject %d (%d)", subj, subjectDescription) - txCurse, errCurse := chState.RMNRemote.Curse(chain.DeployerKey, subj) - _, errConfirm := deployment.ConfirmIfNoError(chain, txCurse, errCurse) - require.NoError(t, errConfirm) + + _, err := changeset.RMNCurseChangeset(envWithRMN.Env, changeset.RMNCurseConfig{ + CurseActions: curseActions, + Reason: "test curse", + }) + require.NoError(t, err) } cs, err := chState.RMNRemote.GetCursedSubjects(&bind.CallOpts{Context: ctx}) @@ -639,41 +644,59 @@ func (tc rmnTestCase) callContractsToCurseChains(ctx context.Context, t *testing } } -func (tc rmnTestCase) callContractsToCurseAndRevokeCurse(ctx context.Context, t *testing.T, onChainState changeset.CCIPOnChainState, envWithRMN changeset.DeployedEnv) { +func (tc rmnTestCase) callContractsToCurseAndRevokeCurse(ctx context.Context, eg *errgroup.Group, t *testing.T, onChainState changeset.CCIPOnChainState, envWithRMN changeset.DeployedEnv) { for _, remoteCfg := range tc.remoteChainsConfig { remoteSel := tc.pf.chainSelectors[remoteCfg.chainIdx] chState, ok := onChainState.Chains[remoteSel] require.True(t, ok) - chain, ok := envWithRMN.Env.Chains[remoteSel] + _, ok = envWithRMN.Env.Chains[remoteSel] require.True(t, ok) - cursedSubjects, ok := tc.revokedCursedSubjectsPerChain[remoteCfg.chainIdx] - if !ok { - continue // nothing to curse on this chain - } + cursedSubjects := tc.revokedCursedSubjectsPerChain[remoteCfg.chainIdx] for subjectDescription, revokeAfter := range cursedSubjects { - subj := reader.GlobalCurseSubject - if subjectDescription != globalCurse { - subj = chainSelectorToBytes16(tc.pf.chainSelectors[subjectDescription]) + curseActions := make([]changeset.CurseAction, 0) + + if subjectDescription == globalCurse { + curseActions = append(curseActions, changeset.CurseGloballyOnlyOnChain(remoteSel)) + } else { + curseActions = append(curseActions, changeset.CurseLaneOnlyOnSource(remoteSel, tc.pf.chainSelectors[subjectDescription])) } - t.Logf("cursing subject %d (%d)", subj, subjectDescription) - txCurse, errCurse := chState.RMNRemote.Curse(chain.DeployerKey, subj) - _, errConfirm := deployment.ConfirmIfNoError(chain, txCurse, errCurse) - require.NoError(t, errConfirm) - go func() { + _, err := changeset.RMNCurseChangeset(envWithRMN.Env, changeset.RMNCurseConfig{ + CurseActions: curseActions, + Reason: "test curse", + }) + require.NoError(t, err) + + eg.Go(func() error { <-time.NewTimer(revokeAfter).C - t.Logf("revoking curse on subject %d (%d)", subj, subjectDescription) - txUncurse, errUncurse := chState.RMNRemote.Uncurse(chain.DeployerKey, subj) - _, errConfirm = deployment.ConfirmIfNoError(chain, txUncurse, errUncurse) - require.NoError(t, errConfirm) - }() + t.Logf("revoking curse on subject %d (%d)", subjectDescription, subjectDescription) + + _, err := changeset.RMNUncurseChangeset(envWithRMN.Env, changeset.RMNCurseConfig{ + CurseActions: curseActions, + Reason: "test uncurse", + }) + if err != nil { + return err + } + return nil + }) } - cs, err := chState.RMNRemote.GetCursedSubjects(&bind.CallOpts{Context: ctx}) require.NoError(t, err) - t.Logf("Cursed subjects: %v", cs) + t.Logf("Cursed subjects: %v, %v", cs, remoteSel) + eg.Go(func() error { + <-time.NewTimer(time.Second * 10).C + cs, err := chState.RMNRemote.GetCursedSubjects(&bind.CallOpts{Context: ctx}) + + if err != nil { + return err + } + + t.Logf("Cursed subjects after revoking: %v, %v", cs, remoteSel) + return nil + }) } } @@ -684,10 +707,3 @@ func (tc rmnTestCase) enableOracles(ctx context.Context, t *testing.T, envWithRM t.Logf("node %s enabled", n) } } - -func chainSelectorToBytes16(chainSel uint64) [16]byte { - var result [16]byte - // Convert the uint64 to bytes and place it in the last 8 bytes of the array - binary.BigEndian.PutUint64(result[8:], chainSel) - return result -}