diff --git a/.changeset/gentle-waves-appear.md b/.changeset/gentle-waves-appear.md new file mode 100644 index 000000000..c7d736010 --- /dev/null +++ b/.changeset/gentle-waves-appear.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": minor +--- + +feat(mcms): check MCM state before calling SetRoot or Execute diff --git a/engine/cld/legacy/cli/mcmsv2/mcms_v2.go b/engine/cld/legacy/cli/mcmsv2/mcms_v2.go index b83f36a5a..06856b896 100644 --- a/engine/cld/legacy/cli/mcmsv2/mcms_v2.go +++ b/engine/cld/legacy/cli/mcmsv2/mcms_v2.go @@ -200,6 +200,11 @@ func buildExecuteOperationv2Cmd(lggr logger.Logger, domain cldf_domain.Domain, p if err != nil { return fmt.Errorf("error converting proposal to executable: %w", err) } + inspector, err := getInspectorFromChainSelector(*cfg) + if err != nil { + return fmt.Errorf("failed to get inspector: %w", err) + } + if cfg.fork { lggr.Info("Fork mode is on, all transactions will be executed on a forked chain") } @@ -213,6 +218,23 @@ func buildExecuteOperationv2Cmd(lggr logger.Logger, domain cldf_domain.Domain, p return fmt.Errorf("operation %d is not for chain %d", index, cfg.chainSelector) } + opCount, err := inspector.GetOpCount(cmd.Context(), cfg.proposal.ChainMetadata[types.ChainSelector(cfg.chainSelector)].MCMAddress) + if err != nil { + return fmt.Errorf("failed to get opcount for chain %d: %w", cfg.chainSelector, err) + } + txNonce, err := executable.TxNonce(index) + if err != nil { + return fmt.Errorf("failed to get TxNonce for chain %d: %w", cfg.chainSelector, err) + } + if txNonce < opCount { + lggr.Infow("operation already executed", "index", index, "txNonce", txNonce, "opCount", opCount) + return nil + } + if txNonce > opCount { + lggr.Warnw("txNonce too large", "index", index, "txNonce", txNonce, "opCount", opCount) + return fmt.Errorf("txNonce too large (%d; expected %d)", txNonce, opCount) + } + tx, err := executable.Execute(cmd.Context(), index) if err != nil { err = cldf.DecodeErr(bindings.ManyChainMultiSigABI, err) @@ -1177,6 +1199,11 @@ func executeChainCommand(ctx context.Context, lggr logger.Logger, cfg *cfgv2, sk if err != nil { return fmt.Errorf("error converting proposal to executable: %w", err) } + inspector, err := getInspectorFromChainSelector(*cfg) + if err != nil { + return fmt.Errorf("failed to get inspector: %w", err) + } + if cfg.fork { lggr.Info("Fork mode is on, all transactions will be executed on a forked chain") } @@ -1187,6 +1214,23 @@ func executeChainCommand(ctx context.Context, lggr logger.Logger, cfg *cfgv2, sk continue } + opCount, err := inspector.GetOpCount(ctx, cfg.proposal.ChainMetadata[types.ChainSelector(cfg.chainSelector)].MCMAddress) + if err != nil { + return fmt.Errorf("failed to get opcount for chain %d: %w", cfg.chainSelector, err) + } + txNonce, err := executable.TxNonce(i) + if err != nil { + return fmt.Errorf("failed to get TxNonce for chain %d: %w", cfg.chainSelector, err) + } + if txNonce < opCount { + lggr.Infow("operation already executed", "index", i, "txNonce", txNonce, "opCount", opCount) + continue + } + if txNonce > opCount { + lggr.Warnw("txNonce too large", "index", i, "txNonce", txNonce, "opCount", opCount) + break + } + tx, err := executable.Execute(ctx, i) if err != nil { lggr.Errorf("error executing operation %d: %s", i, err) @@ -1229,6 +1273,27 @@ func setRootCommand(ctx context.Context, lggr logger.Logger, cfg *cfgv2) error { lggr.Info("Fork mode is on, all transactions will be executed on a forked chain") } + inspector, err := getInspectorFromChainSelector(*cfg) + if err != nil { + return fmt.Errorf("failed to get inspector: %w", err) + } + + proposalMerkleTree, err := cfg.proposal.MerkleTree() + if err != nil { + return fmt.Errorf("failed to compute the proposal's merkle tree: %w", err) + } + + mcmAddress := cfg.proposal.ChainMetadata[types.ChainSelector(cfg.chainSelector)].MCMAddress + mcmRoot, _, err := inspector.GetRoot(ctx, mcmAddress) + if err != nil { + return fmt.Errorf("failed to get the merkle tree root from the MCM contract (%v): %w", mcmAddress, err) + } + + if mcmRoot == proposalMerkleTree.Root { + lggr.Infof("Root %v already set in MCM contract %v", mcmRoot, mcmAddress) + return nil + } + executable, err := createExecutable(cfg) if err != nil { return fmt.Errorf("error converting proposal to executable: %w", err) diff --git a/engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go b/engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go index 6465643d1..8ad8c6eb9 100644 --- a/engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go +++ b/engine/cld/legacy/cli/mcmsv2/mcms_v2_test.go @@ -2,6 +2,7 @@ package mcmsv2 import ( "context" + "encoding/json" "errors" "fmt" "maps" @@ -13,16 +14,18 @@ import ( "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/mcms" "github.com/smartcontractkit/mcms/sdk" + mcmsevmsdk "github.com/smartcontractkit/mcms/sdk/evm" mcmsevmbindings "github.com/smartcontractkit/mcms/sdk/evm/bindings" mocksdk "github.com/smartcontractkit/mcms/sdk/mocks" "github.com/smartcontractkit/mcms/types" "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" - "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" - + evmchain "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" datastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/domain" @@ -30,6 +33,7 @@ import ( testruntime "github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime" "github.com/smartcontractkit/chainlink-deployments-framework/experimental/analyzer" fpointer "github.com/smartcontractkit/chainlink-deployments-framework/internal/pointer" + "github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger" ) // nolint:paralleltest // uses and modifies files @@ -337,68 +341,15 @@ func Test_timelockExecuteOptions(t *testing.T) { loader := testenv.NewLoader() env, err := loader.Load(t.Context(), testenv.WithEVMSimulatedN(t, 1)) require.NoError(t, err) + lggr := logger.Test(t) err = os.MkdirAll("domains/exemplar", 0o700) require.NoError(t, err) t.Cleanup(func() { _ = os.RemoveAll("domains") }) - - lggr := logger.Test(t) exemplarDomain := domain.MustGetDomain("exemplar") - chain := slices.Collect(maps.Values(env.BlockChains.EVMChains()))[0] - callProxyAddress := common.Address{} - timelockAddress := common.Address{} - - changeset := cldf.CreateChangeSet( - func(e cldf.Environment, config struct{}) (cldf.ChangesetOutput, error) { - ds := datastore.NewMemoryDataStore() - ab := cldf.NewMemoryAddressBook() - var tx *ethtypes.Transaction - - // deploy call proxy - callProxyAddress, tx, _, err = mcmsevmbindings.DeployCallProxy(chain.DeployerKey, chain.Client, common.Address{}) - require.NoError(t, err) - err = ds.Addresses().Add(datastore.AddressRef{ - Address: callProxyAddress.Hex(), - ChainSelector: chain.Selector, - Type: "CallProxy", - Version: semver.MustParse("1.0.0"), - }) - require.NoError(t, err) - err = ab.Save(chain.Selector, callProxyAddress.Hex(), cldf.MustTypeAndVersionFromString("CallProxy 1.0.0")) - require.NoError(t, err) - _, err = chain.Confirm(tx) - require.NoError(t, err) - // deploy timelock - timelockAddress, tx, _, err = mcmsevmbindings.DeployRBACTimelock(chain.DeployerKey, chain.Client, big.NewInt(0), - chain.DeployerKey.From, - nil, // proposers - []common.Address{callProxyAddress}, // executors - nil, // bypassers - nil, // cancellers - ) - require.NoError(t, err) - err = ds.Addresses().Add(datastore.AddressRef{ - Address: timelockAddress.Hex(), - ChainSelector: chain.Selector, - Type: "RBACTimelock", - Version: semver.MustParse("1.0.0"), - }) - require.NoError(t, err) - err = ab.Save(chain.Selector, timelockAddress.Hex(), cldf.MustTypeAndVersionFromString("RBACTimelock 1.0.0")) - require.NoError(t, err) - _, err = chain.Confirm(tx) - require.NoError(t, err) - - return cldf.ChangesetOutput{AddressBook: ab, DataStore: ds}, nil - }, - func(e cldf.Environment, config struct{}) error { return nil }, // verify, - ) - task := testruntime.ChangesetTask(changeset, struct{}{}) - runtime := testruntime.NewFromEnvironment(*env) - err = runtime.Exec(task) - require.NoError(t, err) - env = fpointer.To(runtime.Environment()) + chain := slices.Collect(maps.Values(env.BlockChains.EVMChains()))[0] + timelockAddress, _, env := deployTimelockAndCallProxy(t, env, chain) errorContains := func(msg string) func(t *testing.T, opts []mcms.Option, err error) { return func(t *testing.T, opts []mcms.Option, err error) { @@ -447,7 +398,7 @@ func Test_timelockExecuteOptions(t *testing.T) { blockchains: env.BlockChains, timelockProposal: &mcms.TimelockProposal{ TimelockAddresses: map[types.ChainSelector]string{ - types.ChainSelector(chain.Selector): timelockAddress.Hex(), + types.ChainSelector(chain.Selector): timelockAddress, }, }, }, @@ -470,7 +421,7 @@ func Test_timelockExecuteOptions(t *testing.T) { }(), timelockProposal: &mcms.TimelockProposal{ TimelockAddresses: map[types.ChainSelector]string{ - types.ChainSelector(chain.Selector): timelockAddress.Hex(), + types.ChainSelector(chain.Selector): timelockAddress, }, }, }, @@ -488,7 +439,7 @@ func Test_timelockExecuteOptions(t *testing.T) { blockchains: env.BlockChains, timelockProposal: &mcms.TimelockProposal{ TimelockAddresses: map[types.ChainSelector]string{ - types.ChainSelector(1): timelockAddress.Hex(), + types.ChainSelector(1): timelockAddress, }, }, }, @@ -508,11 +459,11 @@ func Test_timelockExecuteOptions(t *testing.T) { }(), timelockProposal: &mcms.TimelockProposal{ TimelockAddresses: map[types.ChainSelector]string{ - types.ChainSelector(chain.Selector): timelockAddress.Hex(), + types.ChainSelector(chain.Selector): timelockAddress, }, }, }, - assert: errorContains(fmt.Sprintf("failed to find call proxy contract for timelock %v", timelockAddress.Hex())), + assert: errorContains(fmt.Sprintf("failed to find call proxy contract for timelock %v", timelockAddress)), }, } for _, tt := range tests { @@ -524,3 +475,387 @@ func Test_timelockExecuteOptions(t *testing.T) { }) } } + +func Test_setRootCommand(t *testing.T) { + t.Parallel() + + ctx := t.Context() + lggr, logs := logger.TestObserved(t, zapcore.InfoLevel) + + loader := testenv.NewLoader() + env, err := loader.Load(t.Context(), testenv.WithEVMSimulatedN(t, 1)) + require.NoError(t, err) + err = os.MkdirAll("domains/exemplar", 0o700) + require.NoError(t, err) + t.Cleanup(func() { _ = os.RemoveAll("domains") }) + + chain := slices.Collect(maps.Values(env.BlockChains.EVMChains()))[0] + inspector := mcmsevmsdk.NewInspector(chain.Client) + + privateKey, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + require.NoError(t, err) + signer := mcms.NewPrivateKeySigner(privateKey) + signerAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + + tests := []struct { + name string + cfg *cfgv2 + setup func(t *testing.T, cfg *cfgv2) (mcmAddress string) + skipNonceErrors bool + assertion func(t require.TestingT, mcmAddress string, cfg *cfgv2, err error, args ...any) + }{ + { + name: "success", + cfg: &cfgv2{ + kind: types.KindTimelockProposal, + chainSelector: chain.Selector, + envStr: env.Name, + env: *env, + blockchains: env.BlockChains, + }, + setup: func(t *testing.T, cfg *cfgv2) string { + t.Helper() + mcmAddress, uenv := deployMcm(t, env, chain, signerAddress) + cfg.env = *uenv + cfg.proposal = testMcmProposal(t, chain, mcmAddress) + signProposal(t, &cfg.proposal, signer, chain) + + return mcmAddress + }, + assertion: func(t require.TestingT, mcmAddress string, cfg *cfgv2, err error, args ...any) { + require.NoError(t, err) + + root, _, err := inspector.GetRoot(ctx, mcmAddress) + require.NoError(t, err) + + merkleTree, err := cfg.proposal.MerkleTree() + require.NoError(t, err) + require.Equal(t, merkleTree.Root, root) + }, + }, + { + name: "success on retry", + cfg: &cfgv2{ + kind: types.KindTimelockProposal, + chainSelector: chain.Selector, + envStr: env.Name, + env: *env, + blockchains: env.BlockChains, + }, + setup: func(t *testing.T, cfg *cfgv2) string { + t.Helper() + + mcmAddress, uenv := deployMcm(t, env, chain, signerAddress) + cfg.env = *uenv + cfg.proposal = testMcmProposal(t, chain, mcmAddress) + signProposal(t, &cfg.proposal, signer, chain) + + // call SetRoot the first time + err := setRootCommand(ctx, lggr, cfg) + require.NoError(t, err) + + root, _, err := inspector.GetRoot(ctx, mcmAddress) + require.NoError(t, err) + merkleTree, err := cfg.proposal.MerkleTree() + require.NoError(t, err) + require.Equal(t, merkleTree.Root, root) + + return mcmAddress + }, + assertion: func(t require.TestingT, mcmAddress string, cfg *cfgv2, err error, args ...any) { + require.NoError(t, err) + + root, _, err := inspector.GetRoot(ctx, mcmAddress) + require.NoError(t, err) + merkleTree, err := cfg.proposal.MerkleTree() + require.NoError(t, err) + require.Equal(t, merkleTree.Root, root) + + require.Equal(t, 1, logs.FilterMessage(fmt.Sprintf("Root %v already set in MCM contract %v", root, mcmAddress)).Len()) + }, + }, + } + for _, tt := range tests { //nolint:paralleltest + t.Run(tt.name, func(t *testing.T) { + mcmAddress := tt.setup(t, tt.cfg) + err := setRootCommand(ctx, lggr, tt.cfg) + tt.assertion(t, mcmAddress, tt.cfg, err) + }) + } +} + +func Test_executeChainCommand(t *testing.T) { + t.Parallel() + + ctx := t.Context() + lggr, logs := logger.TestObserved(t, zapcore.InfoLevel) + + loader := testenv.NewLoader() + env, err := loader.Load(t.Context(), testenv.WithEVMSimulatedN(t, 1)) + require.NoError(t, err) + err = os.MkdirAll("domains/exemplar", 0o700) + require.NoError(t, err) + t.Cleanup(func() { _ = os.RemoveAll("domains") }) + + chain := slices.Collect(maps.Values(env.BlockChains.EVMChains()))[0] + inspector := mcmsevmsdk.NewInspector(chain.Client) + + privateKey, err := crypto.HexToECDSA("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") + require.NoError(t, err) + signer := mcms.NewPrivateKeySigner(privateKey) + signerAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + + tests := []struct { + name string + cfg *cfgv2 + setup func(t *testing.T, cfg *cfgv2) (mcmAddress string) + skipNonceErrors bool + assertion func(t require.TestingT, mcmAddress string, cfg *cfgv2, err error, args ...any) + }{ + { + name: "success", + cfg: &cfgv2{ + kind: types.KindTimelockProposal, + chainSelector: chain.Selector, + envStr: env.Name, + env: *env, + blockchains: env.BlockChains, + }, + setup: func(t *testing.T, cfg *cfgv2) string { + t.Helper() + mcmAddress, uenv := deployMcm(t, env, chain, signerAddress) + cfg.env = *uenv + cfg.proposal = testMcmProposal(t, chain, mcmAddress) + + signProposal(t, &cfg.proposal, signer, chain) + + err := setRootCommand(ctx, lggr, cfg) + require.NoError(t, err) + + return mcmAddress + }, + assertion: func(t require.TestingT, mcmAddress string, cfg *cfgv2, err error, args ...any) { + require.NoError(t, err) + + opCount, err := inspector.GetOpCount(ctx, mcmAddress) + require.NoError(t, err) + require.Equal(t, uint64(1), opCount) + }, + }, + { + name: "success on retry", + cfg: &cfgv2{ + kind: types.KindTimelockProposal, + chainSelector: chain.Selector, + envStr: env.Name, + env: *env, + blockchains: env.BlockChains, + }, + setup: func(t *testing.T, cfg *cfgv2) string { + t.Helper() + + mcmAddress, uenv := deployMcm(t, env, chain, signerAddress) + cfg.env = *uenv + cfg.proposal = testMcmProposal(t, chain, mcmAddress) + + signProposal(t, &cfg.proposal, signer, chain) + + err := setRootCommand(ctx, lggr, cfg) + require.NoError(t, err) + + err = executeChainCommand(ctx, lggr, cfg, false) + require.NoError(t, err) + + opCount, err := inspector.GetOpCount(ctx, mcmAddress) + require.NoError(t, err) + require.Equal(t, uint64(1), opCount) + + return mcmAddress + }, + assertion: func(t require.TestingT, mcmAddress string, cfg *cfgv2, err error, args ...any) { + require.NoError(t, err) + + opCount, err := inspector.GetOpCount(ctx, mcmAddress) + require.NoError(t, err) + require.Equal(t, uint64(1), opCount) + require.Equal(t, logs.FilterMessage("operation already executed").All()[0].ContextMap(), map[string]any{ //nolint:testifylint + "index": int64(0), + "opCount": uint64(1), + "txNonce": uint64(0), + }) + }, + }, + } + for _, tt := range tests { //nolint:paralleltest + t.Run(tt.name, func(t *testing.T) { + mcmAddress := tt.setup(t, tt.cfg) + err := setRootCommand(ctx, lggr, tt.cfg) + require.NoError(t, err) + err = executeChainCommand(ctx, lggr, tt.cfg, tt.skipNonceErrors) + tt.assertion(t, mcmAddress, tt.cfg, err) + }) + } +} + +// ----- helpers and fixtures ----- + +func deployMcm( + t *testing.T, env *cldf.Environment, chain evmchain.Chain, signerAddress common.Address, +) (string, *cldf.Environment) { + t.Helper() + + mcmAddress := common.Address{} + changeset := cldf.CreateChangeSet( + func(e cldf.Environment, config struct{}) (cldf.ChangesetOutput, error) { + ds := datastore.NewMemoryDataStore() + var tx *ethtypes.Transaction + + // deploy mcm + var mcmContract *mcmsevmbindings.ManyChainMultiSig + var err error + mcmAddress, tx, mcmContract, err = mcmsevmbindings.DeployManyChainMultiSig(chain.DeployerKey, chain.Client) + require.NoError(t, err) + _, err = chain.Confirm(tx) + require.NoError(t, err) + err = ds.Addresses().Add(datastore.AddressRef{ + Address: mcmAddress.Hex(), + ChainSelector: chain.Selector, + Type: "ManyChainMultiSig", + Version: semver.MustParse("1.0.0"), + }) + require.NoError(t, err) + + // set config + tx, err = mcmContract.SetConfig(chain.DeployerKey, + []common.Address{signerAddress}, // signerAddresses + []uint8{0}, // signerGroups + [32]uint8{1}, // groupQuorums + [32]uint8{0}, // groupParents + true, + ) + require.NoError(t, err) + _, err = chain.Confirm(tx) + require.NoError(t, err) + + return cldf.ChangesetOutput{DataStore: ds}, nil + }, + func(e cldf.Environment, config struct{}) error { return nil }, // verify, + ) + + task := testruntime.ChangesetTask(changeset, struct{}{}) + runtime := testruntime.NewFromEnvironment(*env) + err := runtime.Exec(task) + require.NoError(t, err) + env = fpointer.To(runtime.Environment()) + + return mcmAddress.Hex(), env +} + +func deployTimelockAndCallProxy( + t *testing.T, env *cldf.Environment, chain evmchain.Chain, +) (string, string, *cldf.Environment) { + t.Helper() + + callProxyAddress := common.Address{} + timelockAddress := common.Address{} + changeset := cldf.CreateChangeSet( + func(e cldf.Environment, config struct{}) (cldf.ChangesetOutput, error) { + ds := datastore.NewMemoryDataStore() + ab := cldf.NewMemoryAddressBook() + var tx *ethtypes.Transaction + var err error + + // deploy call proxy + callProxyAddress, tx, _, err = mcmsevmbindings.DeployCallProxy(chain.DeployerKey, chain.Client, common.Address{}) + require.NoError(t, err) + err = ds.Addresses().Add(datastore.AddressRef{ + Address: callProxyAddress.Hex(), + ChainSelector: chain.Selector, + Type: "CallProxy", + Version: semver.MustParse("1.0.0"), + }) + require.NoError(t, err) + err = ab.Save(chain.Selector, callProxyAddress.Hex(), cldf.MustTypeAndVersionFromString("CallProxy 1.0.0")) + require.NoError(t, err) + _, err = chain.Confirm(tx) + require.NoError(t, err) + + // deploy timelock + timelockAddress, tx, _, err = mcmsevmbindings.DeployRBACTimelock(chain.DeployerKey, chain.Client, big.NewInt(0), + chain.DeployerKey.From, + nil, // proposers + []common.Address{callProxyAddress}, // executors + nil, // bypassers + nil, // cancellers + ) + require.NoError(t, err) + err = ds.Addresses().Add(datastore.AddressRef{ + Address: timelockAddress.Hex(), + ChainSelector: chain.Selector, + Type: "RBACTimelock", + Version: semver.MustParse("1.0.0"), + }) + require.NoError(t, err) + err = ab.Save(chain.Selector, timelockAddress.Hex(), cldf.MustTypeAndVersionFromString("RBACTimelock 1.0.0")) + require.NoError(t, err) + _, err = chain.Confirm(tx) + require.NoError(t, err) + + return cldf.ChangesetOutput{AddressBook: ab, DataStore: ds}, nil + }, + func(e cldf.Environment, config struct{}) error { return nil }, // verify, + ) + + task := testruntime.ChangesetTask(changeset, struct{}{}) + runtime := testruntime.NewFromEnvironment(*env) + err := runtime.Exec(task) + require.NoError(t, err) + env = fpointer.To(runtime.Environment()) + + return timelockAddress.Hex(), callProxyAddress.Hex(), env +} + +func testMcmProposal( + t *testing.T, + chain evmchain.Chain, + mcmAddress string, +) mcms.Proposal { + t.Helper() + + proposal, err := mcms.NewProposalBuilder(). + SetVersion("v1"). + SetValidUntil(2082758399). + SetDescription("test proposal"). + SetOverridePreviousRoot(true). + AddChainMetadata( + types.ChainSelector(chain.Selector), + types.ChainMetadata{MCMAddress: mcmAddress}, + ). + AddOperation(types.Operation{ + ChainSelector: types.ChainSelector(chain.Selector), + Transaction: types.Transaction{ + To: chain.DeployerKey.From.Hex(), + Data: []byte("0x"), + AdditionalFields: json.RawMessage(`{"value": 0}`), + }, + }).Build() + require.NoError(t, err) + + return *proposal +} + +func signProposal( + t *testing.T, proposal *mcms.Proposal, signer *mcms.PrivateKeySigner, chain evmchain.Chain, +) { + t.Helper() + + inspector := mcmsevmsdk.NewInspector(chain.Client) + + signable, err := mcms.NewSignable(proposal, map[types.ChainSelector]sdk.Inspector{ + types.ChainSelector(chain.Selector): inspector, + }) + require.NoError(t, err) + + _, err = signable.SignAndAppend(signer) + require.NoError(t, err) +} diff --git a/engine/test/onchain/evm.go b/engine/test/onchain/evm.go index bf2d0da4c..cc1561cf4 100644 --- a/engine/test/onchain/evm.go +++ b/engine/test/onchain/evm.go @@ -20,8 +20,11 @@ type EVMSimLoaderConfig struct { // NewEVMSimLoader creates a new EVM chain loader with default simulated backend configuration. // Uses go-ethereum's simulated backend with default settings for fast test execution. func NewEVMSimLoader() *ChainLoader { + selectors := getTestSelectorsByFamily(chainselectors.FamilyEVM) + selectors = append([]uint64{chainselectors.GETH_TESTNET.Selector}, selectors...) + return &ChainLoader{ - selectors: getTestSelectorsByFamily(chainselectors.FamilyEVM), + selectors: selectors, factory: func(t *testing.T, selector uint64) (fchain.BlockChain, error) { t.Helper() diff --git a/engine/test/onchain/evm_test.go b/engine/test/onchain/evm_test.go index abba64059..e42b32b4a 100644 --- a/engine/test/onchain/evm_test.go +++ b/engine/test/onchain/evm_test.go @@ -17,7 +17,10 @@ func Test_NewEVMSimLoaderEVM(t *testing.T) { // Should have the same selectors as getTestSelectorsByFamily returns require.NotNil(t, loader.selectors) - want := getTestSelectorsByFamily(chainselectors.FamilyEVM) + want := append( + []uint64{chainselectors.GETH_TESTNET.Selector}, + getTestSelectorsByFamily(chainselectors.FamilyEVM)..., + ) assert.Equal(t, want, loader.selectors) // Note: We can't actually call the factory without starting simulated backends, @@ -38,7 +41,10 @@ func Test_NewEVMSimLoaderEVMWithConfig(t *testing.T) { // Should have the same selectors as getTestSelectorsByFamily returns require.NotNil(t, loader.selectors) - want := getTestSelectorsByFamily(chainselectors.FamilyEVM) + want := append( + []uint64{chainselectors.GETH_TESTNET.Selector}, + getTestSelectorsByFamily(chainselectors.FamilyEVM)..., + ) assert.Equal(t, want, loader.selectors) // Factory should be configured with the provided config diff --git a/go.mod b/go.mod index 5314a4091..309956abc 100644 --- a/go.mod +++ b/go.mod @@ -33,12 +33,12 @@ require ( github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250805210128-7f8a0f403c3a github.com/smartcontractkit/chainlink-protos/job-distributor v0.17.0 github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 - github.com/smartcontractkit/chainlink-testing-framework/framework v0.11.3 + github.com/smartcontractkit/chainlink-testing-framework/framework v0.11.7 github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.2 github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20250815105909-75499abc4335 github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e github.com/smartcontractkit/libocr v0.0.0-20250707144819-babe0ec4e358 - github.com/smartcontractkit/mcms v0.30.0 + github.com/smartcontractkit/mcms v0.30.1 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 diff --git a/go.sum b/go.sum index e86ae0856..32393fe40 100644 --- a/go.sum +++ b/go.sum @@ -703,8 +703,8 @@ github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 h1:AEnxv4HM3WD1Rb github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4/go.mod h1:PjZD54vr6rIKEKQj6HNA4hllvYI/QpT+Zefj3tqkFAs= github.com/smartcontractkit/chainlink-sui v0.0.0-20251103204108-d181d6769bab h1:X1VWFEaoLSCCaxFR3BWbo09UMEpHANtylesISbIDgsw= github.com/smartcontractkit/chainlink-sui v0.0.0-20251103204108-d181d6769bab/go.mod h1:VlyZhVw+a93Sk8rVHOIH6tpiXrMzuWLZrjs1eTIExW8= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.11.3 h1:crYKFHTxxt1TNYuPOptjVyJdW4wO15aV5vVuEeLhICY= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.11.3/go.mod h1:ssfyl4ynbxSyASGztjuAxhsum5i6uZSHM7Dd0v2p8sc= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.11.7 h1:jVlRG9GTpDGYtP0iabxHZW4s3pXdpN4/lTgZEdE64P4= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.11.7/go.mod h1:BTUmWJGbOQtMdDW8cy4fu0wLoj8tKFQiLR7SE+OyTXU= github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.2 h1:ZJ/8Jx6Be5//TyjPi1pS1uotnmcYq5vVkSyISIymSj8= github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.2/go.mod h1:kHYJnZUqiPF7/xN5273prV+srrLJkS77GbBXHLKQpx0= github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20250815105909-75499abc4335 h1:7bxYNrPpygn8PUSBiEKn8riMd7CXMi/4bjTy0fHhcrY= @@ -717,8 +717,8 @@ github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12i github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20250707144819-babe0ec4e358 h1:+NVzR5LZVazRUunzVn34u+lwnpmn6NTVPCeZOVyQHLo= github.com/smartcontractkit/libocr v0.0.0-20250707144819-babe0ec4e358/go.mod h1:Acy3BTBxou83ooMESLO90s8PKSu7RvLCzwSTbxxfOK0= -github.com/smartcontractkit/mcms v0.30.0 h1:cFIhLxz1R7qwKcOEEeEujqTw7dtU7NTKpDkfjQnJ450= -github.com/smartcontractkit/mcms v0.30.0/go.mod h1:DqQAMMqoDxzHVbgU4N5VSZRR47D+pLctoJ9Eth9PKmo= +github.com/smartcontractkit/mcms v0.30.1 h1:AtsbK/gAbSp2fwDNiUCncV+LjgyM19cilPquFKGwTYg= +github.com/smartcontractkit/mcms v0.30.1/go.mod h1:ow51e6OZg2mUgShaoNHUYVpChvNejUR4H8crV79ZBGk= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=