cmd.go 9.73 KB
Newer Older
1 2 3
package genesis

import (
4
	"context"
5
	"encoding/json"
6
	"errors"
7
	"fmt"
8
	"math/big"
9
	"os"
10 11
	"path/filepath"

12
	"github.com/urfave/cli/v2"
13

14
	"github.com/ethereum/go-ethereum/common"
Mark Tyneway's avatar
Mark Tyneway committed
15
	"github.com/ethereum/go-ethereum/core/state"
16 17
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/ethclient"
18
	"github.com/ethereum/go-ethereum/log"
19
	"github.com/ethereum/go-ethereum/rpc"
20

21
	"github.com/ethereum-optimism/optimism/op-bindings/hardhat"
22
	"github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
23 24
)

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
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,
	}
)

83 84
var Subcommands = cli.Commands{
	{
Mark Tyneway's avatar
Mark Tyneway committed
85 86
		Name:  "l1",
		Usage: "Generates a L1 genesis state file",
87
		Flags: l1Flags,
88
		Action: func(ctx *cli.Context) error {
89
			deployConfig := ctx.String("deploy-config")
90
			config, err := genesis.NewDeployConfig(deployConfig)
91 92 93 94
			if err != nil {
				return err
			}

Mark Tyneway's avatar
Mark Tyneway committed
95 96 97 98 99 100
			var deployments *genesis.L1Deployments
			if l1Deployments := ctx.String("l1-deployments"); l1Deployments != "" {
				deployments, err = genesis.NewL1Deployments(l1Deployments)
				if err != nil {
					return err
				}
101 102
			}

Mark Tyneway's avatar
Mark Tyneway committed
103 104
			if deployments != nil {
				config.SetDeployments(deployments)
105 106
			}

Mark Tyneway's avatar
Mark Tyneway committed
107 108
			if err := config.Check(); err != nil {
				return fmt.Errorf("deploy config at %s invalid: %w", deployConfig, err)
109 110
			}

111 112 113 114 115
			// Check the addresses after setting the deployments
			if err := config.CheckAddresses(); err != nil {
				return fmt.Errorf("deploy config at %s invalid: %w", deployConfig, err)
			}

Mark Tyneway's avatar
Mark Tyneway committed
116 117 118 119 120 121
			var dump *state.Dump
			if l1Allocs := ctx.String("l1-allocs"); l1Allocs != "" {
				dump, err = genesis.NewStateDump(l1Allocs)
				if err != nil {
					return err
				}
122 123
			}

Mark Tyneway's avatar
Mark Tyneway committed
124
			l1Genesis, err := genesis.BuildL1DeveloperGenesis(config, dump, deployments, true)
125 126 127
			if err != nil {
				return err
			}
128

129
			return writeJSONFile(ctx.String("outfile.l1"), l1Genesis)
130 131
		},
	},
132 133 134
	{
		Name:  "l2",
		Usage: "Generates an L2 genesis file and rollup config suitable for a deployed network",
135 136
		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. " +
137 138 139 140
			"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,
141
		Action: func(ctx *cli.Context) error {
142
			deployConfig := ctx.Path("deploy-config")
143
			log.Info("Deploy config", "path", deployConfig)
144 145 146 147 148
			config, err := genesis.NewDeployConfig(deployConfig)
			if err != nil {
				return err
			}

149 150
			deployDir := ctx.Path("deployment-dir")
			l1Deployments := ctx.Path("l1-deployments")
151 152 153 154 155 156

			if deployDir != "" && l1Deployments != "" {
				return errors.New("cannot specify both --deployment-dir and --l1-deployments")
			}
			if deployDir == "" && l1Deployments == "" {
				return errors.New("must specify either --deployment-dir or --l1-deployments")
157 158
			}

159 160 161 162 163 164 165 166 167 168
			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")
			}

169 170 171 172 173 174 175 176 177 178 179 180
			if deployDir != "" {
				log.Info("Deployment directory", "path", deployDir)
				depPath, network := filepath.Split(deployDir)
				hh, err := hardhat.New(network, nil, []string{depPath})
				if err != nil {
					return err
				}

				// Read the appropriate deployment addresses from disk
				if err := config.GetDeployedAddresses(hh); err != nil {
					return err
				}
181 182
			}

183 184 185
			if l1Deployments != "" {
				deployments, err := genesis.NewL1Deployments(l1Deployments)
				if err != nil {
186
					return fmt.Errorf("cannot read L1 deployments at %s: %w", l1Deployments, err)
187 188
				}
				config.SetDeployments(deployments)
189 190
			}

191 192 193 194 195
			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)
				}
196 197
			}

198
			if l1RPC != "" {
199
				client, err := ethclient.Dial(l1RPC)
200 201 202 203 204 205 206
				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 {
207
						return fmt.Errorf("cannot fetch latest block: %w", err)
208 209 210 211 212 213
					}
					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 {
214
						return fmt.Errorf("cannot fetch block by hash: %w", err)
215 216 217 218
					}
				} else if config.L1StartingBlockTag.BlockNumber != nil {
					l1StartBlock, err = client.BlockByNumber(context.Background(), big.NewInt(config.L1StartingBlockTag.BlockNumber.Int64()))
					if err != nil {
219
						return fmt.Errorf("cannot fetch block by number: %w", err)
220 221
					}
				}
222
			}
223 224 225 226

			// Ensure that there is a starting L1 block
			if l1StartBlock == nil {
				return errors.New("no starting L1 block")
227
			}
228 229 230 231 232 233 234

			// Sanity check the config. Do this after filling in the L1StartingBlockTag
			// if it is not defined.
			if err := config.Check(); err != nil {
				return err
			}

235
			log.Info("Using L1 Start Block", "number", l1StartBlock.Number(), "hash", l1StartBlock.Hash().Hex())
236

Mark Tyneway's avatar
Mark Tyneway committed
237
			// Build the L2 genesis block
238
			l2Genesis, err := genesis.BuildL2Genesis(config, l1StartBlock)
239
			if err != nil {
Mark Tyneway's avatar
Mark Tyneway committed
240
				return fmt.Errorf("error creating l2 genesis: %w", err)
241 242
			}

243 244
			l2GenesisBlock := l2Genesis.ToBlock()
			rollupConfig, err := config.RollupConfig(l1StartBlock, l2GenesisBlock.Hash(), l2GenesisBlock.Number().Uint64())
245 246 247
			if err != nil {
				return err
			}
248 249 250
			if err := rollupConfig.Check(); err != nil {
				return fmt.Errorf("generated rollup config does not pass validation: %w", err)
			}
251

252
			if err := writeJSONFile(ctx.String("outfile.l2"), l2Genesis); err != nil {
253 254
				return err
			}
255
			return writeJSONFile(ctx.String("outfile.rollup"), rollupConfig)
256 257 258 259
		},
	},
}

260 261 262 263
// 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)
264 265 266 267 268 269 270 271 272
	if err != nil {
		return err
	}
	defer f.Close()

	enc := json.NewEncoder(f)
	enc.SetIndent("", "  ")
	return enc.Encode(input)
}
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324

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