|
| 1 | +package cmd |
| 2 | + |
| 3 | +import ( |
| 4 | + "bytes" |
| 5 | + "compress/flate" |
| 6 | + "compress/gzip" |
| 7 | + "encoding/json" |
| 8 | + "errors" |
| 9 | + "fmt" |
| 10 | + "os" |
| 11 | + "path/filepath" |
| 12 | + "runtime" |
| 13 | + |
| 14 | + "github.com/ethereum-optimism/optimism/op-service/jsonutil" |
| 15 | + "github.com/ethereum-optimism/superchain-registry/add-chain/flags" |
| 16 | + "github.com/ethereum/go-ethereum/common" |
| 17 | + "github.com/ethereum/go-ethereum/common/hexutil" |
| 18 | + "github.com/ethereum/go-ethereum/core" |
| 19 | + "github.com/ethereum/go-ethereum/core/types" |
| 20 | + "github.com/ethereum/go-ethereum/crypto" |
| 21 | + "github.com/urfave/cli/v2" |
| 22 | +) |
| 23 | + |
| 24 | +var CompressGenesisCmd = cli.Command{ |
| 25 | + Name: "compress-genesis", |
| 26 | + Flags: []cli.Flag{ |
| 27 | + flags.L2GenesisFlag, |
| 28 | + flags.L2GenesisHeaderFlag, |
| 29 | + flags.ChainShortNameFlag, |
| 30 | + flags.SuperchainTargetFlag, |
| 31 | + }, |
| 32 | + Usage: "Generate a single gzipped data file from a bytecode hex string", |
| 33 | + Action: func(ctx *cli.Context) error { |
| 34 | + // Get the current script filepath |
| 35 | + _, thisFile, _, ok := runtime.Caller(0) |
| 36 | + if !ok { |
| 37 | + panic("error getting current filepath") |
| 38 | + } |
| 39 | + superchainRepoRoot := filepath.Dir(filepath.Dir(filepath.Dir(thisFile))) |
| 40 | + superchainTarget := ctx.String(flags.SuperchainTargetFlag.Name) |
| 41 | + if superchainTarget == "" { |
| 42 | + return fmt.Errorf("missing required flag: %s", flags.SuperchainTargetFlag.Name) |
| 43 | + } |
| 44 | + chainShortName := ctx.String(flags.ChainShortNameFlag.Name) |
| 45 | + if chainShortName == "" { |
| 46 | + return fmt.Errorf("missing required flag: %s", flags.ChainShortNameFlag.Name) |
| 47 | + } |
| 48 | + |
| 49 | + zipOutputDir := filepath.Join(superchainRepoRoot, "/superchain/extra/genesis", superchainTarget, chainShortName+".json.gz") |
| 50 | + genesisPath := ctx.Path(flags.L2GenesisFlag.Name) |
| 51 | + if genesisPath == "" { |
| 52 | + // When the genesis-state is too large, or not meant to be available, only the header data is made available. |
| 53 | + // This allows the user to verify the header-chain starting from genesis, and state-sync the latest state, |
| 54 | + // skipping the historical state. |
| 55 | + // Archive nodes that depend on this historical state should instantiate the chain from a full genesis dump |
| 56 | + // with allocation data, or datadir. |
| 57 | + genesisHeaderPath := ctx.Path(flags.L2GenesisHeaderFlag.Name) |
| 58 | + genesisHeader, err := loadJSON[types.Header](genesisHeaderPath) |
| 59 | + if err != nil { |
| 60 | + return fmt.Errorf("genesis-header %q failed to load: %w", genesisHeaderPath, err) |
| 61 | + } |
| 62 | + if genesisHeader.TxHash != types.EmptyTxsHash { |
| 63 | + return errors.New("genesis-header based genesis must have no transactions") |
| 64 | + } |
| 65 | + if genesisHeader.ReceiptHash != types.EmptyReceiptsHash { |
| 66 | + return errors.New("genesis-header based genesis must have no receipts") |
| 67 | + } |
| 68 | + if genesisHeader.UncleHash != types.EmptyUncleHash { |
| 69 | + return errors.New("genesis-header based genesis must have no uncle hashes") |
| 70 | + } |
| 71 | + if genesisHeader.WithdrawalsHash != nil && *genesisHeader.WithdrawalsHash != types.EmptyWithdrawalsHash { |
| 72 | + return errors.New("genesis-header based genesis must have no withdrawals") |
| 73 | + } |
| 74 | + out := Genesis{ |
| 75 | + Nonce: genesisHeader.Nonce.Uint64(), |
| 76 | + Timestamp: genesisHeader.Time, |
| 77 | + ExtraData: genesisHeader.Extra, |
| 78 | + GasLimit: genesisHeader.GasLimit, |
| 79 | + Difficulty: (*hexutil.Big)(genesisHeader.Difficulty), |
| 80 | + Mixhash: genesisHeader.MixDigest, |
| 81 | + Coinbase: genesisHeader.Coinbase, |
| 82 | + Number: genesisHeader.Number.Uint64(), |
| 83 | + GasUsed: genesisHeader.GasUsed, |
| 84 | + ParentHash: genesisHeader.ParentHash, |
| 85 | + BaseFee: (*hexutil.Big)(genesisHeader.BaseFee), |
| 86 | + ExcessBlobGas: genesisHeader.ExcessBlobGas, // EIP-4844 |
| 87 | + BlobGasUsed: genesisHeader.BlobGasUsed, // EIP-4844 |
| 88 | + Alloc: make(jsonutil.LazySortedJsonMap[common.Address, GenesisAccount]), |
| 89 | + StateHash: &genesisHeader.Root, |
| 90 | + } |
| 91 | + if err := writeGzipJSON(zipOutputDir, out); err != nil { |
| 92 | + return fmt.Errorf("failed to write output: %w", err) |
| 93 | + } |
| 94 | + return nil |
| 95 | + } |
| 96 | + |
| 97 | + genesis, err := loadJSON[core.Genesis](genesisPath) |
| 98 | + if err != nil { |
| 99 | + return fmt.Errorf("failed to load L2 genesis: %w", err) |
| 100 | + } |
| 101 | + |
| 102 | + // export all contract bytecodes, write them to bytecodes collection |
| 103 | + bytecodesDir := filepath.Join(superchainRepoRoot, "/superchain/extra/bytecodes") |
| 104 | + fmt.Printf("using output bytecodes dir: %s\n", bytecodesDir) |
| 105 | + if err := os.MkdirAll(bytecodesDir, 0o755); err != nil { |
| 106 | + return fmt.Errorf("failed to make bytecodes dir: %w", err) |
| 107 | + } |
| 108 | + for addr, account := range genesis.Alloc { |
| 109 | + if len(account.Code) > 0 { |
| 110 | + err = writeBytecode(bytecodesDir, account.Code, addr) |
| 111 | + if err != nil { |
| 112 | + return err |
| 113 | + } |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + // convert into allocation data |
| 118 | + out := Genesis{ |
| 119 | + Nonce: genesis.Nonce, |
| 120 | + Timestamp: genesis.Timestamp, |
| 121 | + ExtraData: genesis.ExtraData, |
| 122 | + GasLimit: genesis.GasLimit, |
| 123 | + Difficulty: (*hexutil.Big)(genesis.Difficulty), |
| 124 | + Mixhash: genesis.Mixhash, |
| 125 | + Coinbase: genesis.Coinbase, |
| 126 | + Number: genesis.Number, |
| 127 | + GasUsed: genesis.GasUsed, |
| 128 | + ParentHash: genesis.ParentHash, |
| 129 | + BaseFee: (*hexutil.Big)(genesis.BaseFee), |
| 130 | + ExcessBlobGas: genesis.ExcessBlobGas, // EIP-4844 |
| 131 | + BlobGasUsed: genesis.BlobGasUsed, // EIP-4844 |
| 132 | + Alloc: make(jsonutil.LazySortedJsonMap[common.Address, GenesisAccount]), |
| 133 | + } |
| 134 | + |
| 135 | + // write genesis, but only reference code by code-hash, and don't encode the L2 predeploys to save space. |
| 136 | + for addr, account := range genesis.Alloc { |
| 137 | + var codeHash common.Hash |
| 138 | + if len(account.Code) > 0 { |
| 139 | + codeHash = crypto.Keccak256Hash(account.Code) |
| 140 | + } |
| 141 | + outAcc := GenesisAccount{ |
| 142 | + CodeHash: codeHash, |
| 143 | + Nonce: account.Nonce, |
| 144 | + } |
| 145 | + if account.Balance != nil && account.Balance.Cmp(common.Big0) != 0 { |
| 146 | + outAcc.Balance = (*hexutil.Big)(account.Balance) |
| 147 | + } |
| 148 | + if len(account.Storage) > 0 { |
| 149 | + outAcc.Storage = make(jsonutil.LazySortedJsonMap[common.Hash, common.Hash]) |
| 150 | + for k, v := range account.Storage { |
| 151 | + outAcc.Storage[k] = v |
| 152 | + } |
| 153 | + } |
| 154 | + out.Alloc[addr] = outAcc |
| 155 | + } |
| 156 | + |
| 157 | + // write genesis alloc |
| 158 | + if err := writeGzipJSON(zipOutputDir, out); err != nil { |
| 159 | + return fmt.Errorf("failed to write output: %w", err) |
| 160 | + } |
| 161 | + return nil |
| 162 | + }, |
| 163 | +} |
| 164 | + |
| 165 | +func writeBytecode(bytecodesDir string, code []byte, addr common.Address) error { |
| 166 | + codeHash := crypto.Keccak256Hash(code) |
| 167 | + name := filepath.Join(bytecodesDir, fmt.Sprintf("%s.bin.gz", codeHash)) |
| 168 | + _, err := os.Stat(name) |
| 169 | + if err == nil { |
| 170 | + // file already exists |
| 171 | + return nil |
| 172 | + } |
| 173 | + if !os.IsNotExist(err) { |
| 174 | + return fmt.Errorf("failed to check for pre-existing bytecode %s for address %s: %w", codeHash, addr, err) |
| 175 | + } |
| 176 | + var buf bytes.Buffer |
| 177 | + w, err := gzip.NewWriterLevel(&buf, 9) |
| 178 | + if err != nil { |
| 179 | + return fmt.Errorf("failed to construct gzip writer for bytecode %s: %w", codeHash, err) |
| 180 | + } |
| 181 | + if _, err := w.Write(code); err != nil { |
| 182 | + return fmt.Errorf("failed to write bytecode %s to gzip writer: %w", codeHash, err) |
| 183 | + } |
| 184 | + if err := w.Close(); err != nil { |
| 185 | + return fmt.Errorf("failed to close gzip writer: %w", err) |
| 186 | + } |
| 187 | + // new bytecode |
| 188 | + if err := os.WriteFile(name, buf.Bytes(), 0o755); err != nil { |
| 189 | + return fmt.Errorf("failed to write bytecode %s of account %s: %w", codeHash, addr, err) |
| 190 | + } |
| 191 | + fmt.Printf("created new bytecodes file: %s\n", filepath.Base(name)) |
| 192 | + return nil |
| 193 | +} |
| 194 | + |
| 195 | +type GenesisAccount struct { |
| 196 | + CodeHash common.Hash `json:"codeHash,omitempty"` |
| 197 | + Storage jsonutil.LazySortedJsonMap[common.Hash, common.Hash] `json:"storage,omitempty"` |
| 198 | + Balance *hexutil.Big `json:"balance,omitempty"` |
| 199 | + Nonce uint64 `json:"nonce,omitempty"` |
| 200 | +} |
| 201 | + |
| 202 | +type Genesis struct { |
| 203 | + Nonce uint64 `json:"nonce"` |
| 204 | + Timestamp uint64 `json:"timestamp"` |
| 205 | + ExtraData []byte `json:"extraData"` |
| 206 | + GasLimit uint64 `json:"gasLimit"` |
| 207 | + Difficulty *hexutil.Big `json:"difficulty"` |
| 208 | + Mixhash common.Hash `json:"mixHash"` |
| 209 | + Coinbase common.Address `json:"coinbase"` |
| 210 | + Number uint64 `json:"number"` |
| 211 | + GasUsed uint64 `json:"gasUsed"` |
| 212 | + ParentHash common.Hash `json:"parentHash"` |
| 213 | + BaseFee *hexutil.Big `json:"baseFeePerGas"` |
| 214 | + ExcessBlobGas *uint64 `json:"excessBlobGas"` // EIP-4844 |
| 215 | + BlobGasUsed *uint64 `json:"blobGasUsed"` // EIP-4844 |
| 216 | + |
| 217 | + Alloc jsonutil.LazySortedJsonMap[common.Address, GenesisAccount] `json:"alloc"` |
| 218 | + // For genesis definitions without full state (OP-Mainnet, OP-Goerli) |
| 219 | + StateHash *common.Hash `json:"stateHash,omitempty"` |
| 220 | +} |
| 221 | + |
| 222 | +func loadJSON[X any](inputPath string) (*X, error) { |
| 223 | + if inputPath == "" { |
| 224 | + return nil, errors.New("no path specified") |
| 225 | + } |
| 226 | + f, err := os.OpenFile(inputPath, os.O_RDONLY, 0) |
| 227 | + if err != nil { |
| 228 | + return nil, fmt.Errorf("failed to open file %q: %w", inputPath, err) |
| 229 | + } |
| 230 | + defer f.Close() |
| 231 | + var obj X |
| 232 | + if err := json.NewDecoder(f).Decode(&obj); err != nil { |
| 233 | + return nil, fmt.Errorf("failed to decode file %q: %w", inputPath, err) |
| 234 | + } |
| 235 | + return &obj, nil |
| 236 | +} |
| 237 | + |
| 238 | +func writeGzipJSON(outputPath string, value any) error { |
| 239 | + fmt.Printf("using output gzip filepath: %s\n", outputPath) |
| 240 | + f, err := os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) |
| 241 | + if err != nil { |
| 242 | + return fmt.Errorf("failed to open output file: %w", err) |
| 243 | + } |
| 244 | + defer f.Close() |
| 245 | + w, err := gzip.NewWriterLevel(f, flate.BestCompression) |
| 246 | + if err != nil { |
| 247 | + return fmt.Errorf("failed to create gzip writer: %w", err) |
| 248 | + } |
| 249 | + defer w.Close() |
| 250 | + enc := json.NewEncoder(w) |
| 251 | + if err := enc.Encode(value); err != nil { |
| 252 | + return fmt.Errorf("failed to encode to JSON: %w", err) |
| 253 | + } |
| 254 | + return nil |
| 255 | +} |
0 commit comments