Skip to content

Commit 3011cf2

Browse files
feat(mcms): add datastore-backed MCMS fire drill changeset
Introduce a new firedrill changeset that resolves MCMS refs from the datastore and builds noop timelock proposals via the set-config/deploy registry pattern, replacing the legacy address book flow for EVM and Solana.
1 parent da2e813 commit 3011cf2

14 files changed

Lines changed: 1024 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Package all blank-imports built-in MCMS fire-drill families and MCMS readers.
2+
// Use it when a pipeline needs every supported chain family. For single-family
3+
// pipelines, blank-import mcms/<family>/firedrill and mcms/<family>/readers instead.
4+
package all
5+
6+
import (
7+
_ "github.com/smartcontractkit/cld-changesets/mcms/evm/firedrill"
8+
_ "github.com/smartcontractkit/cld-changesets/mcms/evm/readers"
9+
_ "github.com/smartcontractkit/cld-changesets/mcms/solana/firedrill"
10+
_ "github.com/smartcontractkit/cld-changesets/mcms/solana/readers"
11+
)
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package firedrill
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"slices"
7+
8+
chainselectors "github.com/smartcontractkit/chain-selectors"
9+
cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain"
10+
"github.com/smartcontractkit/chainlink-deployments-framework/changeset/sequenceutils"
11+
cldfdatastore "github.com/smartcontractkit/chainlink-deployments-framework/datastore"
12+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
13+
)
14+
15+
var _ cldf.ChangeSetV2[Input] = Changeset{}
16+
17+
// Changeset creates an MCMS signing fire-drill proposal with noop operations per chain.
18+
// It exercises signing and execution pipelines without mutating on-chain configuration.
19+
type Changeset struct{}
20+
21+
// ResolvedSelectors returns the chain selectors VerifyPreconditions and Apply will use.
22+
// When cfg.Selectors is empty, it defaults to every Solana chain in the environment followed by every EVM chain.
23+
func (cfg Config) ResolvedSelectors(e cldf.Environment) []uint64 {
24+
return resolvedSelectors(e, cfg.Selectors)
25+
}
26+
27+
func (Changeset) VerifyPreconditions(e cldf.Environment, input Input) error {
28+
if e.DataStore == nil {
29+
return errors.New("datastore is required for MCMS fire drill")
30+
}
31+
if input.MCMS == nil {
32+
return errors.New("MCMS timelock proposal input is required")
33+
}
34+
if err := input.MCMS.Validate(); err != nil {
35+
return fmt.Errorf("invalid MCMS timelock proposal input: %w", err)
36+
}
37+
38+
selectors := resolvedSelectors(e, input.Cfg.Selectors)
39+
if len(selectors) == 0 {
40+
return errors.New("no chain selectors resolved for MCMS fire drill")
41+
}
42+
43+
byFamily := make(map[string][]ChainInput)
44+
for _, chainSelector := range selectors {
45+
family, err := chainselectors.GetSelectorFamily(chainSelector)
46+
if err != nil {
47+
return err
48+
}
49+
byFamily[family] = append(byFamily[family], ChainInput{
50+
ChainSelector: chainSelector,
51+
MCMS: *input.MCMS,
52+
})
53+
}
54+
55+
families := make([]string, 0, len(byFamily))
56+
for family := range byFamily {
57+
families = append(families, family)
58+
}
59+
slices.Sort(families)
60+
61+
for _, family := range families {
62+
if err := verifyForFamily(family, e, byFamily[family]); err != nil {
63+
return err
64+
}
65+
}
66+
67+
return nil
68+
}
69+
70+
func (Changeset) Apply(e cldf.Environment, input Input) (cldf.ChangesetOutput, error) {
71+
if input.MCMS == nil {
72+
return cldf.ChangesetOutput{}, errors.New("MCMS timelock proposal input is required")
73+
}
74+
75+
selectors := resolvedSelectors(e, input.Cfg.Selectors)
76+
if len(selectors) == 0 {
77+
return cldf.ChangesetOutput{}, errors.New("no chain selectors resolved for MCMS fire drill")
78+
}
79+
80+
deps := Deps{
81+
BlockChains: e.BlockChains,
82+
DataStore: e.DataStore,
83+
}
84+
85+
var agg sequenceutils.OnChainOutput
86+
87+
for _, chainSelector := range selectors {
88+
seq, seqErr := SequenceForChainSelector(chainSelector)
89+
if seqErr != nil {
90+
return buildOutput(e, input.MCMS, agg, fmt.Errorf("chain selector %d: %w", chainSelector, seqErr))
91+
}
92+
93+
var mergeErr error
94+
agg, mergeErr = sequenceutils.ExecuteOnChainSequenceAndMerge(
95+
e.OperationsBundle,
96+
deps,
97+
seq,
98+
ChainInput{
99+
ChainSelector: chainSelector,
100+
MCMS: *input.MCMS,
101+
},
102+
agg,
103+
)
104+
if mergeErr != nil {
105+
return buildOutput(e, input.MCMS, agg, mergeErr)
106+
}
107+
}
108+
109+
return buildOutput(e, input.MCMS, agg, nil)
110+
}
111+
112+
func buildOutput(
113+
e cldf.Environment,
114+
mcmsInput *cldf.MCMSTimelockProposalInput,
115+
agg sequenceutils.OnChainOutput,
116+
err error,
117+
) (cldf.ChangesetOutput, error) {
118+
ds := cldfdatastore.NewMemoryDataStore()
119+
if e.DataStore != nil {
120+
if mergeErr := ds.Merge(e.DataStore); mergeErr != nil {
121+
return cldf.ChangesetOutput{}, fmt.Errorf("merge environment datastore: %w", mergeErr)
122+
}
123+
}
124+
if metaErr := ds.WriteMetadata(agg.Metadata); metaErr != nil {
125+
return cldf.ChangesetOutput{DataStore: ds},
126+
fmt.Errorf("write metadata to datastore: %w", metaErr)
127+
}
128+
129+
partialOutput := cldf.ChangesetOutput{DataStore: ds}
130+
if err != nil {
131+
return partialOutput, err
132+
}
133+
134+
builder := cldf.NewOutputBuilder(e, ds).
135+
WithTimelockProposal(*mcmsInput, agg.BatchOps)
136+
137+
out, buildErr := builder.Build()
138+
if buildErr != nil {
139+
return out, fmt.Errorf("build changeset output: %w", buildErr)
140+
}
141+
142+
if len(out.MCMSTimelockProposals) > 0 {
143+
e.Logger.Infow("MCMS fire drill proposal created", "proposalCount", len(out.MCMSTimelockProposals))
144+
}
145+
146+
return out, nil
147+
}
148+
149+
func resolvedSelectors(e cldf.Environment, selectors []uint64) []uint64 {
150+
if len(selectors) > 0 {
151+
return selectors
152+
}
153+
154+
solSelectors := e.BlockChains.ListChainSelectors(cldf_chain.WithFamily(chainselectors.FamilySolana))
155+
evmSelectors := e.BlockChains.ListChainSelectors(cldf_chain.WithFamily(chainselectors.FamilyEVM))
156+
out := make([]uint64, 0, len(solSelectors)+len(evmSelectors))
157+
out = append(out, solSelectors...)
158+
out = append(out, evmSelectors...)
159+
160+
return out
161+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package firedrill_test
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
chainselectors "github.com/smartcontractkit/chain-selectors"
8+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
9+
cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"
10+
cldftesthelpers "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils/testhelpers"
11+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment"
12+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime"
13+
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
14+
"github.com/smartcontractkit/mcms"
15+
mcmstypes "github.com/smartcontractkit/mcms/types"
16+
"github.com/stretchr/testify/require"
17+
18+
legacymcms "github.com/smartcontractkit/cld-changesets/legacy/mcms/changesets"
19+
firedrill "github.com/smartcontractkit/cld-changesets/mcms/changesets/firedrill"
20+
21+
_ "github.com/smartcontractkit/cld-changesets/mcms/changesets/firedrill/all"
22+
)
23+
24+
func TestChangeset_Apply_evmProposal(t *testing.T) {
25+
t.Parallel()
26+
27+
selector := chainselectors.TEST_90000001.Selector
28+
rt := newEVMFireDrillRuntime(t, selector)
29+
30+
err := rt.Exec(runtime.ChangesetTask(firedrill.Changeset{}, firedrill.Input{
31+
MCMS: &cldf.MCMSTimelockProposalInput{
32+
TimelockAction: mcmstypes.TimelockActionSchedule,
33+
ValidUntil: uint32(time.Now().Add(2 * time.Hour).UTC().Unix()), //nolint:gosec // test timestamp
34+
TimelockDelay: mcmstypes.NewDuration(time.Second),
35+
Description: "firedrill integration test",
36+
},
37+
Cfg: firedrill.Config{Selectors: []uint64{selector}},
38+
}))
39+
require.NoError(t, err)
40+
41+
var proposal mcms.TimelockProposal
42+
var foundProposal bool
43+
for _, out := range rt.State().Outputs {
44+
if len(out.MCMSTimelockProposals) > 0 {
45+
proposal = out.MCMSTimelockProposals[0]
46+
foundProposal = true
47+
}
48+
}
49+
require.True(t, foundProposal, "expected one MCMS timelock proposal")
50+
51+
require.Equal(t, "firedrill integration test", proposal.Description)
52+
require.Len(t, proposal.Operations, 1)
53+
require.Equal(t, mcmstypes.ChainSelector(selector), proposal.Operations[0].ChainSelector)
54+
require.Len(t, proposal.Operations[0].Transactions, 1)
55+
require.Equal(t, "FireDrillNoop", proposal.Operations[0].Transactions[0].ContractType)
56+
}
57+
58+
func newEVMFireDrillRuntime(t *testing.T, selector uint64) *runtime.Runtime {
59+
t.Helper()
60+
61+
rt, err := runtime.New(t.Context(), runtime.WithEnvOpts(
62+
environment.WithEVMSimulated(t, []uint64{selector}),
63+
environment.WithLogger(logger.Test(t)),
64+
))
65+
require.NoError(t, err)
66+
67+
err = rt.Exec(
68+
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(legacymcms.DeployMCMSWithTimelockV2), map[uint64]cldfproposalutils.MCMSWithTimelockConfig{
69+
selector: cldftesthelpers.SingleGroupTimelockConfig(t),
70+
}),
71+
)
72+
require.NoError(t, err)
73+
74+
return rt
75+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package firedrill_test
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
chainselectors "github.com/smartcontractkit/chain-selectors"
8+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
9+
cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils"
10+
cldftesthelpers "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils/testhelpers"
11+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/environment"
12+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/test/runtime"
13+
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
14+
"github.com/smartcontractkit/mcms"
15+
mcmstypes "github.com/smartcontractkit/mcms/types"
16+
"github.com/stretchr/testify/require"
17+
18+
legacymcms "github.com/smartcontractkit/cld-changesets/legacy/mcms/changesets"
19+
soltestutils "github.com/smartcontractkit/cld-changesets/legacy/pkg/family/solana/testutils"
20+
firedrill "github.com/smartcontractkit/cld-changesets/mcms/changesets/firedrill"
21+
22+
_ "github.com/smartcontractkit/cld-changesets/mcms/changesets/firedrill/all"
23+
)
24+
25+
//nolint:paralleltest // global mcm.SetProgramID state; serialized via soltestutils.PreloadMCMS lock
26+
func TestChangeset_Apply_solanaProposal(t *testing.T) {
27+
selector := chainselectors.TEST_22222222222222222222222222222222222222222222.Selector
28+
rt := newSolanaFireDrillRuntime(t, selector)
29+
30+
err := rt.Exec(runtime.ChangesetTask(firedrill.Changeset{}, firedrill.Input{
31+
MCMS: &cldf.MCMSTimelockProposalInput{
32+
TimelockAction: mcmstypes.TimelockActionSchedule,
33+
ValidUntil: uint32(time.Now().Add(2 * time.Hour).UTC().Unix()), //nolint:gosec // test timestamp
34+
TimelockDelay: mcmstypes.NewDuration(time.Second),
35+
Description: "firedrill solana integration test",
36+
},
37+
Cfg: firedrill.Config{Selectors: []uint64{selector}},
38+
}))
39+
require.NoError(t, err)
40+
41+
var proposal mcms.TimelockProposal
42+
var foundProposal bool
43+
for _, out := range rt.State().Outputs {
44+
if len(out.MCMSTimelockProposals) > 0 {
45+
proposal = out.MCMSTimelockProposals[0]
46+
foundProposal = true
47+
}
48+
}
49+
require.True(t, foundProposal, "expected one MCMS timelock proposal")
50+
51+
require.Equal(t, "firedrill solana integration test", proposal.Description)
52+
require.Len(t, proposal.Operations, 1)
53+
require.Equal(t, mcmstypes.ChainSelector(selector), proposal.Operations[0].ChainSelector)
54+
require.Len(t, proposal.Operations[0].Transactions, 1)
55+
require.Equal(t, "Memo", proposal.Operations[0].Transactions[0].ContractType)
56+
}
57+
58+
func newSolanaFireDrillRuntime(t *testing.T, selector uint64) *runtime.Runtime {
59+
t.Helper()
60+
61+
programsPath, programIDs, ab := soltestutils.PreloadMCMS(t, selector)
62+
rt, err := runtime.New(t.Context(), runtime.WithEnvOpts(
63+
environment.WithSolanaContainer(t, []uint64{selector}, programsPath, programIDs),
64+
environment.WithAddressBook(ab),
65+
environment.WithLogger(logger.Test(t)),
66+
))
67+
require.NoError(t, err)
68+
require.Contains(t, rt.Environment().BlockChains.SolanaChains(), selector)
69+
70+
err = rt.Exec(
71+
runtime.ChangesetTask(cldf.CreateLegacyChangeSet(legacymcms.DeployMCMSWithTimelockV2), map[uint64]cldfproposalutils.MCMSWithTimelockConfig{
72+
selector: cldftesthelpers.SingleGroupTimelockConfig(t),
73+
}),
74+
)
75+
require.NoError(t, err)
76+
77+
return rt
78+
}

0 commit comments

Comments
 (0)