|
| 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