Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New rmn curse changeset #15868

Merged
merged 28 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d76dcea
Initial commit
carte7000 Dec 20, 2024
305e712
Add unit tests
carte7000 Jan 8, 2025
b217f52
Cleanup
carte7000 Jan 8, 2025
79a3324
Add uncurse changeset
carte7000 Jan 9, 2025
fe5bf11
Address linting issues
carte7000 Jan 9, 2025
df42f76
Fix more linting issues
carte7000 Jan 9, 2025
3863f82
Use changeset in e2e test
carte7000 Jan 9, 2025
abeba1a
Address PR feedback
carte7000 Jan 10, 2025
f3cf0b3
Fix linting issue and bug with subject
carte7000 Jan 10, 2025
0a953e5
Remove nolint
carte7000 Jan 10, 2025
ebb18e0
Use error group
carte7000 Jan 10, 2025
acd4df8
Use global curse only instead of CurseChain since it curse all connec…
carte7000 Jan 13, 2025
b9b5a88
Address PR comments
carte7000 Jan 13, 2025
adacbde
Fix linting issue
carte7000 Jan 13, 2025
c863d70
Merge branch 'develop' into new-rmn-curse-changeset
carte7000 Jan 13, 2025
ac43c5a
Merge develop
carte7000 Jan 13, 2025
37abca0
Use fmnt.Errorf
carte7000 Jan 13, 2025
ecadeb1
Fix linting issue
carte7000 Jan 13, 2025
d2e8683
Fix linting issue
carte7000 Jan 13, 2025
f80a6e5
Additional comments and renaming
carte7000 Jan 14, 2025
e5436d6
Enhance idempotency checks in RMN curse and uncurse operations
carte7000 Jan 14, 2025
2597661
Merge branch 'develop' into new-rmn-curse-changeset
carte7000 Jan 14, 2025
23b24e2
Fix build error
carte7000 Jan 14, 2025
0f808af
Address PR comments
carte7000 Jan 14, 2025
95e4256
Fix build error
carte7000 Jan 14, 2025
235ecaf
Fix test name
carte7000 Jan 14, 2025
19e3410
Add more test for deployer group
carte7000 Jan 15, 2025
0cd231c
Fix linting issue
carte7000 Jan 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 334 additions & 0 deletions deployment/ccip/changeset/cs_rmn_curse_uncurse.go
Original file line number Diff line number Diff line change
@@ -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 {
carte7000 marked this conversation as resolved.
Show resolved Hide resolved
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
}
carte7000 marked this conversation as resolved.
Show resolved Hide resolved

// 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)
AnieeG marked this conversation as resolved.
Show resolved Hide resolved

if err != nil {
return fmt.Errorf("failed to load onchain state: %w", err)
}

if len(c.CurseActions) == 0 {
carte7000 marked this conversation as resolved.
Show resolved Hide resolved
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 {
carte7000 marked this conversation as resolved.
Show resolved Hide resolved
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a simpler approach to me:

func groupRMNSubjectBySelector(rmnSubjects []RMNCurseAction, keepOnlyGlobal bool) (result map[uint64][]Subject) {
	grouped := make(map[uint64]map[Subject]struct{})

	// Group subjects by chain selector and ensure uniqueness
	for _, subject := range rmnSubjects {
		if grouped[subject.ChainSelector] == nil {
            grouped[subject.ChainSelector] = make(map[Subject]struct{})
        }
        if keepOnlyGlobal && subject.SubjectToCurse == SelectorToSubject(subject.ChainSelector) {
            continue
        }
        grouped[subject.ChainSelector][subject.SubjectToCurse] = struct{}{}
	}

	result = make(map[uint64][]Subject)
	for selector, subjects := range grouped {
		// see https://pkg.go.dev/golang.org/x/exp/maps#Values
		result[selector] = maps.Values(subjects)
	}

	return result
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be missing some parts, ie if the GlobalSubject is part of the subject to curse on a chain we would want to only keep this one when keepOnlyGlobal is active. I updated the original to be clearer let me know if it looks good to you

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),
// },
carte7000 marked this conversation as resolved.
Show resolved Hide resolved
// 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)

carte7000 marked this conversation as resolved.
Show resolved Hide resolved
// 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
carte7000 marked this conversation as resolved.
Show resolved Hide resolved
}

_, 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)
}
Loading
Loading