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
5 changes: 5 additions & 0 deletions .changeset/public-mugs-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink-deployments-framework": patch
---

feat: log from, to and raw data in forktests
40 changes: 19 additions & 21 deletions chain/evm/provider/ctf_anvil_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ type CTFAnvilChainProvider struct {

chain *evm.Chain
httpURL string
container testcontainers.Container
Container testcontainers.Container
}

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

err := p.config.validate()
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to validate config: %w", err)
}

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

httpURL, err := p.startContainer(ctx, chainID)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to start container: %w", err)
}
p.httpURL = httpURL

lggr, err := logger.New()
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to create new logger: %w", err)
}

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

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

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

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

confirmFunc, err := p.config.ConfirmFunctor.Generate(
ctx, p.selector, client, deployerKey.From,
)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to generate confirm function: %w", err)
}

p.chain = &evm.Chain{
Expand Down Expand Up @@ -515,12 +515,12 @@ func (p *CTFAnvilChainProvider) GetNodeHTTPURL() string {
//
// Returns an error if the container termination fails.
func (p *CTFAnvilChainProvider) Cleanup(ctx context.Context) error {
if p.container != nil {
err := p.container.Terminate(ctx)
if p.Container != nil {
err := p.Container.Terminate(ctx)
if err != nil {
return fmt.Errorf("failed to terminate Anvil container: %w", err)
}
p.container = nil // Clear the reference after successful termination
p.Container = nil // Clear the reference after successful termination
}

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

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

// Store container reference for manual cleanup
p.container = output.Container
p.Container = output.Container

// Only register cleanup if T is available (for test cleanup)
if p.config.T != nil {
Expand Down
12 changes: 6 additions & 6 deletions chain/evm/provider/ctf_anvil_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ func TestCTFAnvilChainProvider_Cleanup(t *testing.T) {
require.NoError(t, err, "Cleanup should succeed even when no container exists")

// Verify container is still nil
assert.Nil(t, provider.container, "Container reference should remain nil")
assert.Nil(t, provider.Container, "Container reference should remain nil")

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

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

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

assert.Nil(t, provider.container, "Container reference should be cleared after cleanup")
assert.Nil(t, provider.Container, "Container reference should be cleared after cleanup")
})

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

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

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

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

// Second cleanup - should be a no-op
err = provider.Cleanup(ctx)
require.NoError(t, err, "Second cleanup should succeed (no-op)")
assert.Nil(t, provider.container, "Container reference should remain nil after second cleanup")
assert.Nil(t, provider.Container, "Container reference should remain nil after second cleanup")
})
}
195 changes: 195 additions & 0 deletions engine/cld/legacy/cli/mcmsv2/execute_fork.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package mcmsv2

import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"maps"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
gethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
chainsel "github.com/smartcontractkit/chain-selectors"
"github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain"
"github.com/smartcontractkit/chainlink-testing-framework/framework/rpc"
"github.com/smartcontractkit/mcms"
mcmssdk "github.com/smartcontractkit/mcms/sdk"
mcmstypes "github.com/smartcontractkit/mcms/types"

"github.com/smartcontractkit/chainlink-deployments-framework/chain"
cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm"
"github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/legacy/cli/mcmsv2/layout"
"github.com/smartcontractkit/chainlink-deployments-framework/pkg/logger"
)

func executeFork(
ctx context.Context, lggr logger.Logger, cfg *cfgv2, testSigner bool,
) error {
family, err := chainsel.GetSelectorFamily(cfg.chainSelector)
if err != nil {
return fmt.Errorf("failed to get selector family: %w", err)
}
if family != chainsel.FamilyEVM {
lggr.Infof("Skipping fork execution: chain selector %d is not EVM. Family is %s", cfg.chainSelector, family)
return nil // don’t fail, just exit cleanly
}

logTransactions(lggr, cfg)

if len(cfg.forkedEnv.ChainConfigs[cfg.chainSelector].HTTPRPCs) == 0 {
return fmt.Errorf("no rpcs loaded in forked environment for chain %d (fork tests require public RPCs)", cfg.chainSelector)
}

// get the chain URL, chain ID and MCM contract address
url := cfg.forkedEnv.ChainConfigs[cfg.chainSelector].HTTPRPCs[0].External
anvilClient := rpc.New(url, nil)
chainID := cfg.forkedEnv.ChainConfigs[cfg.chainSelector].ChainID
mcmAddress := cfg.proposal.ChainMetadata[mcmstypes.ChainSelector(cfg.chainSelector)].MCMAddress
timelockAddress := common.HexToAddress(cfg.timelockProposal.TimelockAddresses[mcmstypes.ChainSelector(cfg.chainSelector)])

ctx, cancel := context.WithTimeout(ctx, 300*time.Second)
defer cancel()
if testSigner {
if lerr := layout.SetMCMSigner(
ctx,
lggr,
layout.MCMSLayout,
blockchain.DefaultAnvilPrivateKey,
blockchain.DefaultAnvilPublicKey,
blockchain.DefaultAnvilPublicKey,
url,
chainID,
mcmAddress,
); lerr != nil {
return fmt.Errorf("failed to set signer: %w", lerr)
}

// Override signatures for proposal
privKey, lerr := crypto.HexToECDSA(blockchain.DefaultAnvilPrivateKey)
if lerr != nil {
return fmt.Errorf("failed to parse anvil's default private key: %w", lerr)
}

lerr = overwriteProposalSignatureWithTestKey(ctx, cfg, privKey)
if lerr != nil {
return fmt.Errorf("failed to overwrite proposal signature: %w", lerr)
}
}

// set root
// TODO: improve error decoding on the mcms lib for "set root".
err = setRootCommand(ctx, lggr, cfg)
if err != nil {
return fmt.Errorf("MCM.setRoot() - failure: %w", err)
}
lggr.Info("MCM.setRoot() - success")

// TODO: improve error decoding on the mcms lib for "execute chain".
err = executeChainCommand(ctx, lggr, cfg, true)
if err != nil {
return fmt.Errorf("MCM.execute() - failure: %w", err)
}
lggr.Info("MCM.execute() - success")

lggr.Info("Wait for the chain to be mined before executing timelock chain command")
if err = anvilClient.EVMIncreaseTime(uint64(cfg.timelockProposal.Delay.Seconds())); err != nil {
return fmt.Errorf("failed to increase time: %w", err)
}
if err = anvilClient.AnvilMine([]interface{}{1}); err != nil {
return fmt.Errorf("failed to mine block: %w", err)
}

if cfg.timelockProposal.Action != mcmstypes.TimelockActionSchedule {
lggr.Infof("Proposal has type %s, skipping executing timelock chain command", cfg.timelockProposal.Action)
return nil
}

lggr.Info("Executing timelock chain command")
err = timelockExecuteChainCommand(ctx, lggr, cfg)
if err != nil {
lggr.Warnw("Timelock.execute() - failure; starting calling individual ops for debugging", "err", err)
if derr := diagnoseTimelockRevert(ctx, lggr, anvilClient.URL, cfg.chainSelector, cfg.timelockProposal.Operations,
timelockAddress, cfg.env.ExistingAddresses, cfg.proposalCtx); derr != nil { //nolint:staticcheck
lggr.Errorw("Diagnosis results", "err", derr)
return fmt.Errorf("failed to timelock execute chain: %w", derr)
}

return fmt.Errorf("failed to timelock execute chain: %w", err)
}
lggr.Info("Timelock.execute() - success")

return nil
}

// --- helper types and functions ---

func logTransactions(lggr logger.Logger, cfg *cfgv2) {
lggr.Infof("logging transactions sent to forked chain %v", cfg.chainSelector)

chains := maps.Collect(cfg.blockchains.All())

evmChain, ok := chains[cfg.chainSelector].(cldf_evm.Chain)
if !ok {
lggr.Warnf("failed to configure transaction logging for chain selector %v (not evm: %T)", cfg.chainSelector, chains[cfg.chainSelector])
return
}

evmChain.Client = &loggingRpcClient{OnchainClient: evmChain.Client, txOpts: evmChain.DeployerKey, lggr: lggr}
chains[cfg.chainSelector] = evmChain
cfg.blockchains = chain.NewBlockChains(chains)
}

// overwriteProposalSignatureWithTestKey overwrites the proposal's signature with a test key signature.
func overwriteProposalSignatureWithTestKey(ctx context.Context, cfg *cfgv2, testKey *ecdsa.PrivateKey) error {
p := &cfg.proposal

// Override the proposal fields that are used in the signing hash to ensure no errors occur related to those.
if time.Unix(int64(p.ValidUntil), 0).Before(time.Now().Add(10 * time.Minute)) {
p.ValidUntil = uint32(time.Now().Add(5 * time.Hour).Unix()) //nolint:gosec // G404: time-based validity is acceptable for test signatures
}
p.Signatures = nil
p.OverridePreviousRoot = true

inspector, err := getInspectorFromChainSelector(*cfg)
if err != nil {
return fmt.Errorf("error getting inspector from chain selector: %w", err)
}
signable, errSignable := mcms.NewSignable(p, map[mcmstypes.ChainSelector]mcmssdk.Inspector{
mcmstypes.ChainSelector(cfg.chainSelector): inspector,
})
if errSignable != nil {
return fmt.Errorf("error creating signable: %w", errSignable)
}

signature, err := signable.SignAndAppend(mcms.NewPrivateKeySigner(testKey))
p.Signatures = []mcmstypes.Signature{signature}
if err != nil {
return fmt.Errorf("error creating signable: %w", err)
}

quorumMet, err := signable.CheckQuorum(ctx, mcmstypes.ChainSelector(cfg.chainSelector))
if err != nil {
return fmt.Errorf("failed to check quorum: %w", err)
}
if !quorumMet {
return errors.New("quorum not met")
}

return nil
}

type loggingRpcClient struct {
cldf_evm.OnchainClient
txOpts *bind.TransactOpts
lggr logger.Logger
}

func (c *loggingRpcClient) SendTransaction(ctx context.Context, tx *gethtypes.Transaction) error {
c.lggr.Infow("sending on-chain transaction", "from", c.txOpts.From, "to", tx.To(), "value", tx.Value(),
"data", common.Bytes2Hex(tx.Data()))

return c.OnchainClient.SendTransaction(ctx, tx)
}
Loading