-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
330 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
package changeset | ||
|
||
import ( | ||
"encoding/binary" | ||
"fmt" | ||
|
||
"github.com/smartcontractkit/chainlink/deployment" | ||
) | ||
|
||
const ( | ||
GLOBAL_CURSE_SUBJECT = 0 | ||
) | ||
|
||
type RMNCurseAction struct { | ||
ChainSelector uint64 | ||
SubjectToCurse uint64 | ||
} | ||
|
||
type CurseAction func(e deployment.Environment) []RMNCurseAction | ||
|
||
type RMNCurseConfig struct { | ||
HomeChainSelector uint64 | ||
MCMS *MCMSConfig | ||
CurseActions []CurseAction | ||
CurseReason string | ||
} | ||
|
||
func subjectToByte16(subject uint64) [16]byte { | ||
var b [16]byte | ||
binary.LittleEndian.PutUint64(b[:8], subject) | ||
return b | ||
} | ||
|
||
func CurseLane(sourceSelector uint64, destinationSelector uint64) CurseAction { | ||
// Bidirectional curse between two chains | ||
return func(e deployment.Environment) []RMNCurseAction { | ||
return []RMNCurseAction{ | ||
{ | ||
ChainSelector: sourceSelector, | ||
SubjectToCurse: destinationSelector, | ||
}, | ||
{ | ||
ChainSelector: destinationSelector, | ||
SubjectToCurse: sourceSelector, | ||
}, | ||
} | ||
} | ||
} | ||
|
||
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: 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, RMNCurseAction{ | ||
ChainSelector: chainSelector, | ||
SubjectToCurse: GLOBAL_CURSE_SUBJECT, | ||
}) | ||
|
||
return curseActions | ||
} | ||
} | ||
|
||
func groupRMNSubjectBySelector(rmnSubjects []RMNCurseAction) map[uint64][]uint64 { | ||
grouped := make(map[uint64][]uint64) | ||
for _, subject := range rmnSubjects { | ||
grouped[subject.ChainSelector] = append(grouped[subject.ChainSelector], subject.SubjectToCurse) | ||
} | ||
|
||
// Only keep unique subjects, preserve only global curse if present and eliminate any curse where the selector is the same as the subject | ||
for chainSelector, subjects := range grouped { | ||
uniqueSubjects := make(map[uint64]struct{}) | ||
for _, subject := range subjects { | ||
if subject == chainSelector { | ||
continue | ||
} | ||
uniqueSubjects[subject] = struct{}{} | ||
} | ||
|
||
if _, ok := uniqueSubjects[GLOBAL_CURSE_SUBJECT]; ok { | ||
grouped[chainSelector] = []uint64{GLOBAL_CURSE_SUBJECT} | ||
} else { | ||
var uniqueSubjectsSlice []uint64 | ||
for subject := range uniqueSubjects { | ||
uniqueSubjectsSlice = append(uniqueSubjectsSlice, subject) | ||
} | ||
grouped[chainSelector] = uniqueSubjectsSlice | ||
} | ||
} | ||
|
||
return grouped | ||
} | ||
|
||
func NewRMNCurseChangeset(e deployment.Environment, cfg RMNCurseConfig) (deployment.ChangesetOutput, error) { | ||
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) | ||
|
||
// For each chain in the environement get the RMNRemote contract and call curse | ||
for selector, chain := range state.Chains { | ||
deployer := deployerGroup.getDeployer(selector) | ||
if curseSubjects, ok := grouped[selector]; ok { | ||
subjectsByte16 := make([][16]byte, len(curseSubjects)) | ||
for i, subject := range curseSubjects { | ||
subjectsByte16[i] = subjectToByte16(subject) | ||
} | ||
|
||
_, err := chain.RMNRemote.Curse0(deployer, subjectsByte16) | ||
if err != nil { | ||
return deployment.ChangesetOutput{}, fmt.Errorf("failed to curse chain %d: %w", selector, err) | ||
} | ||
} | ||
} | ||
|
||
return deployerGroup.enact(fmt.Sprintf("proposal to curse RMNs: %s", cfg.CurseReason)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package changeset | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
type curseAssertion struct { | ||
chainSelector uint64 | ||
subject uint64 | ||
} | ||
|
||
type CurseTestCase struct { | ||
useMCMS bool | ||
name string | ||
curseActionsBuilder []CurseAction | ||
curseAssertions []curseAssertion | ||
} | ||
|
||
func TestRMNCurse(t *testing.T) { | ||
t.Parallel() | ||
testCases := []CurseTestCase{ | ||
// { | ||
// useMCMS: true, | ||
// name: "with MCMS", | ||
// }, | ||
{ | ||
useMCMS: false, | ||
name: "without MCMS", | ||
curseActionsBuilder: []CurseAction{CurseLane(0, 1)}, | ||
curseAssertions: []curseAssertion{ | ||
{chainSelector: 0, subject: 1}, | ||
{chainSelector: 1, subject: 0}, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
t.Parallel() | ||
testRmnCurse(t, tc) | ||
}) | ||
} | ||
} | ||
|
||
func testRmnCurse(t *testing.T, tc CurseTestCase) { | ||
e := NewMemoryEnvironment(t, WithChains(2)) | ||
|
||
config := RMNCurseConfig{ | ||
HomeChainSelector: e.HomeChainSel, | ||
CurseActions: tc.curseActionsBuilder, | ||
CurseReason: "test curse", | ||
} | ||
|
||
if tc.useMCMS { | ||
config.MCMS = &MCMSConfig{ | ||
MinDelay: 0, | ||
} | ||
} | ||
|
||
NewRMNCurseChangeset(e.Env, config) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
package changeset | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"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 | ||
} | ||
|
||
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 { | ||
sim := deployment.SimTransactOpts() | ||
oldSigner := sim.Signer | ||
sim.Signer = func(a common.Address, t *types.Transaction) (*types.Transaction, error) { | ||
tx, err := oldSigner(a, t) | ||
if err != nil { | ||
return nil, err | ||
} | ||
d.transactions[chain] = append(d.transactions[chain], tx) | ||
return tx, nil | ||
} | ||
return sim | ||
} | ||
|
||
func (d *DeployerGroup) enact(deploymentDescription string) (deployment.ChangesetOutput, error) { | ||
if d.mcmConfig != nil { | ||
return d.enactMcms(deploymentDescription) | ||
} else { | ||
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) | ||
} | ||
} | ||
} | ||
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 | ||
} |