Skip to content

Commit 3791c84

Browse files
feat(mcms): log "from", "to" and "raw data" send on-chain during fork tests (#655)
As requested by the payments team, log the "from", "to" and "data" fields sent on-chain during a fork test. The PR also adds the first, basic integration test for the fork test feature. --- OPT-349
1 parent a8928d5 commit 3791c84

File tree

36 files changed

+1946
-196
lines changed

36 files changed

+1946
-196
lines changed

.changeset/public-mugs-agree.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": patch
3+
---
4+
5+
feat: log from, to and raw data in forktests

chain/evm/provider/ctf_anvil_provider.go

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ type CTFAnvilChainProvider struct {
339339

340340
chain *evm.Chain
341341
httpURL string
342-
container testcontainers.Container
342+
Container testcontainers.Container
343343
}
344344

345345
// NewCTFAnvilChainProvider creates a new CTFAnvilChainProvider with the given selector and
@@ -370,23 +370,23 @@ func (p *CTFAnvilChainProvider) Initialize(ctx context.Context) (chain.BlockChai
370370

371371
err := p.config.validate()
372372
if err != nil {
373-
return nil, err
373+
return nil, fmt.Errorf("failed to validate config: %w", err)
374374
}
375375

376376
chainID, err := chainsel.GetChainIDFromSelector(p.selector)
377377
if err != nil {
378-
return nil, err
378+
return nil, fmt.Errorf("failed to get chain id from selector: %w", err)
379379
}
380380

381381
httpURL, err := p.startContainer(ctx, chainID)
382382
if err != nil {
383-
return nil, err
383+
return nil, fmt.Errorf("failed to start container: %w", err)
384384
}
385385
p.httpURL = httpURL
386386

387387
lggr, err := logger.New()
388388
if err != nil {
389-
return nil, err
389+
return nil, fmt.Errorf("failed to create new logger: %w", err)
390390
}
391391

392392
client, err := rpcclient.NewMultiClient(lggr, rpcclient.RPCConfig{
@@ -418,22 +418,22 @@ func (p *CTFAnvilChainProvider) Initialize(ctx context.Context) (chain.BlockChai
418418
// Use custom deployer transactor generator
419419
deployerKey, err = p.config.DeployerTransactorGen.Generate(chainIDBigInt)
420420
if err != nil {
421-
return nil, err
421+
return nil, fmt.Errorf("failed to generate deployer transactor: %w", err)
422422
}
423423

424424
signHashFunc = func(hash []byte) ([]byte, error) {
425425
return p.config.DeployerTransactorGen.SignHash(hash)
426426
}
427427
} else {
428428
// Use default Anvil deployer account
429-
deployerPrivateKey, parseErr := crypto.HexToECDSA(anvilTestPrivateKeys[0])
430-
if parseErr != nil {
431-
return nil, parseErr
429+
deployerPrivateKey, perr := crypto.HexToECDSA(anvilTestPrivateKeys[0])
430+
if perr != nil {
431+
return nil, fmt.Errorf("failed to parse anvil test private key: %w", perr)
432432
}
433433

434-
deployerKey, err = bind.NewKeyedTransactorWithChainID(deployerPrivateKey, chainIDBigInt)
435-
if err != nil {
436-
return nil, err
434+
deployerKey, perr = bind.NewKeyedTransactorWithChainID(deployerPrivateKey, chainIDBigInt)
435+
if perr != nil {
436+
return nil, fmt.Errorf("failed to create eth transactor: %w", perr)
437437
}
438438

439439
signHashFunc = func(hash []byte) ([]byte, error) {
@@ -449,14 +449,14 @@ func (p *CTFAnvilChainProvider) Initialize(ctx context.Context) (chain.BlockChai
449449
// Build additional user transactors from the default Anvil accounts
450450
userTransactors, err := p.getUserTransactors(chainID)
451451
if err != nil {
452-
return nil, err
452+
return nil, fmt.Errorf("failed to get user transactors: %w", err)
453453
}
454454

455455
confirmFunc, err := p.config.ConfirmFunctor.Generate(
456456
ctx, p.selector, client, deployerKey.From,
457457
)
458458
if err != nil {
459-
return nil, err
459+
return nil, fmt.Errorf("failed to generate confirm function: %w", err)
460460
}
461461

462462
p.chain = &evm.Chain{
@@ -515,12 +515,12 @@ func (p *CTFAnvilChainProvider) GetNodeHTTPURL() string {
515515
//
516516
// Returns an error if the container termination fails.
517517
func (p *CTFAnvilChainProvider) Cleanup(ctx context.Context) error {
518-
if p.container != nil {
519-
err := p.container.Terminate(ctx)
518+
if p.Container != nil {
519+
err := p.Container.Terminate(ctx)
520520
if err != nil {
521521
return fmt.Errorf("failed to terminate Anvil container: %w", err)
522522
}
523-
p.container = nil // Clear the reference after successful termination
523+
p.Container = nil // Clear the reference after successful termination
524524
}
525525

526526
return nil
@@ -537,9 +537,7 @@ func (p *CTFAnvilChainProvider) Cleanup(ctx context.Context) error {
537537
//
538538
// Returns the external HTTP URL that can be used to connect to the Anvil node.
539539
func (p *CTFAnvilChainProvider) startContainer(ctx context.Context, chainID string) (string, error) {
540-
var (
541-
attempts = uint(10)
542-
)
540+
attempts := uint(10)
543541

544542
err := framework.DefaultNetwork(p.config.Once)
545543
if err != nil {
@@ -587,7 +585,7 @@ func (p *CTFAnvilChainProvider) startContainer(ctx context.Context, chainID stri
587585
}
588586

589587
// Store container reference for manual cleanup
590-
p.container = output.Container
588+
p.Container = output.Container
591589

592590
// Only register cleanup if T is available (for test cleanup)
593591
if p.config.T != nil {

chain/evm/provider/ctf_anvil_provider_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ func TestCTFAnvilChainProvider_Cleanup(t *testing.T) {
452452
require.NoError(t, err, "Cleanup should succeed even when no container exists")
453453

454454
// Verify container is still nil
455-
assert.Nil(t, provider.container, "Container reference should remain nil")
455+
assert.Nil(t, provider.Container, "Container reference should remain nil")
456456

457457
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute)
458458
defer cancel()
@@ -461,12 +461,12 @@ func TestCTFAnvilChainProvider_Cleanup(t *testing.T) {
461461
require.NoError(t, err)
462462
require.NotNil(t, blockchain)
463463

464-
assert.NotNil(t, provider.container, "Container reference should be stored after initialization")
464+
assert.NotNil(t, provider.Container, "Container reference should be stored after initialization")
465465

466466
err = provider.Cleanup(ctx)
467467
require.NoError(t, err, "Cleanup should succeed")
468468

469-
assert.Nil(t, provider.container, "Container reference should be cleared after cleanup")
469+
assert.Nil(t, provider.Container, "Container reference should be cleared after cleanup")
470470
})
471471

472472
t.Run("cleanup after initialization with fixed port (T=nil)", func(t *testing.T) {
@@ -504,16 +504,16 @@ func TestCTFAnvilChainProvider_Cleanup(t *testing.T) {
504504
require.NoError(t, err)
505505
require.NotNil(t, blockchain)
506506

507-
assert.NotNil(t, provider.container, "Container reference should be stored after initialization")
507+
assert.NotNil(t, provider.Container, "Container reference should be stored after initialization")
508508

509509
err = provider.Cleanup(ctx)
510510
require.NoError(t, err, "Cleanup should succeed even when T is nil")
511511

512-
assert.Nil(t, provider.container, "Container reference should be cleared after cleanup")
512+
assert.Nil(t, provider.Container, "Container reference should be cleared after cleanup")
513513

514514
// Second cleanup - should be a no-op
515515
err = provider.Cleanup(ctx)
516516
require.NoError(t, err, "Second cleanup should succeed (no-op)")
517-
assert.Nil(t, provider.container, "Container reference should remain nil after second cleanup")
517+
assert.Nil(t, provider.Container, "Container reference should remain nil after second cleanup")
518518
})
519519
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package mcmsv2
2+
3+
import (
4+
"context"
5+
"crypto/ecdsa"
6+
"errors"
7+
"fmt"
8+
"maps"
9+
"time"
10+
11+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
12+
"github.com/ethereum/go-ethereum/common"
13+
gethtypes "github.com/ethereum/go-ethereum/core/types"
14+
"github.com/ethereum/go-ethereum/crypto"
15+
chainsel "github.com/smartcontractkit/chain-selectors"
16+
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
17+
"github.com/smartcontractkit/chainlink-testing-framework/framework/rpc"
18+
"github.com/smartcontractkit/mcms"
19+
mcmssdk "github.com/smartcontractkit/mcms/sdk"
20+
mcmstypes "github.com/smartcontractkit/mcms/types"
21+
22+
"github.com/smartcontractkit/chainlink-deployments-framework/chain"
23+
cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
24+
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/legacy/cli/mcmsv2/layout"
25+
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
26+
)
27+
28+
func executeFork(
29+
ctx context.Context, lggr logger.Logger, cfg *cfgv2, testSigner bool,
30+
) error {
31+
family, err := chainsel.GetSelectorFamily(cfg.chainSelector)
32+
if err != nil {
33+
return fmt.Errorf("failed to get selector family: %w", err)
34+
}
35+
if family != chainsel.FamilyEVM {
36+
lggr.Infof("Skipping fork execution: chain selector %d is not EVM. Family is %s", cfg.chainSelector, family)
37+
return nil // don’t fail, just exit cleanly
38+
}
39+
40+
logTransactions(lggr, cfg)
41+
42+
if len(cfg.forkedEnv.ChainConfigs[cfg.chainSelector].HTTPRPCs) == 0 {
43+
return fmt.Errorf("no rpcs loaded in forked environment for chain %d (fork tests require public RPCs)", cfg.chainSelector)
44+
}
45+
46+
// get the chain URL, chain ID and MCM contract address
47+
url := cfg.forkedEnv.ChainConfigs[cfg.chainSelector].HTTPRPCs[0].External
48+
anvilClient := rpc.New(url, nil)
49+
chainID := cfg.forkedEnv.ChainConfigs[cfg.chainSelector].ChainID
50+
mcmAddress := cfg.proposal.ChainMetadata[mcmstypes.ChainSelector(cfg.chainSelector)].MCMAddress
51+
timelockAddress := common.HexToAddress(cfg.timelockProposal.TimelockAddresses[mcmstypes.ChainSelector(cfg.chainSelector)])
52+
53+
ctx, cancel := context.WithTimeout(ctx, 300*time.Second)
54+
defer cancel()
55+
if testSigner {
56+
if lerr := layout.SetMCMSigner(
57+
ctx,
58+
lggr,
59+
layout.MCMSLayout,
60+
blockchain.DefaultAnvilPrivateKey,
61+
blockchain.DefaultAnvilPublicKey,
62+
blockchain.DefaultAnvilPublicKey,
63+
url,
64+
chainID,
65+
mcmAddress,
66+
); lerr != nil {
67+
return fmt.Errorf("failed to set signer: %w", lerr)
68+
}
69+
70+
// Override signatures for proposal
71+
privKey, lerr := crypto.HexToECDSA(blockchain.DefaultAnvilPrivateKey)
72+
if lerr != nil {
73+
return fmt.Errorf("failed to parse anvil's default private key: %w", lerr)
74+
}
75+
76+
lerr = overwriteProposalSignatureWithTestKey(ctx, cfg, privKey)
77+
if lerr != nil {
78+
return fmt.Errorf("failed to overwrite proposal signature: %w", lerr)
79+
}
80+
}
81+
82+
// set root
83+
// TODO: improve error decoding on the mcms lib for "set root".
84+
err = setRootCommand(ctx, lggr, cfg)
85+
if err != nil {
86+
return fmt.Errorf("MCM.setRoot() - failure: %w", err)
87+
}
88+
lggr.Info("MCM.setRoot() - success")
89+
90+
// TODO: improve error decoding on the mcms lib for "execute chain".
91+
err = executeChainCommand(ctx, lggr, cfg, true)
92+
if err != nil {
93+
return fmt.Errorf("MCM.execute() - failure: %w", err)
94+
}
95+
lggr.Info("MCM.execute() - success")
96+
97+
lggr.Info("Wait for the chain to be mined before executing timelock chain command")
98+
if err = anvilClient.EVMIncreaseTime(uint64(cfg.timelockProposal.Delay.Seconds())); err != nil {
99+
return fmt.Errorf("failed to increase time: %w", err)
100+
}
101+
if err = anvilClient.AnvilMine([]interface{}{1}); err != nil {
102+
return fmt.Errorf("failed to mine block: %w", err)
103+
}
104+
105+
if cfg.timelockProposal.Action != mcmstypes.TimelockActionSchedule {
106+
lggr.Infof("Proposal has type %s, skipping executing timelock chain command", cfg.timelockProposal.Action)
107+
return nil
108+
}
109+
110+
lggr.Info("Executing timelock chain command")
111+
err = timelockExecuteChainCommand(ctx, lggr, cfg)
112+
if err != nil {
113+
lggr.Warnw("Timelock.execute() - failure; starting calling individual ops for debugging", "err", err)
114+
if derr := diagnoseTimelockRevert(ctx, lggr, anvilClient.URL, cfg.chainSelector, cfg.timelockProposal.Operations,
115+
timelockAddress, cfg.env.ExistingAddresses, cfg.proposalCtx); derr != nil { //nolint:staticcheck
116+
lggr.Errorw("Diagnosis results", "err", derr)
117+
return fmt.Errorf("failed to timelock execute chain: %w", derr)
118+
}
119+
120+
return fmt.Errorf("failed to timelock execute chain: %w", err)
121+
}
122+
lggr.Info("Timelock.execute() - success")
123+
124+
return nil
125+
}
126+
127+
// --- helper types and functions ---
128+
129+
func logTransactions(lggr logger.Logger, cfg *cfgv2) {
130+
lggr.Infof("logging transactions sent to forked chain %v", cfg.chainSelector)
131+
132+
chains := maps.Collect(cfg.blockchains.All())
133+
134+
evmChain, ok := chains[cfg.chainSelector].(cldf_evm.Chain)
135+
if !ok {
136+
lggr.Warnf("failed to configure transaction logging for chain selector %v (not evm: %T)", cfg.chainSelector, chains[cfg.chainSelector])
137+
return
138+
}
139+
140+
evmChain.Client = &loggingRpcClient{OnchainClient: evmChain.Client, txOpts: evmChain.DeployerKey, lggr: lggr}
141+
chains[cfg.chainSelector] = evmChain
142+
cfg.blockchains = chain.NewBlockChains(chains)
143+
}
144+
145+
// overwriteProposalSignatureWithTestKey overwrites the proposal's signature with a test key signature.
146+
func overwriteProposalSignatureWithTestKey(ctx context.Context, cfg *cfgv2, testKey *ecdsa.PrivateKey) error {
147+
p := &cfg.proposal
148+
149+
// Override the proposal fields that are used in the signing hash to ensure no errors occur related to those.
150+
if time.Unix(int64(p.ValidUntil), 0).Before(time.Now().Add(10 * time.Minute)) {
151+
p.ValidUntil = uint32(time.Now().Add(5 * time.Hour).Unix()) //nolint:gosec // G404: time-based validity is acceptable for test signatures
152+
}
153+
p.Signatures = nil
154+
p.OverridePreviousRoot = true
155+
156+
inspector, err := getInspectorFromChainSelector(*cfg)
157+
if err != nil {
158+
return fmt.Errorf("error getting inspector from chain selector: %w", err)
159+
}
160+
signable, errSignable := mcms.NewSignable(p, map[mcmstypes.ChainSelector]mcmssdk.Inspector{
161+
mcmstypes.ChainSelector(cfg.chainSelector): inspector,
162+
})
163+
if errSignable != nil {
164+
return fmt.Errorf("error creating signable: %w", errSignable)
165+
}
166+
167+
signature, err := signable.SignAndAppend(mcms.NewPrivateKeySigner(testKey))
168+
p.Signatures = []mcmstypes.Signature{signature}
169+
if err != nil {
170+
return fmt.Errorf("error creating signable: %w", err)
171+
}
172+
173+
quorumMet, err := signable.CheckQuorum(ctx, mcmstypes.ChainSelector(cfg.chainSelector))
174+
if err != nil {
175+
return fmt.Errorf("failed to check quorum: %w", err)
176+
}
177+
if !quorumMet {
178+
return errors.New("quorum not met")
179+
}
180+
181+
return nil
182+
}
183+
184+
type loggingRpcClient struct {
185+
cldf_evm.OnchainClient
186+
txOpts *bind.TransactOpts
187+
lggr logger.Logger
188+
}
189+
190+
func (c *loggingRpcClient) SendTransaction(ctx context.Context, tx *gethtypes.Transaction) error {
191+
c.lggr.Infow("sending on-chain transaction", "from", c.txOpts.From, "to", tx.To(), "value", tx.Value(),
192+
"data", common.Bytes2Hex(tx.Data()))
193+
194+
return c.OnchainClient.SendTransaction(ctx, tx)
195+
}

0 commit comments

Comments
 (0)