Skip to content

Commit 2f9d41c

Browse files
committed
feat(port): firedrill mcms with operations api refactor (#25)
1 parent 5919919 commit 2f9d41c

8 files changed

Lines changed: 704 additions & 0 deletions

File tree

common/changeset/run_changeset.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package changesets
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/smartcontractkit/chainlink-deployments-framework/deployment"
7+
)
8+
9+
type WrappedChangeSet[C any] struct {
10+
operation deployment.ChangeSetV2[C]
11+
}
12+
13+
// RunChangeset is used to run a changeset in another changeset
14+
// It executes VerifyPreconditions internally to handle changeset errors.
15+
func RunChangeset[C any](
16+
operation deployment.ChangeSetV2[C],
17+
env deployment.Environment,
18+
config C,
19+
) (deployment.ChangesetOutput, error) {
20+
cs := WrappedChangeSet[C]{operation: operation}
21+
22+
err := cs.operation.VerifyPreconditions(env, config)
23+
if err != nil {
24+
return deployment.ChangesetOutput{}, fmt.Errorf("failed to run precondition: %w", err)
25+
}
26+
27+
return cs.operation.Apply(env, config)
28+
}

mcms/changesets/firedrill.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package changesets
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/smartcontractkit/mcms"
8+
9+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
10+
cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"
11+
fwops "github.com/smartcontractkit/chainlink-deployments-framework/operations"
12+
13+
chainsel "github.com/smartcontractkit/chain-selectors"
14+
cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain"
15+
16+
mcops "github.com/smartcontractkit/cld-changesets/mcms/operations"
17+
evmstate "github.com/smartcontractkit/cld-changesets/pkg/family/evm"
18+
)
19+
20+
var _ cldf.ChangeSetV2[FireDrillConfig] = MCMSSignFireDrillChangeset{}
21+
22+
// FireDrillConfig selects chains and MCMS timelock routing for a signing fire drill.
23+
type FireDrillConfig struct {
24+
TimelockCfg cldfproposalutils.TimelockConfig `json:"timelockCfg"`
25+
Selectors []uint64 `json:"selectors,omitempty"`
26+
}
27+
28+
// MCMSSignFireDrillChangeset creates an MCMS signing fire-drill proposal with noop operations per chain.
29+
// It exercises signing and execution pipelines without mutating on-chain configuration.
30+
type MCMSSignFireDrillChangeset struct{}
31+
32+
// ResolvedSelectors returns the chain selectors VerifyPreconditions and the fire-drill operation will use.
33+
// When cfg.Selectors is empty, it defaults to every Solana chain in the environment followed by every EVM chain.
34+
func (cfg FireDrillConfig) ResolvedSelectors(e cldf.Environment) []uint64 {
35+
return cfg.resolvedSelectors(e)
36+
}
37+
38+
// VerifyPreconditions ensures each target chain exists and MCMS timelock state satisfies the configured action.
39+
func (MCMSSignFireDrillChangeset) VerifyPreconditions(e cldf.Environment, cfg FireDrillConfig) error {
40+
selectors := cfg.ResolvedSelectors(e)
41+
if len(selectors) == 0 {
42+
return errors.New("no chain selectors resolved for MCMS fire drill")
43+
}
44+
45+
for _, selector := range selectors {
46+
family, err := chainsel.GetSelectorFamily(selector)
47+
if err != nil {
48+
return err
49+
}
50+
51+
switch family {
52+
case chainsel.FamilyEVM:
53+
ch, ok := e.BlockChains.EVMChains()[selector]
54+
if !ok {
55+
return fmt.Errorf("evm chain %d not found in environment", selector)
56+
}
57+
58+
addresses, err := e.ExistingAddresses.AddressesForChain(selector) //nolint:staticcheck // SA1019
59+
if err != nil {
60+
return fmt.Errorf("addresses for chain %d: %w", selector, err)
61+
}
62+
63+
st, err := evmstate.MaybeLoadMCMSWithTimelockChainState(ch, addresses)
64+
if err != nil {
65+
return fmt.Errorf("load MCMS timelock state for chain %d: %w", selector, err)
66+
}
67+
68+
if err := cfg.TimelockCfg.Validate(ch, st); err != nil {
69+
return fmt.Errorf("timelock config for chain %d: %w", selector, err)
70+
}
71+
72+
case chainsel.FamilySolana:
73+
if _, ok := e.BlockChains.SolanaChains()[selector]; !ok {
74+
return fmt.Errorf("solana chain %d not found in environment", selector)
75+
}
76+
77+
if err := cfg.TimelockCfg.ValidateSolana(e, selector); err != nil {
78+
return fmt.Errorf("timelock config for chain %d: %w", selector, err)
79+
}
80+
81+
default:
82+
return fmt.Errorf("unsupported chain family for selector %d", selector)
83+
}
84+
}
85+
86+
return nil
87+
}
88+
89+
// Apply builds the fire-drill proposal via the operations API (with force execute for repeatable drills).
90+
func (MCMSSignFireDrillChangeset) Apply(e cldf.Environment, cfg FireDrillConfig) (cldf.ChangesetOutput, error) {
91+
deps := mcops.FireDrillDeps{Environment: e}
92+
input := mcops.FireDrillInput{TimelockCfg: cfg.TimelockCfg, Selectors: cfg.Selectors}
93+
94+
report, err := fwops.ExecuteOperation[mcops.FireDrillInput, mcops.FireDrillOutput, mcops.FireDrillDeps](
95+
e.OperationsBundle,
96+
mcops.BuildMCMSFiredrillProposalOp,
97+
deps,
98+
input,
99+
fwops.WithForceExecute[mcops.FireDrillInput, mcops.FireDrillDeps](),
100+
)
101+
out := cldf.ChangesetOutput{
102+
Reports: []fwops.Report[any, any]{report.ToGenericReport()},
103+
}
104+
if err != nil {
105+
return out, err
106+
}
107+
108+
out.MCMSTimelockProposals = []mcms.TimelockProposal{report.Output.Proposal}
109+
110+
return out, nil
111+
}
112+
113+
func (cfg FireDrillConfig) resolvedSelectors(e cldf.Environment) []uint64 {
114+
if len(cfg.Selectors) > 0 {
115+
return cfg.Selectors
116+
}
117+
solSelectors := e.BlockChains.ListChainSelectors(cldf_chain.WithFamily(chainsel.FamilySolana))
118+
evmSelectors := e.BlockChains.ListChainSelectors(cldf_chain.WithFamily(chainsel.FamilyEVM))
119+
out := make([]uint64, 0, len(solSelectors)+len(evmSelectors))
120+
out = append(out, solSelectors...)
121+
out = append(out, evmSelectors...)
122+
123+
return out
124+
}

mcms/changesets/firedrill_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package changesets
2+
3+
import (
4+
"testing"
5+
6+
chainselectors "github.com/smartcontractkit/chain-selectors"
7+
cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain"
8+
cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
9+
cldf_solana "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana"
10+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
11+
cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"
12+
mcmstypes "github.com/smartcontractkit/mcms/types"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestMCMSSignFireDrillChangeset_VerifyPreconditions_NoChainsResolved(t *testing.T) {
17+
t.Parallel()
18+
19+
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(nil))
20+
cfg := FireDrillConfig{TimelockCfg: cldfproposalutils.TimelockConfig{}}
21+
22+
err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg)
23+
require.ErrorContains(t, err, "no chain selectors resolved")
24+
}
25+
26+
func TestMCMSSignFireDrillChangeset_VerifyPreconditions_UnknownChain(t *testing.T) {
27+
t.Parallel()
28+
29+
sel := uint64(999991)
30+
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(nil))
31+
cfg := FireDrillConfig{
32+
TimelockCfg: cldfproposalutils.TimelockConfig{},
33+
Selectors: []uint64{sel},
34+
}
35+
36+
err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg)
37+
require.Error(t, err)
38+
_, famErr := chainselectors.GetSelectorFamily(sel)
39+
if famErr != nil {
40+
require.ErrorContains(t, err, famErr.Error())
41+
} else {
42+
require.ErrorContains(t, err, "not found in environment")
43+
}
44+
}
45+
46+
func TestMCMSSignFireDrillChangeset_VerifyPreconditions_unsupportedChainFamily(t *testing.T) {
47+
t.Parallel()
48+
49+
sel := chainselectors.APTOS_MAINNET.Selector
50+
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(nil))
51+
cfg := FireDrillConfig{
52+
TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule},
53+
Selectors: []uint64{sel},
54+
}
55+
56+
err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg)
57+
require.ErrorContains(t, err, "unsupported chain family")
58+
}
59+
60+
func TestMCMSSignFireDrillChangeset_VerifyPreconditions_evmChainNotInEnvironment(t *testing.T) {
61+
t.Parallel()
62+
63+
evmSel := chainselectors.TEST_90000002.Selector
64+
solSel := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector
65+
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{
66+
solSel: cldf_solana.Chain{Selector: solSel},
67+
}))
68+
cfg := FireDrillConfig{
69+
TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule},
70+
Selectors: []uint64{evmSel},
71+
}
72+
73+
err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg)
74+
require.ErrorContains(t, err, "evm chain")
75+
require.ErrorContains(t, err, "not found in environment")
76+
}
77+
78+
func TestMCMSSignFireDrillChangeset_VerifyPreconditions_solanaChainNotInEnvironment(t *testing.T) {
79+
t.Parallel()
80+
81+
evmSel := chainselectors.TEST_90000002.Selector
82+
solSel := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector
83+
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{
84+
evmSel: cldf_evm.Chain{Selector: evmSel},
85+
}))
86+
cfg := FireDrillConfig{
87+
TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule},
88+
Selectors: []uint64{solSel},
89+
}
90+
91+
err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg)
92+
require.ErrorContains(t, err, "solana chain")
93+
require.ErrorContains(t, err, "not found in environment")
94+
}
95+
96+
func TestMCMSSignFireDrillChangeset_VerifyPreconditions_missingAddressBookEntry(t *testing.T) {
97+
t.Parallel()
98+
99+
evmSel := chainselectors.TEST_90000002.Selector
100+
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{
101+
evmSel: cldf_evm.Chain{Selector: evmSel},
102+
}))
103+
cfg := FireDrillConfig{
104+
TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule},
105+
Selectors: []uint64{evmSel},
106+
}
107+
108+
err := MCMSSignFireDrillChangeset{}.VerifyPreconditions(env, cfg)
109+
require.ErrorContains(t, err, "addresses for chain")
110+
}
111+
112+
func TestFireDrillConfig_ResolvedSelectors_defaultOrderSolanaBeforeEVM(t *testing.T) {
113+
t.Parallel()
114+
115+
evmSel := chainselectors.TEST_90000002.Selector
116+
solSel := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector
117+
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{
118+
evmSel: cldf_evm.Chain{Selector: evmSel},
119+
solSel: cldf_solana.Chain{Selector: solSel},
120+
}))
121+
122+
got := FireDrillConfig{TimelockCfg: cldfproposalutils.TimelockConfig{}}.ResolvedSelectors(env)
123+
require.Equal(t, []uint64{solSel, evmSel}, got)
124+
}
125+
126+
func TestFireDrillConfig_ResolvedSelectors_explicitPreservesInputOrder(t *testing.T) {
127+
t.Parallel()
128+
129+
evmSel := chainselectors.TEST_90000002.Selector
130+
solSel := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector
131+
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(map[uint64]cldf_chain.BlockChain{
132+
evmSel: cldf_evm.Chain{Selector: evmSel},
133+
solSel: cldf_solana.Chain{Selector: solSel},
134+
}))
135+
136+
got := FireDrillConfig{
137+
TimelockCfg: cldfproposalutils.TimelockConfig{},
138+
Selectors: []uint64{evmSel, solSel},
139+
}.ResolvedSelectors(env)
140+
require.Equal(t, []uint64{evmSel, solSel}, got)
141+
}
142+
143+
func TestMCMSSignFireDrillChangeset_Apply_returnsReportOnFailure(t *testing.T) {
144+
t.Parallel()
145+
146+
env := testEnvironment(t, cldf.NewMemoryAddressBook(), cldf_chain.NewBlockChains(nil))
147+
cfg := FireDrillConfig{
148+
TimelockCfg: cldfproposalutils.TimelockConfig{MCMSAction: mcmstypes.TimelockActionSchedule},
149+
}
150+
151+
out, err := MCMSSignFireDrillChangeset{}.Apply(env, cfg)
152+
require.ErrorContains(t, err, "no chain selectors resolved")
153+
require.Len(t, out.Reports, 1)
154+
require.Empty(t, out.MCMSTimelockProposals)
155+
require.NotNil(t, out.Reports[0].Err)
156+
require.ErrorContains(t, out.Reports[0].Err, "no chain selectors resolved")
157+
}

0 commit comments

Comments
 (0)