Skip to content

Commit

Permalink
op-node,contracts-bedrock: setup loadAllocs usage
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tynes committed Nov 13, 2023
1 parent 2088e2e commit 37bb134
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 72 deletions.
241 changes: 170 additions & 71 deletions op-node/cmd/genesis/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -85,52 +126,28 @@ var Subcommands = cli.Commands{
return err
}

return writeGenesisFile(ctx.String("outfile.l1"), l1Genesis)
return writeJSONFile(ctx.String("outfile.l1"), l1Genesis)
},
},
{
Name: "l2",
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")
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
}
4 changes: 3 additions & 1 deletion packages/contracts-bedrock/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 38 additions & 0 deletions packages/contracts-bedrock/scripts/generate-l2-genesis.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading

0 comments on commit 37bb134

Please sign in to comment.