Commit 37bb1345 authored by Mark Tyneway's avatar Mark Tyneway

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.
parent 2088e2e7
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
...@@ -21,29 +22,69 @@ import ( ...@@ -21,29 +22,69 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/genesis" "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{ var Subcommands = cli.Commands{
{ {
Name: "l1", Name: "l1",
Usage: "Generates a L1 genesis state file", Usage: "Generates a L1 genesis state file",
Flags: []cli.Flag{ Flags: l1Flags,
&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",
},
},
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {
deployConfig := ctx.String("deploy-config") deployConfig := ctx.String("deploy-config")
config, err := genesis.NewDeployConfig(deployConfig) config, err := genesis.NewDeployConfig(deployConfig)
...@@ -85,7 +126,7 @@ var Subcommands = cli.Commands{ ...@@ -85,7 +126,7 @@ var Subcommands = cli.Commands{
return err return err
} }
return writeGenesisFile(ctx.String("outfile.l1"), l1Genesis) return writeJSONFile(ctx.String("outfile.l1"), l1Genesis)
}, },
}, },
{ {
...@@ -93,44 +134,20 @@ var Subcommands = cli.Commands{ ...@@ -93,44 +134,20 @@ var Subcommands = cli.Commands{
Usage: "Generates an L2 genesis file and rollup config suitable for a deployed network", 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. " + 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. " + "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.", "The deploy directory and L1 deployments file are generated by the L1 contract deployments. " +
Flags: []cli.Flag{ "An L1 starting block is necessary, it can either be fetched dynamically using config in the deploy config " +
&cli.StringFlag{ "or it can be provided as a JSON file.",
Name: "l1-rpc", Flags: l2Flags,
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",
},
},
Action: func(ctx *cli.Context) error { Action: func(ctx *cli.Context) error {
deployConfig := ctx.String("deploy-config") deployConfig := ctx.Path("deploy-config")
log.Info("Deploy config", "path", deployConfig) log.Info("Deploy config", "path", deployConfig)
config, err := genesis.NewDeployConfig(deployConfig) config, err := genesis.NewDeployConfig(deployConfig)
if err != nil { if err != nil {
return err return err
} }
deployDir := ctx.String("deployment-dir") deployDir := ctx.Path("deployment-dir")
l1Deployments := ctx.String("l1-deployments") l1Deployments := ctx.Path("l1-deployments")
if deployDir != "" && l1Deployments != "" { if deployDir != "" && l1Deployments != "" {
return errors.New("cannot specify both --deployment-dir and --l1-deployments") return errors.New("cannot specify both --deployment-dir and --l1-deployments")
...@@ -139,6 +156,16 @@ var Subcommands = cli.Commands{ ...@@ -139,6 +156,16 @@ var Subcommands = cli.Commands{
return errors.New("must specify either --deployment-dir or --l1-deployments") 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 != "" { if deployDir != "" {
log.Info("Deployment directory", "path", deployDir) log.Info("Deployment directory", "path", deployDir)
depPath, network := filepath.Split(deployDir) depPath, network := filepath.Split(deployDir)
...@@ -154,31 +181,49 @@ var Subcommands = cli.Commands{ ...@@ -154,31 +181,49 @@ var Subcommands = cli.Commands{
} }
if l1Deployments != "" { if l1Deployments != "" {
log.Info("L1 deployments", "path", l1Deployments)
deployments, err := genesis.NewL1Deployments(l1Deployments) deployments, err := genesis.NewL1Deployments(l1Deployments)
if err != nil { if err != nil {
return err return fmt.Errorf("cannot read L1 deployments at %s: %w", l1Deployments, err)
} }
config.SetDeployments(deployments) config.SetDeployments(deployments)
} }
client, err := ethclient.Dial(ctx.String("l1-rpc")) var l1StartBlock *types.Block
if err != nil { if l1StartBlockPath != "" {
return fmt.Errorf("cannot dial %s: %w", ctx.String("l1-rpc"), err) 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 l1RPC != "" {
if config.L1StartingBlockTag == nil { client, err := ethclient.Dial(ctx.String("l1-rpc"))
l1StartBlock, err = client.BlockByNumber(context.Background(), nil) if err != nil {
tag := rpc.BlockNumberOrHashWithHash(l1StartBlock.Hash(), true) return fmt.Errorf("cannot dial %s: %w", l1RPC, err)
config.L1StartingBlockTag = (*genesis.MarshalableRPCBlockNumberOrHash)(&tag) }
} else if config.L1StartingBlockTag.BlockHash != nil {
l1StartBlock, err = client.BlockByHash(context.Background(), *config.L1StartingBlockTag.BlockHash) if config.L1StartingBlockTag == nil {
} else if config.L1StartingBlockTag.BlockNumber != nil { l1StartBlock, err = client.BlockByNumber(context.Background(), nil)
l1StartBlock, err = client.BlockByNumber(context.Background(), big.NewInt(config.L1StartingBlockTag.BlockNumber.Int64())) 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 // Sanity check the config. Do this after filling in the L1StartingBlockTag
...@@ -204,16 +249,18 @@ var Subcommands = cli.Commands{ ...@@ -204,16 +249,18 @@ var Subcommands = cli.Commands{
return fmt.Errorf("generated rollup config does not pass validation: %w", err) 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 err
} }
return writeGenesisFile(ctx.String("outfile.rollup"), rollupConfig) return writeJSONFile(ctx.String("outfile.rollup"), rollupConfig)
}, },
}, },
} }
func writeGenesisFile(outfile string, input any) error { // writeJSONFile will write a JSON file to disk at the given path
f, err := os.OpenFile(outfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) // 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 { if err != nil {
return err return err
} }
...@@ -223,3 +270,55 @@ func writeGenesisFile(outfile string, input any) error { ...@@ -223,3 +270,55 @@ func writeGenesisFile(outfile string, input any) error {
enc.SetIndent("", " ") enc.SetIndent("", " ")
return enc.Encode(input) 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
}
...@@ -3,12 +3,14 @@ artifacts ...@@ -3,12 +3,14 @@ artifacts
forge-artifacts forge-artifacts
cache cache
broadcast broadcast
typechain
# Metrics # Metrics
coverage.out coverage.out
.resource-metering.csv .resource-metering.csv
# Testing State
.testdata
# Scripts # Scripts
scripts/go-ffi/go-ffi scripts/go-ffi/go-ffi
......
#!/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"
{
"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"
}
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