Commit c98fc846 authored by Joshua Gutow's avatar Joshua Gutow Committed by GitHub

Merge pull request #8474 from testinprod-io/tip/derivation-checker

op-chain-ops: Add derivation checker
parents 682c026d 1bfa1d13
package main
import (
"context"
"crypto/ecdsa"
"errors"
"fmt"
"math/big"
"math/rand"
"os"
"time"
clients2 "github.com/ethereum-optimism/optimism/op-chain-ops/clients"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-service/client"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-service/retry"
"github.com/ethereum-optimism/optimism/op-service/sources"
"github.com/ethereum-optimism/optimism/op-service/testutils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/mattn/go-isatty"
"github.com/urfave/cli/v2"
)
func main() {
log.Root().SetHandler(log.StreamHandler(os.Stderr, log.TerminalFormat(isatty.IsTerminal(os.Stderr.Fd()))))
app := cli.NewApp()
app.Name = "check-derivation"
app.Usage = "Optimism derivation checker"
app.Commands = []*cli.Command{
{
Name: "detect-l2-reorg",
Usage: "Detects unsafe block reorg",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "l2-rpc-url",
Value: "http://127.0.0.1:9545",
Usage: "L2 RPC URL",
EnvVars: []string{"L2_RPC_URL"},
},
&cli.DurationFlag{
Name: "polling-interval",
Value: time.Millisecond * 500,
Usage: "Polling interval",
},
},
Action: detectL2Reorg,
},
{
Name: "check-consolidation",
Usage: "Checks consolidation",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "l2-rpc-url",
Value: "http://127.0.0.1:9545",
Usage: "L2 RPC URL",
EnvVars: []string{"L2_RPC_URL"},
},
&cli.DurationFlag{
Name: "polling-interval",
Value: time.Millisecond * 1000,
Usage: "Polling interval",
},
&cli.StringFlag{
Name: "private-key",
Value: "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
Usage: "Private key for signing L2 transactions. " +
"Default: devnet pre-funded account",
},
&cli.IntFlag{
Name: "tx-count",
Value: 4,
Usage: "Number of transactions to send. Minimum value is 4 for checking every tx type.",
},
&cli.Uint64Flag{
Name: "l2-chain-id",
Value: 901,
Usage: "L2 chain ID",
},
},
Action: checkConsolidation,
},
}
if err := app.Run(os.Args); err != nil {
log.Crit("error checking l2", "err", err)
}
}
func newClientsFromContext(cliCtx *cli.Context) (*ethclient.Client, *sources.EthClient, error) {
clients, err := clients2.NewClientsFromContext(cliCtx)
if err != nil {
return nil, nil, err
}
ethClCfg := sources.EthClientConfig{
MaxRequestsPerBatch: 10,
MaxConcurrentRequests: 10,
ReceiptsCacheSize: 10,
TransactionsCacheSize: 10,
HeadersCacheSize: 10,
PayloadsCacheSize: 10,
TrustRPC: false,
MustBePostMerge: true,
RPCProviderKind: sources.RPCKindStandard,
MethodResetDuration: time.Minute,
}
cl := ethclient.NewClient(clients.L2RpcClient)
ethCl, err := sources.NewEthClient(client.NewBaseRPCClient(clients.L2RpcClient), log.Root(), nil, &ethClCfg)
if err != nil {
return nil, nil, err
}
return cl, ethCl, nil
}
func getHead(ctx context.Context, client *sources.EthClient, label eth.BlockLabel) (eth.BlockID, common.Hash, error) {
return retry.Do2(ctx, 10, &retry.FixedStrategy{Dur: 100 * time.Millisecond}, func() (eth.BlockID, common.Hash, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
blockInfo, err := client.InfoByLabel(ctx, label)
if err != nil {
return eth.BlockID{}, common.Hash{}, err
}
return eth.BlockID{Hash: blockInfo.Hash(), Number: blockInfo.NumberU64()}, blockInfo.ParentHash(), nil
})
}
func getUnsafeHead(ctx context.Context, client *sources.EthClient) (eth.BlockID, common.Hash, error) {
return getHead(ctx, client, eth.Unsafe)
}
func getSafeHead(ctx context.Context, client *sources.EthClient) (eth.BlockID, common.Hash, error) {
return getHead(ctx, client, eth.Safe)
}
func checkReorg(blockMap map[uint64]common.Hash, number uint64, hash common.Hash) {
prevHash, ok := blockMap[number]
if ok {
if prevHash != hash {
log.Error("Unsafe head reorg", "blockNum:", number,
"prevHash", prevHash.String(), "currHash", hash.String())
}
}
}
// detectL2Reorg polls safe heads and detects l2 unsafe block reorg.
func detectL2Reorg(cliCtx *cli.Context) error {
ctx := context.Background()
_, ethCl, err := newClientsFromContext(cliCtx)
if err != nil {
return err
}
var pollingInterval = cliCtx.Duration("polling-interval")
// blockMap maps blockNumber to blockHash
blockMap := make(map[uint64]common.Hash)
var prevUnsafeHeadNum uint64
for {
unsafeHeadBlockId, parentHash, err := getUnsafeHead(ctx, ethCl)
if err != nil {
return fmt.Errorf("failed to fetch unsafe head: %w", err)
}
checkReorg(blockMap, unsafeHeadBlockId.Number-1, parentHash)
checkReorg(blockMap, unsafeHeadBlockId.Number, unsafeHeadBlockId.Hash)
if unsafeHeadBlockId.Number > prevUnsafeHeadNum {
log.Info("Fetched Unsafe block", "blockNum", unsafeHeadBlockId.Number, "hash", unsafeHeadBlockId.Hash.String())
}
blockMap[unsafeHeadBlockId.Number-1] = parentHash
blockMap[unsafeHeadBlockId.Number] = unsafeHeadBlockId.Hash
prevUnsafeHeadNum = unsafeHeadBlockId.Number
time.Sleep(pollingInterval)
}
}
// getRandomAddress returns vanity address of the form 0x000000000000000000000000[random 32 bits][prefix]
// example: 0x00000000000000000000000030bd3402deadbeef
func getRandomAddress(rng *rand.Rand, prefix uint64) common.Address {
var vanity uint64 = prefix + (uint64(rng.Uint32()) << 32)
return common.HexToAddress(fmt.Sprintf("0x%X", vanity))
}
func getPrivateKey(cliCtx *cli.Context) (*ecdsa.PrivateKey, error) {
privateKey, err := crypto.HexToECDSA(cliCtx.String("private-key"))
if err != nil {
return nil, fmt.Errorf("invalid private key: %w", err)
}
return privateKey, nil
}
func getSenderAddress(privateKey *ecdsa.PrivateKey) (common.Address, error) {
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return common.Address{}, fmt.Errorf("error casting public key to ECDSA")
}
from := crypto.PubkeyToAddress(*publicKeyECDSA)
return from, nil
}
// getRandomSignedTransaction returns signed tx which sends 1 wei to random address
func getRandomSignedTransaction(ctx context.Context, ethClient *ethclient.Client, rng *rand.Rand, from common.Address, privateKey *ecdsa.PrivateKey, chainId *big.Int, txType int, protected bool) (*types.Transaction, error) {
randomAddress := getRandomAddress(rng, 0xDEADBEEF)
amount := big.NewInt(1)
nonce, err := ethClient.PendingNonceAt(ctx, from)
if err != nil {
return nil, fmt.Errorf("failed to get nonce: %w", err)
}
gasPrice, err := ethClient.SuggestGasPrice(ctx)
if err != nil {
return nil, fmt.Errorf("failed to suggest gas price: %w", err)
}
data := testutils.RandomData(rng, 10)
var txData types.TxData
switch txType {
case types.LegacyTxType:
gasLimit, err := core.IntrinsicGas(data, nil, false, true, true, false)
if err != nil {
return nil, fmt.Errorf("failed to get intrinsicGas: %w", err)
}
txData = &types.LegacyTx{
Nonce: nonce,
GasPrice: gasPrice,
Gas: gasLimit,
To: &randomAddress,
Value: amount,
Data: data,
}
case types.AccessListTxType:
accessList := types.AccessList{types.AccessTuple{
Address: randomAddress,
StorageKeys: []common.Hash{common.HexToHash("0x1234")},
}}
gasLimit, err := core.IntrinsicGas(data, accessList, false, true, true, false)
if err != nil {
return nil, fmt.Errorf("failed to get intrinsicGas: %w", err)
}
txData = &types.AccessListTx{
ChainID: chainId,
Nonce: nonce,
GasPrice: gasPrice,
Gas: gasLimit,
To: &randomAddress,
Value: amount,
AccessList: accessList,
Data: data,
}
case types.DynamicFeeTxType:
gasLimit, err := core.IntrinsicGas(data, nil, false, true, true, false)
if err != nil {
return nil, fmt.Errorf("failed to get intrinsicGas: %w", err)
}
gasTipCap, err := ethClient.SuggestGasTipCap(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get gas tip cap: %w", err)
}
txData = &types.DynamicFeeTx{
ChainID: chainId,
Nonce: nonce,
GasTipCap: gasTipCap,
GasFeeCap: gasPrice,
Gas: gasLimit,
To: &randomAddress,
Value: amount,
Data: data,
}
default:
return nil, fmt.Errorf("unsupported tx type: %d", txType)
}
tx := types.NewTx(txData)
signer := types.NewLondonSigner(chainId)
if !protected {
if txType == types.LegacyTxType {
signer = types.HomesteadSigner{}
} else {
return nil, errors.New("typed tx cannot be unprotected")
}
}
signedTx, err := types.SignTx(tx, signer, privateKey)
if err != nil {
return nil, fmt.Errorf("failed to sign transaction: %w", err)
}
return signedTx, nil
}
// confirmTransaction polls receipts to confirm transaction is included in the block.
func confirmTransaction(ctx context.Context, ethClient *ethclient.Client, l2BlockTime uint64, txHash common.Hash) (eth.BlockID, error) {
var retryCount uint64
for {
receipt, _ := ethClient.TransactionReceipt(ctx, txHash)
if retryCount > 30 {
return eth.BlockID{}, fmt.Errorf("transaction confirmation failure: txHash: %s", txHash.String())
}
if receipt == nil {
log.Info("Waiting for transaction receipt", "txHash", txHash.String())
retryCount++
// wait at least l2 block time
time.Sleep(time.Duration(l2BlockTime) * time.Second)
continue
}
block := eth.BlockID{
Hash: receipt.BlockHash,
Number: receipt.BlockNumber.Uint64(),
}
log.Info("Transaction receipt found", "block", block, "status", receipt.Status)
return block, nil
}
}
// checkConsolidation sends transactions and ensures them to be included in unsafe block.
// Then polls safe head to check unsafe blocks which includes sent tx are consolidated.
func checkConsolidation(cliCtx *cli.Context) error {
ctx := context.Background()
cl, ethCl, err := newClientsFromContext(cliCtx)
if err != nil {
return err
}
var pollingInterval = cliCtx.Duration("polling-interval")
privateKey, err := getPrivateKey(cliCtx)
if err != nil {
return err
}
from, err := getSenderAddress(privateKey)
if err != nil {
return err
}
txCount := cliCtx.Int("tx-count")
if txCount < 4 {
return fmt.Errorf("tx count %d is too low. requires minimum 4 txs to test all tx types", txCount)
}
l2ChainID := new(big.Int).SetUint64(cliCtx.Uint64("l2-chain-id"))
l2BlockTime := uint64(2)
rollupCfg, err := rollup.LoadOPStackRollupConfig(l2ChainID.Uint64())
if err == nil {
l2BlockTime = rollupCfg.BlockTime
} else {
log.Warn("Superchain config not loaded", "l2-chain-id", l2ChainID)
log.Warn("Using default config", "l2-block-time", l2BlockTime)
}
rng := rand.New(rand.NewSource(1337))
// txMap maps txHash to blockID
txMap := make(map[common.Hash]eth.BlockID)
// Submit random txs for each tx types
for i := 0; i < txCount; i++ {
txType := types.LegacyTxType
protected := true
// Generate all tx types alternately
switch i % 4 {
case 0:
protected = false // legacy unprotected TX (Homestead)
case 1:
// legacy protected TX
case 2:
txType = types.AccessListTxType
case 3:
txType = types.DynamicFeeTxType
}
tx, err := getRandomSignedTransaction(ctx, cl, rng, from, privateKey, l2ChainID, txType, protected)
if err != nil {
return err
}
err = cl.SendTransaction(ctx, tx)
if err != nil {
return fmt.Errorf("failed to send transaction: %w", err)
}
log.Info("Sent transaction", "txType", txType, "protected", protected)
txHash := tx.Hash()
blockId, err := confirmTransaction(ctx, cl, l2BlockTime, txHash)
if err != nil {
return err
}
txMap[txHash] = blockId
}
lastSafeHeadNum := uint64(0)
numChecked := 0
failed := false
for {
safeHeadBlockId, _, err := getSafeHead(ctx, ethCl)
if err != nil {
return fmt.Errorf("failed to fetch safe head: %w", err)
}
log.Info("Fetched Safe head", "blockNum", safeHeadBlockId.Number, "hash", safeHeadBlockId.Hash.String(), "remainingTxCount", txCount-numChecked)
for txHash, blockId := range txMap {
if lastSafeHeadNum < blockId.Number && safeHeadBlockId.Number >= blockId.Number {
safeBlockHash := safeHeadBlockId.Hash
if safeHeadBlockId.Number != blockId.Number {
safeBlock, err := retry.Do(ctx, 10, &retry.FixedStrategy{Dur: 100 * time.Millisecond}, func() (*types.Block, error) {
return cl.BlockByNumber(ctx, new(big.Int).SetUint64(blockId.Number))
})
if err != nil {
return fmt.Errorf("failed to fetch block by number: %w", err)
}
safeBlockHash = safeBlock.Hash()
}
if safeBlockHash == blockId.Hash {
log.Info("Transaction included at safe block", "block", blockId, "txHash", txHash.String())
} else {
log.Error("Transaction included block is reorged", "blockNum", blockId.Number, "prevHash", blockId.Hash, "currBlock", safeBlockHash, "txHash", txHash.String())
failed = true
}
numChecked++
}
}
if numChecked == txCount {
if failed {
log.Error("Failed")
} else {
log.Info("Succeeded")
}
break
}
lastSafeHeadNum = safeHeadBlockId.Number
time.Sleep(pollingInterval)
}
return nil
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment