Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
124 changes: 124 additions & 0 deletions mcms/changesets/firedrill.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package changesets

import (
"errors"
"fmt"

"github.com/smartcontractkit/mcms"

cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"
fwops "github.com/smartcontractkit/chainlink-deployments-framework/operations"

chainsel "github.com/smartcontractkit/chain-selectors"
cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain"

mcops "github.com/smartcontractkit/cld-changesets/mcms/operations"
evmstate "github.com/smartcontractkit/cld-changesets/pkg/family/evm"
)

var _ cldf.ChangeSetV2[FireDrillConfig] = MCMSSignFireDrillChangeset{}

// FireDrillConfig selects chains and MCMS timelock routing for a signing fire drill.
type FireDrillConfig struct {
TimelockCfg cldfproposalutils.TimelockConfig `json:"timelockCfg"`
Selectors []uint64 `json:"selectors,omitempty"`
}

// MCMSSignFireDrillChangeset creates an MCMS signing fire-drill proposal with noop operations per chain.
// It exercises signing and execution pipelines without mutating on-chain configuration.
type MCMSSignFireDrillChangeset struct{}

// ResolvedSelectors returns the chain selectors VerifyPreconditions and the fire-drill operation will use.
// When cfg.Selectors is empty, it defaults to every Solana chain in the environment followed by every EVM chain.
func (cfg FireDrillConfig) ResolvedSelectors(e cldf.Environment) []uint64 {
return cfg.resolvedSelectors(e)
}

// VerifyPreconditions ensures each target chain exists and MCMS timelock state satisfies the configured action.
func (MCMSSignFireDrillChangeset) VerifyPreconditions(e cldf.Environment, cfg FireDrillConfig) error {
selectors := cfg.ResolvedSelectors(e)
if len(selectors) == 0 {
return errors.New("no chain selectors resolved for MCMS fire drill")
}

for _, selector := range selectors {
family, err := chainsel.GetSelectorFamily(selector)
if err != nil {
return err
}

switch family {
case chainsel.FamilyEVM:
ch, ok := e.BlockChains.EVMChains()[selector]
if !ok {
return fmt.Errorf("evm chain %d not found in environment", selector)
}

addresses, err := e.ExistingAddresses.AddressesForChain(selector) //nolint:staticcheck // SA1019
if err != nil {
return fmt.Errorf("addresses for chain %d: %w", selector, err)
}

st, err := evmstate.MaybeLoadMCMSWithTimelockChainState(ch, addresses)
if err != nil {
return fmt.Errorf("load MCMS timelock state for chain %d: %w", selector, err)
}

if err := cfg.TimelockCfg.Validate(ch, st); err != nil {
return fmt.Errorf("timelock config for chain %d: %w", selector, err)
}

case chainsel.FamilySolana:
if _, ok := e.BlockChains.SolanaChains()[selector]; !ok {
return fmt.Errorf("solana chain %d not found in environment", selector)
}

if err := cfg.TimelockCfg.ValidateSolana(e, selector); err != nil {
return fmt.Errorf("timelock config for chain %d: %w", selector, err)
}

default:
return fmt.Errorf("unsupported chain family for selector %d", selector)
}
}

return nil
}

// Apply builds the fire-drill proposal via the operations API (with force execute for repeatable drills).
func (MCMSSignFireDrillChangeset) Apply(e cldf.Environment, cfg FireDrillConfig) (cldf.ChangesetOutput, error) {
deps := mcops.FireDrillDeps{Environment: e}
input := mcops.FireDrillInput{TimelockCfg: cfg.TimelockCfg, Selectors: cfg.Selectors}

report, err := fwops.ExecuteOperation[mcops.FireDrillInput, mcops.FireDrillOutput, mcops.FireDrillDeps](
e.OperationsBundle,
mcops.BuildMCMSFiredrillProposalOp,
deps,
input,
fwops.WithForceExecute[mcops.FireDrillInput, mcops.FireDrillDeps](),
)
out := cldf.ChangesetOutput{
Reports: []fwops.Report[any, any]{report.ToGenericReport()},
}
if err != nil {
return out, err
}

out.MCMSTimelockProposals = []mcms.TimelockProposal{report.Output.Proposal}

return out, nil
}

func (cfg FireDrillConfig) resolvedSelectors(e cldf.Environment) []uint64 {
if len(cfg.Selectors) > 0 {
return cfg.Selectors
}
solSelectors := e.BlockChains.ListChainSelectors(cldf_chain.WithFamily(chainsel.FamilySolana))
evmSelectors := e.BlockChains.ListChainSelectors(cldf_chain.WithFamily(chainsel.FamilyEVM))
out := make([]uint64, 0, len(solSelectors)+len(evmSelectors))
out = append(out, solSelectors...)
out = append(out, evmSelectors...)

return out
}
157 changes: 157 additions & 0 deletions mcms/changesets/firedrill_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package changesets

import (
"testing"

chainselectors "github.com/smartcontractkit/chain-selectors"
cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain"
cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
cldf_solana "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana"
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"
mcmstypes "github.com/smartcontractkit/mcms/types"
"github.com/stretchr/testify/require"
)

func TestMCMSSignFireDrillChangeset_VerifyPreconditions_NoChainsResolved(t *testing.T) {
t.Parallel()

env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(nil))
cfg := FireDrillConfig{TimelockCfg: cldfproposalutils.TimelockConfig{}}

err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg)
require.ErrorContains(t, err, "no chain selectors resolved")
}

func TestMCMSSignFireDrillChangeset_VerifyPreconditions_UnknownChain(t *testing.T) {
t.Parallel()

sel := uint64(999991)
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(nil))
cfg := FireDrillConfig{
TimelockCfg: cldfproposalutils.TimelockConfig{},
Selectors: []uint64{sel},
}

err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg)
require.Error(t, err)
_, famErr := chainselectors.GetSelectorFamily(sel)
if famErr != nil {
require.ErrorContains(t, err, famErr.Error())
} else {
require.ErrorContains(t, err, "not found in environment")
}
}

func TestMCMSSignFireDrillChangeset_VerifyPreconditions_unsupportedChainFamily(t *testing.T) {
t.Parallel()

sel := chainselectors.APTOS_MAINNET.Selector
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(nil))
cfg := FireDrillConfig{
TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule},
Selectors: []uint64{sel},
}

err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg)
require.ErrorContains(t, err, "unsupported chain family")
}

func TestMCMSSignFireDrillChangeset_VerifyPreconditions_evmChainNotInEnvironment(t *testing.T) {
t.Parallel()

evmSel := chainselectors.TEST_90000002.Selector
solSel := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{
solSel: cldf_solana.Chain{Selector: solSel},
}))
cfg := FireDrillConfig{
TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule},
Selectors: []uint64{evmSel},
}

err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg)
require.ErrorContains(t, err, "evm chain")
require.ErrorContains(t, err, "not found in environment")
}

func TestMCMSSignFireDrillChangeset_VerifyPreconditions_solanaChainNotInEnvironment(t *testing.T) {
t.Parallel()

evmSel := chainselectors.TEST_90000002.Selector
solSel := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{
evmSel: cldf_evm.Chain{Selector: evmSel},
}))
cfg := FireDrillConfig{
TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule},
Selectors: []uint64{solSel},
}

err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg)
require.ErrorContains(t, err, "solana chain")
require.ErrorContains(t, err, "not found in environment")
}

func TestMCMSSignFireDrillChangeset_VerifyPreconditions_missingAddressBookEntry(t *testing.T) {
t.Parallel()

evmSel := chainselectors.TEST_90000002.Selector
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{
evmSel: cldf_evm.Chain{Selector: evmSel},
}))
cfg := FireDrillConfig{
TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule},
Selectors: []uint64{evmSel},
}

err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg)
require.ErrorContains(t, err, "addresses for chain")
}

func TestFireDrillConfig_ResolvedSelectors_defaultOrderSolanaBeforeEVM(t *testing.T) {
t.Parallel()

evmSel := chainselectors.TEST_90000002.Selector
solSel := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{
evmSel: cldf_evm.Chain{Selector: evmSel},
solSel: cldf_solana.Chain{Selector: solSel},
}))

got := FireDrillConfig{TimelockCfg: cldfproposalutils.TimelockConfig{}}.ResolvedSelectors(env)
require.Equal(t, []uint64{solSel, evmSel}, got)
}

func TestFireDrillConfig_ResolvedSelectors_explicitPreservesInputOrder(t *testing.T) {
t.Parallel()

evmSel := chainselectors.TEST_90000002.Selector
solSel := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{
evmSel: cldf_evm.Chain{Selector: evmSel},
solSel: cldf_solana.Chain{Selector: solSel},
}))

got := FireDrillConfig{
TimelockCfg: cldfproposalutils.TimelockConfig{},
Selectors: []uint64{evmSel, solSel},
}.ResolvedSelectors(env)
require.Equal(t, []uint64{evmSel, solSel}, got)
}

func TestMCMSSignFireDrillChangeset_Apply_returnsReportOnFailure(t *testing.T) {
t.Parallel()

env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(nil))
cfg := FireDrillConfig{
TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule},
}

out, err := MCMSSignFireDrillChangeset{}.Apply(env, cfg)
require.ErrorContains(t, err, "no chain selectors resolved")
require.Len(t, out.Reports, 1)
require.Empty(t, out.MCMSTimelockProposals)
require.NotNil(t, out.Reports[0].Err)
require.ErrorContains(t, out.Reports[0].Err, "no chain selectors resolved")
}
Loading
Loading