From 37bb1345601c4034059c1e65bf91846780e48b71 Mon Sep 17 00:00:00 2001 From: Mark Tyneway Date: Thu, 9 Nov 2023 22:43:34 +0300 Subject: [PATCH] op-node,contracts-bedrock: setup `loadAllocs` usage Modularize the `op-node` command for creating L2 genesis blocks such that it can read a starting L1 block from disk instead of needing to fetch it from a node. This makes the process more simple and reproducible, because the block JSON file can be written to disk and committed into a repo. Also add the script for calling the `op-node` L2 genesis generation to `contracts-bedrock`. It will create the L2 genesis state in `contracts-bedrock/.testdata` which will be read by `vm.readAllocs(string)` to populate the initial state. --- op-node/cmd/genesis/cmd.go | 241 ++++++++++++------ packages/contracts-bedrock/.gitignore | 4 +- .../scripts/generate-l2-genesis.sh | 38 +++ .../contracts-bedrock/test/mocks/block.json | 27 ++ 4 files changed, 238 insertions(+), 72 deletions(-) create mode 100755 packages/contracts-bedrock/scripts/generate-l2-genesis.sh create mode 100644 packages/contracts-bedrock/test/mocks/block.json diff --git a/op-node/cmd/genesis/cmd.go b/op-node/cmd/genesis/cmd.go index 2bbb8e70567f..ae05c887c992 100644 --- a/op-node/cmd/genesis/cmd.go +++ b/op-node/cmd/genesis/cmd.go @@ -11,6 +11,7 @@ import ( "github.com/urfave/cli/v2" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" @@ -21,29 +22,69 @@ import ( "github.com/ethereum-optimism/optimism/op-chain-ops/genesis" ) +var ( + l1RPCFlag = &cli.StringFlag{ + Name: "l1-rpc", + Usage: "RPC URL for an Ethereum L1 node. Cannot be used with --l1-starting-block", + } + l1StartingBlockFlag = &cli.PathFlag{ + Name: "l1-starting-block", + Usage: "Path to a JSON file containing the L1 starting block. Overrides the need for using an L1 RPC to fetch the block. Cannot be used with --l1-rpc", + } + deployConfigFlag = &cli.PathFlag{ + Name: "deploy-config", + Usage: "Path to deploy config file", + Required: true, + } + deploymentDirFlag = &cli.PathFlag{ + Name: "deployment-dir", + Usage: "Path to network deployment directory. Cannot be used with --l1-deployments", + } + l1DeploymentsFlag = &cli.PathFlag{ + Name: "l1-deployments", + Usage: "Path to L1 deployments JSON file. Cannot be used with --deployment-dir", + } + outfileL2Flag = &cli.PathFlag{ + Name: "outfile.l2", + Usage: "Path to L2 genesis output file", + } + outfileRollupFlag = &cli.PathFlag{ + Name: "outfile.rollup", + Usage: "Path to rollup output file", + } + + l1AllocsFlag = &cli.StringFlag{ + Name: "l1-allocs", + Usage: "Path to L1 genesis state dump", + } + outfileL1Flag = &cli.StringFlag{ + Name: "outfile.l1", + Usage: "Path to L1 genesis output file", + } + + l1Flags = []cli.Flag{ + deployConfigFlag, + l1AllocsFlag, + l1DeploymentsFlag, + outfileL1Flag, + } + + l2Flags = []cli.Flag{ + l1RPCFlag, + l1StartingBlockFlag, + deployConfigFlag, + deploymentDirFlag, + l1DeploymentsFlag, + outfileL2Flag, + outfileRollupFlag, + } +) + var Subcommands = cli.Commands{ { Name: "l1", Usage: "Generates a L1 genesis state file", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "deploy-config", - Usage: "Path to deploy config file", - Required: true, - }, - &cli.StringFlag{ - Name: "l1-allocs", - Usage: "Path to L1 genesis state dump", - }, - &cli.StringFlag{ - Name: "l1-deployments", - Usage: "Path to L1 deployments file", - }, - &cli.StringFlag{ - Name: "outfile.l1", - Usage: "Path to L1 genesis output file", - }, - }, + Flags: l1Flags, Action: func(ctx *cli.Context) error { deployConfig := ctx.String("deploy-config") config, err := genesis.NewDeployConfig(deployConfig) @@ -85,7 +126,7 @@ var Subcommands = cli.Commands{ return err } - return writeGenesisFile(ctx.String("outfile.l1"), l1Genesis) + return writeJSONFile(ctx.String("outfile.l1"), l1Genesis) }, }, { @@ -93,44 +134,20 @@ var Subcommands = cli.Commands{ Usage: "Generates an L2 genesis file and rollup config suitable for a deployed network", Description: "Generating the L2 genesis depends on knowledge of L1 contract addresses for the bridge to be secure. " + "A deploy config and either a deployment directory or an L1 deployments file are used to create the L2 genesis. " + - "The deploy directory and L1 deployments file are generated by the L1 contract deployments.", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "l1-rpc", - Usage: "L1 RPC URL", - }, - &cli.StringFlag{ - Name: "deploy-config", - Usage: "Path to deploy config file", - Required: true, - }, - &cli.StringFlag{ - Name: "deployment-dir", - Usage: "Path to network deployment directory. Cannot be used with --l1-deployments", - }, - &cli.StringFlag{ - Name: "l1-deployments", - Usage: "Path to L1 deployments JSON file. Cannot be used with --deployment-dir", - }, - &cli.StringFlag{ - Name: "outfile.l2", - Usage: "Path to L2 genesis output file", - }, - &cli.StringFlag{ - Name: "outfile.rollup", - Usage: "Path to rollup output file", - }, - }, + "The deploy directory and L1 deployments file are generated by the L1 contract deployments. " + + "An L1 starting block is necessary, it can either be fetched dynamically using config in the deploy config " + + "or it can be provided as a JSON file.", + Flags: l2Flags, Action: func(ctx *cli.Context) error { - deployConfig := ctx.String("deploy-config") + deployConfig := ctx.Path("deploy-config") log.Info("Deploy config", "path", deployConfig) config, err := genesis.NewDeployConfig(deployConfig) if err != nil { return err } - deployDir := ctx.String("deployment-dir") - l1Deployments := ctx.String("l1-deployments") + deployDir := ctx.Path("deployment-dir") + l1Deployments := ctx.Path("l1-deployments") if deployDir != "" && l1Deployments != "" { return errors.New("cannot specify both --deployment-dir and --l1-deployments") @@ -139,6 +156,16 @@ var Subcommands = cli.Commands{ return errors.New("must specify either --deployment-dir or --l1-deployments") } + l1StartBlockPath := ctx.Path("l1-starting-block") + l1RPC := ctx.String("l1-rpc") + + if l1StartBlockPath == "" && l1RPC == "" { + return errors.New("must specify either --l1-starting-block or --l1-rpc") + } + if l1StartBlockPath != "" && l1RPC != "" { + return errors.New("cannot specify both --l1-starting-block and --l1-rpc") + } + if deployDir != "" { log.Info("Deployment directory", "path", deployDir) depPath, network := filepath.Split(deployDir) @@ -154,31 +181,49 @@ var Subcommands = cli.Commands{ } if l1Deployments != "" { - log.Info("L1 deployments", "path", l1Deployments) deployments, err := genesis.NewL1Deployments(l1Deployments) if err != nil { - return err + return fmt.Errorf("cannot read L1 deployments at %s: %w", l1Deployments, err) } config.SetDeployments(deployments) } - client, err := ethclient.Dial(ctx.String("l1-rpc")) - if err != nil { - return fmt.Errorf("cannot dial %s: %w", ctx.String("l1-rpc"), err) + var l1StartBlock *types.Block + if l1StartBlockPath != "" { + if l1StartBlock, err = readBlockJSON(l1StartBlockPath); err != nil { + return fmt.Errorf("cannot read L1 starting block at %s: %w", l1StartBlockPath, err) + } } - var l1StartBlock *types.Block - if config.L1StartingBlockTag == nil { - l1StartBlock, err = client.BlockByNumber(context.Background(), nil) - tag := rpc.BlockNumberOrHashWithHash(l1StartBlock.Hash(), true) - config.L1StartingBlockTag = (*genesis.MarshalableRPCBlockNumberOrHash)(&tag) - } else if config.L1StartingBlockTag.BlockHash != nil { - l1StartBlock, err = client.BlockByHash(context.Background(), *config.L1StartingBlockTag.BlockHash) - } else if config.L1StartingBlockTag.BlockNumber != nil { - l1StartBlock, err = client.BlockByNumber(context.Background(), big.NewInt(config.L1StartingBlockTag.BlockNumber.Int64())) + if l1RPC != "" { + client, err := ethclient.Dial(ctx.String("l1-rpc")) + if err != nil { + return fmt.Errorf("cannot dial %s: %w", l1RPC, err) + } + + if config.L1StartingBlockTag == nil { + l1StartBlock, err = client.BlockByNumber(context.Background(), nil) + if err != nil { + return fmt.Errorf("%w", err) + } + tag := rpc.BlockNumberOrHashWithHash(l1StartBlock.Hash(), true) + config.L1StartingBlockTag = (*genesis.MarshalableRPCBlockNumberOrHash)(&tag) + } else if config.L1StartingBlockTag.BlockHash != nil { + l1StartBlock, err = client.BlockByHash(context.Background(), *config.L1StartingBlockTag.BlockHash) + if err != nil { + return fmt.Errorf("%w", err) + } + } else if config.L1StartingBlockTag.BlockNumber != nil { + l1StartBlock, err = client.BlockByNumber(context.Background(), big.NewInt(config.L1StartingBlockTag.BlockNumber.Int64())) + if err != nil { + return fmt.Errorf("%w", err) + } + } } - if err != nil { - return fmt.Errorf("error getting l1 start block: %w", err) + + // Ensure that there is a starting L1 block + if l1StartBlock == nil { + return errors.New("no starting L1 block") } // Sanity check the config. Do this after filling in the L1StartingBlockTag @@ -204,16 +249,18 @@ var Subcommands = cli.Commands{ return fmt.Errorf("generated rollup config does not pass validation: %w", err) } - if err := writeGenesisFile(ctx.String("outfile.l2"), l2Genesis); err != nil { + if err := writeJSONFile(ctx.String("outfile.l2"), l2Genesis); err != nil { return err } - return writeGenesisFile(ctx.String("outfile.rollup"), rollupConfig) + return writeJSONFile(ctx.String("outfile.rollup"), rollupConfig) }, }, } -func writeGenesisFile(outfile string, input any) error { - f, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) +// writeJSONFile will write a JSON file to disk at the given path +// containing the JSON serialized input value. +func writeJSONFile(outfile string, input any) error { + f, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) if err != nil { return err } @@ -223,3 +270,55 @@ func writeGenesisFile(outfile string, input any) error { enc.SetIndent("", " ") return enc.Encode(input) } + +// rpcBlock represents the JSON serialization of a block from an Ethereum RPC. +type rpcBlock struct { + Hash common.Hash `json:"hash"` + Transactions []rpcTransaction `json:"transactions"` + UncleHashes []common.Hash `json:"uncles"` + Withdrawals []*types.Withdrawal `json:"withdrawals,omitempty"` +} + +// rpcTransaction represents the JSON serialization of a transaction from an Ethereum RPC. +type rpcTransaction struct { + tx *types.Transaction + txExtraInfo +} + +// txExtraInfo includes extra information about a transaction that is returned from +// and Ethereum RPC endpoint. +type txExtraInfo struct { + BlockNumber *string `json:"blockNumber,omitempty"` + BlockHash *common.Hash `json:"blockHash,omitempty"` + From *common.Address `json:"from,omitempty"` +} + +// readBlockJSON will read a JSON file from disk containing a serialized block. +// This logic can break if the block format changes but there is no modular way +// to turn a block into JSON in go-ethereum. +func readBlockJSON(path string) (*types.Block, error) { + raw, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("block file at %s not found: %w", path, err) + } + + var header types.Header + if err := json.Unmarshal(raw, &header); err != nil { + return nil, fmt.Errorf("cannot unmarshal block: %w", err) + } + + var body rpcBlock + if err := json.Unmarshal(raw, &body); err != nil { + return nil, err + } + + if len(body.UncleHashes) > 0 { + return nil, fmt.Errorf("cannot unmarshal block with uncles") + } + + txs := make([]*types.Transaction, len(body.Transactions)) + for i, tx := range body.Transactions { + txs[i] = tx.tx + } + return types.NewBlockWithHeader(&header).WithBody(txs, nil).WithWithdrawals(body.Withdrawals), nil +} diff --git a/packages/contracts-bedrock/.gitignore b/packages/contracts-bedrock/.gitignore index 182e7983b2a1..3aa490443cf7 100644 --- a/packages/contracts-bedrock/.gitignore +++ b/packages/contracts-bedrock/.gitignore @@ -3,12 +3,14 @@ artifacts forge-artifacts cache broadcast -typechain # Metrics coverage.out .resource-metering.csv +# Testing State +.testdata + # Scripts scripts/go-ffi/go-ffi diff --git a/packages/contracts-bedrock/scripts/generate-l2-genesis.sh b/packages/contracts-bedrock/scripts/generate-l2-genesis.sh new file mode 100755 index 000000000000..231aade16fbd --- /dev/null +++ b/packages/contracts-bedrock/scripts/generate-l2-genesis.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Create a L2 genesis.json suitable for the solidity tests to +# ingest using `vm.loadAllocs(string)`. +# This script depends on the relative path to the op-node from +# contracts-bedrock + +SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null && pwd)" +CONTRACTS_DIR="$(realpath "$SCRIPTS_DIR/..")" +MONOREPO_BASE="$(realpath "$CONTRACTS_DIR/../..")" + +DEPLOY_ARTIFACT="$CONTRACTS_DIR/deployments/hardhat/.deploy" +OP_NODE="$MONOREPO_BASE/op-node/cmd/main.go" +L1_STARTING_BLOCK_PATH="$CONTRACTS_DIR/test/mocks/block.json" +TESTDATA_DIR="$CONTRACTS_DIR/.testdata" + +OUTFILE_L2="$TESTDATA_DIR/genesis.json" +OUTFILE_ROLLUP="$TESTDATA_DIR/rollup.json" +OUTFILE_ALLOC="$TESTDATA_DIR/alloc.json" + +mkdir -p "$TESTDATA_DIR" + +if [ ! -f "$DEPLOY_ARTIFACT" ]; then + forge script $CONTRACTS_DIR/scripts/Deploy.s.sol:Deploy 2>&1 /dev/null +fi + +# TODO: +# if the testdata dir doesn't exist, run the command +# add a clean command to the package.json + +go run $OP_NODE genesis l2 \ + --deploy-config "$CONTRACTS_DIR/deploy-config/hardhat.json" \ + --l1-deployments "$DEPLOY_ARTIFACT" \ + --l1-starting-block "$L1_STARTING_BLOCK_PATH" \ + --outfile.l2 "$OUTFILE_L2" \ + --outfile.rollup "$OUTFILE_ROLLUP" >/dev/null 2>&1 + +jq .alloc < "$OUTFILE_L2" > "$OUTFILE_ALLOC" diff --git a/packages/contracts-bedrock/test/mocks/block.json b/packages/contracts-bedrock/test/mocks/block.json new file mode 100644 index 000000000000..7de13ed6135a --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/block.json @@ -0,0 +1,27 @@ +{ + "hash": "0xfd3c5e25a80f54a53c58bd3ad8c076dc1c0cdbd44ec2164d2d2b8cc50481cb78", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "miner": "0x0000000000000000000000000000000000000000", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "number": "0x0", + "gasUsed": "0x0", + "gasLimit": "0x1c9c380", + "extraData": "0x", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "timestamp": "0x654caabb", + "difficulty": "0x0", + "totalDifficulty": "0x0", + "sealFields": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000" + ], + "uncles": [], + "transactions": [], + "size": "0x202", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "baseFeePerGas": "0x3b9aca00" +}