# Nimbus # Copyright (c) 2021-2023 Status Research & Development GmbH # Licensed under either of # * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) # * MIT license ([LICENSE-MIT](LICENSE-MIT)) # at your option. # This file may not be copied, modified, or distributed except according to # those terms. {.push raises: [].} import std/[tables, strutils, options, times, macros], eth/[common, rlp, p2p], stint, stew/[byteutils], json_serialization, chronicles, json_serialization/std/options as jsoptions, json_serialization/std/tables as jstable, json_serialization/lexer, "."/[genesis_alloc, hardforks] export hardforks type Genesis* = ref object # for geth compatibility config* : ChainConfig nonce* : BlockNonce timestamp* : EthTime extraData* : seq[byte] gasLimit* : GasInt difficulty* : DifficultyInt mixHash* : Hash256 coinbase* : EthAddress alloc* : GenesisAlloc number* : BlockNumber gasUser* : GasInt parentHash* : Hash256 baseFeePerGas*: Option[UInt256] # EIP-1559 blobGasUsed* : Option[uint64] # EIP-4844 excessBlobGas*: Option[uint64] # EIP-4844 parentBeaconBlockRoot*: Option[Hash256] # EIP-4788 GenesisAlloc* = Table[EthAddress, GenesisAccount] GenesisAccount* = object code* : seq[byte] storage*: Table[UInt256, UInt256] balance*: UInt256 nonce* : AccountNonce NetworkParams* = object config* : ChainConfig genesis*: Genesis const CustomNet* = 0.NetworkId # these are public network id MainNet* = 1.NetworkId GoerliNet* = 5.NetworkId SepoliaNet* = 11155111.NetworkId HoleskyNet* = 17000.NetworkId # ------------------------------------------------------------------------------ # Private helper functions # ------------------------------------------------------------------------------ proc writeValue(writer: var JsonWriter, value: Option[EthTime]) {.gcsafe, raises: [IOError].} = mixin writeValue if value.isSome: writer.writeValue value.get.uint64 else: writer.writeValue JsonString("null") type Slots = object key: UInt256 val: UInt256 Misc = object nonce: uint64 code : seq[byte] storage: seq[Slots] AddressBalance = object address: EthAddress account: GenesisAccount proc read*(rlp: var Rlp, T: type AddressBalance): T {.gcsafe, raises: [RlpError].}= let listLen = rlp.listLen rlp.tryEnterList() let val = rlp.read(UInt256).toBytesBE() result.address[0..^1] = val.toOpenArray(12, val.high) result.account.balance = rlp.read(UInt256) if listLen == 3: var misc = rlp.read(Misc) result.account.nonce = misc.nonce result.account.code = system.move(misc.code) for x in misc.storage: result.account.storage[x.key] = x.val proc append*(w: var RlpWriter, ab: AddressBalance) = var listLen = 2 if ab.account.storage.len > 0 or ab.account.nonce != 0.AccountNonce or ab.account.code.len > 0: inc listLen w.startList(listLen) var tmp: array[32, byte] tmp[12..^1] = ab.address[0..^1] var val = UInt256.fromBytesBE(tmp) w.append(val) w.append(ab.account.balance) if listLen == 3: var misc: Misc misc.nonce = ab.account.nonce misc.code = ab.account.code for k, v in ab.account.storage: misc.storage.add Slots(key:k, val: v) w.append(misc) proc append*(w: var RlpWriter, ga: GenesisAlloc) = var list: seq[AddressBalance] for k, v in ga: list.add AddressBalance( address: k, account: v ) w.append(list) func decodePrealloc*(data: seq[byte]): GenesisAlloc {.gcsafe, raises: [RlpError].} = for tup in rlp.decode(data, seq[AddressBalance]): result[tup.address] = tup.account # borrowed from `lexer.hexCharValue()` :) proc fromHex(c: char): int = case c of '0'..'9': ord(c) - ord('0') of 'a'..'f': ord(c) - ord('a') + 10 of 'A'..'F': ord(c) - ord('A') + 10 else: -1 proc readValue(reader: var JsonReader, value: var UInt256) {.gcsafe, raises: [SerializationError, IOError].} = ## Mixin for `Json.loadFile()`. Note that this driver applies the same ## to `BlockNumber` fields as well as generic `UInt265` fields like the ## account `balance`. var (accu, ok) = (0.u256, true) if reader.lexer.lazyTok == tkNumeric: try: reader.lexer.customIntValueIt: accu = accu * 10 + it.u256 ok = reader.lexer.lazyTok == tkExInt # non-negative wanted except CatchableError: ok = false elif reader.lexer.lazyTok == tkQuoted: try: var (sLen, base) = (0, 10) reader.lexer.customTextValueIt: if ok: var num = it.fromHex if base <= num: ok = false # cannot be larger than base elif sLen < 2: if 0 <= num: accu = accu * base.u256 + num.u256 elif sLen == 1 and it in {'x', 'X'}: base = 16 # handle "0x" prefix else: ok = false sLen.inc elif num < 0: ok = false # not a hex digit elif base == 10: accu = accu * 10 + num.u256 else: accu = accu * 16 + num.u256 except CatchableError: reader.raiseUnexpectedValue("numeric string parse error") else: reader.raiseUnexpectedValue("expect int or hex/int string") if not ok: reader.raiseUnexpectedValue("Uint256 parse error") value = accu reader.lexer.next() proc readValue(reader: var JsonReader, value: var ChainId) {.gcsafe, raises: [SerializationError, IOError].} = value = reader.readValue(int).ChainId proc readValue(reader: var JsonReader, value: var Hash256) {.gcsafe, raises: [SerializationError, IOError].} = value = Hash256.fromHex(reader.readValue(string)) proc readValue(reader: var JsonReader, value: var BlockNonce) {.gcsafe, raises: [SerializationError, IOError].} = try: value = fromHex[uint64](reader.readValue(string)).toBlockNonce except ValueError as ex: reader.raiseUnexpectedValue(ex.msg) # genesis timestamp is in hex/dec proc readValue(reader: var JsonReader, value: var EthTime) {.gcsafe, raises: [SerializationError, IOError].} = try: let data = reader.readValue(string) if data.len > 2 and data[1] == 'x': value = fromHex[int64](data).EthTime else: value = parseInt(data).EthTime except ValueError as ex: reader.raiseUnexpectedValue(ex.msg) # but shanghaiTime and cancunTime in config is in int literal proc readValue(reader: var JsonReader, value: var Option[EthTime]) {.gcsafe, raises: [IOError].} = let tok = reader.lexer.lazyTok if tok == tkNull: reset value reader.lexer.next() else: # both readValue(GasInt/AccountNonce) will be called if # we use readValue(int64/uint64) let tok {.used.} = reader.lexer.tok # resove lazy token let val = EthTime reader.lexer.absIntVal value = some val reader.lexer.next() proc readValue(reader: var JsonReader, value: var seq[byte]) {.gcsafe, raises: [SerializationError, IOError].} = try: value = hexToSeqByte(reader.readValue(string)) except ValueError as ex: reader.raiseUnexpectedValue(ex.msg) proc readValue(reader: var JsonReader, value: var GasInt) {.gcsafe, raises: [SerializationError, IOError].} = try: value = fromHex[GasInt](reader.readValue(string)) except ValueError as ex: reader.raiseUnexpectedValue(ex.msg) proc readValue(reader: var JsonReader, value: var EthAddress) {.gcsafe, raises: [SerializationError, IOError].} = try: value = parseAddress(reader.readValue(string)) except ValueError as ex: reader.raiseUnexpectedValue(ex.msg) proc readValue(reader: var JsonReader, value: var AccountNonce) {.gcsafe, raises: [SerializationError, IOError].} = try: value = fromHex[uint64](reader.readValue(string)) except ValueError as ex: reader.raiseUnexpectedValue(ex.msg) template to(a: string, b: type EthAddress): EthAddress = # json_serialization decode table stuff parseAddress(a) template to(a: string, b: type UInt256): UInt256 = # json_serialization decode table stuff UInt256.fromHex(a) macro fillArrayOfBlockNumberBasedForkOptionals(conf, tmp: typed): untyped = result = newStmtList() for i, x in forkBlockField: let fieldIdent = newIdentNode(x) result.add quote do: `tmp`[`i`] = BlockNumberBasedForkOptional( number : `conf`.`fieldIdent`, name : `x`) macro fillArrayOfTimeBasedForkOptionals(conf, tmp: typed): untyped = result = newStmtList() for i, x in forkTimeField: let fieldIdent = newIdentNode(x) result.add quote do: `tmp`[`i`] = TimeBasedForkOptional( time : `conf`.`fieldIdent`, name : `x`) # ------------------------------------------------------------------------------ # Public functions # ------------------------------------------------------------------------------ proc toHardFork*(map: ForkTransitionTable, forkDeterminer: ForkDeterminationInfo): HardFork = for fork in countdown(HardFork.high, HardFork.low): if isGTETransitionThreshold(map, forkDeterminer, fork): return fork # should always have a match doAssert(false, "unreachable code") proc validateChainConfig*(conf: ChainConfig): bool = result = true if conf.mergeNetsplitBlock.isSome: # geth compatibility conf.mergeForkBlock = conf.mergeNetsplitBlock # FIXME: factor this to remove the duplication between the # block-based ones and the time-based ones. var blockNumberBasedForkOptionals: array[forkBlockField.len, BlockNumberBasedForkOptional] fillArrayOfBlockNumberBasedForkOptionals(conf, blockNumberBasedForkOptionals) var timeBasedForkOptionals: array[forkTimeField.len, TimeBasedForkOptional] fillArrayOfTimeBasedForkOptionals(conf, timeBasedForkOptionals) var lastBlockNumberBasedFork = blockNumberBasedForkOptionals[0] for i in 1.. cur.number.get: error "Unsupported fork ordering", lastFork=lastBlockNumberBasedFork.name, lastNumber=lastBlockNumberBasedFork.number, curFork=cur.name, curNumber=cur.number return false # If it was optional and not set, then ignore it if cur.number.isSome: lastBlockNumberBasedFork = cur # TODO: check to make sure the timestamps are all past the # block numbers? var lastTimeBasedFork = timeBasedForkOptionals[0] for i in 1.. cur.time.get: error "Unsupported fork ordering", lastFork=lastTimeBasedFork.name, lastTime=lastTimeBasedFork.time, curFork=cur.name, curTime=cur.time return false # If it was optional and not set, then ignore it if cur.time.isSome: lastTimeBasedFork = cur if conf.clique.period.isSome or conf.clique.epoch.isSome: conf.consensusType = ConsensusType.POA proc parseGenesis*(data: string): Genesis {.gcsafe.} = try: result = Json.decode(data, Genesis, allowUnknownFields = true) except JsonReaderError as e: error "Invalid genesis config file format", msg=e.formatMsg("") return nil except CatchableError as e: error "Error loading genesis data", exception = e.name, msg = e.msg return nil proc parseGenesisFile*(fileName: string): Genesis {.gcsafe.} = try: result = Json.loadFile(fileName, Genesis, allowUnknownFields = true) except IOError as e: error "Genesis I/O error", fileName, msg=e.msg return nil except JsonReaderError as e: error "Invalid genesis config file format", msg=e.formatMsg("") return nil except CatchableError as e: error "Error loading genesis file", fileName, exception = e.name, msg = e.msg return nil proc validateNetworkParams(params: var NetworkParams, input: string, inputIsFile: bool): bool = if params.genesis.isNil: # lets try with geth's format let genesis = if inputIsFile: parseGenesisFile(input) else: parseGenesis(input) if genesis.isNil: return false params.genesis = genesis if params.config.isNil: warn "Loaded custom network contains no 'config' data" params.config = ChainConfig() validateChainConfig(params.config) proc loadNetworkParams*(fileName: string, params: var NetworkParams): bool = try: params = Json.loadFile(fileName, NetworkParams, allowUnknownFields = true) except IOError as e: error "Network params I/O error", fileName, msg=e.msg return false except JsonReaderError as e: error "Invalid network params file format", fileName, msg=e.formatMsg("") return false except CatchableError as e: error "Error loading network params file", fileName, exception = e.name, msg = e.msg return false validateNetworkParams(params, fileName, true) proc decodeNetworkParams*(jsonString: string, params: var NetworkParams): bool = try: params = Json.decode(jsonString, NetworkParams, allowUnknownFields = true) except JsonReaderError as e: error "Invalid network params format", msg=e.formatMsg("") return false except CatchableError: var msg = getCurrentExceptionMsg() error "Error decoding network params", msg return false validateNetworkParams(params, jsonString, false) proc parseGenesisAlloc*(data: string, ga: var GenesisAlloc): bool {.gcsafe, raises: [CatchableError].} = try: ga = Json.decode(data, GenesisAlloc, allowUnknownFields = true) except JsonReaderError as e: error "Invalid genesis config file format", msg=e.formatMsg("") return false return true proc chainConfigForNetwork*(id: NetworkId): ChainConfig = # For some public networks, NetworkId and ChainId value are identical # but that is not always the case result = case id of MainNet: const mainNetTTD = parse("58750000000000000000000",UInt256) ChainConfig( consensusType: ConsensusType.POW, chainId: MainNet.ChainId, # Genesis (Frontier): # 2015-07-30 15:26:13 UTC # Frontier Thawing: 200_000.toBlockNumber, # 2015-09-07 21:33:09 UTC homesteadBlock: some(1_150_000.toBlockNumber), # 2016-03-14 18:49:53 UTC daoForkBlock: some(1_920_000.toBlockNumber), # 2016-07-20 13:20:40 UTC daoForkSupport: true, eip150Block: some(2_463_000.toBlockNumber), # 2016-10-18 13:19:31 UTC eip150Hash: toDigest("2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0"), eip155Block: some(2_675_000.toBlockNumber), # Same as EIP-158 eip158Block: some(2_675_000.toBlockNumber), # 2016-11-22 16:15:44 UTC byzantiumBlock: some(4_370_000.toBlockNumber), # 2017-10-16 05:22:11 UTC constantinopleBlock: some(7_280_000.toBlockNumber), # Skipped on Mainnet petersburgBlock: some(7_280_000.toBlockNumber), # 2019-02-28 19:52:04 UTC istanbulBlock: some(9_069_000.toBlockNumber), # 2019-12-08 00:25:09 UTC muirGlacierBlock: some(9_200_000.toBlockNumber), # 2020-01-02 08:30:49 UTC berlinBlock: some(12_244_000.toBlockNumber), # 2021-04-15 10:07:03 UTC londonBlock: some(12_965_000.toBlockNumber), # 2021-08-05 12:33:42 UTC arrowGlacierBlock: some(13_773_000.toBlockNumber), # 2021-12-09 19:55:23 UTC grayGlacierBlock: some(15_050_000.toBlockNumber), # 2022-06-30 10:54:04 UTC terminalTotalDifficulty: some(mainNetTTD), shanghaiTime: some(1_681_338_455.EthTime) ) of GoerliNet: ChainConfig( clique: CliqueOptions(period: some(15), epoch: some(30000)), consensusType: ConsensusType.POA, chainId: GoerliNet.ChainId, # Genesis: # 2015-07-30 15:26:13 UTC homesteadBlock: some(0.toBlockNumber), # Included in genesis daoForkSupport: false, eip150Block: some(0.toBlockNumber), # Included in genesis eip150Hash: toDigest("0000000000000000000000000000000000000000000000000000000000000000"), eip155Block: some(0.toBlockNumber), # Included in genesis eip158Block: some(0.toBlockNumber), # Included in genesis byzantiumBlock: some(0.toBlockNumber), # Included in genesis constantinopleBlock: some(0.toBlockNumber), # Included in genesis petersburgBlock: some(0.toBlockNumber), # Included in genesis istanbulBlock: some(1_561_651.toBlockNumber), # 2019-10-30 13:53:05 UTC muirGlacierBlock: some(4_460_644.toBlockNumber), # Skipped in Goerli berlinBlock: some(4_460_644.toBlockNumber), # 2021-03-18 05:29:51 UTC londonBlock: some(5_062_605.toBlockNumber), # 2021-07-01 03:19:39 UTC terminalTotalDifficulty: some(10790000.u256), shanghaiTime: some(1_678_832_736.EthTime) ) of SepoliaNet: const sepoliaTTD = parse("17000000000000000",UInt256) ChainConfig( consensusType: ConsensusType.POW, chainId: SepoliaNet.ChainId, homesteadBlock: some(0.toBlockNumber), daoForkSupport: false, eip150Block: some(0.toBlockNumber), eip150Hash: toDigest("0000000000000000000000000000000000000000000000000000000000000000"), eip155Block: some(0.toBlockNumber), eip158Block: some(0.toBlockNumber), byzantiumBlock: some(0.toBlockNumber), constantinopleBlock: some(0.toBlockNumber), petersburgBlock: some(0.toBlockNumber), istanbulBlock: some(0.toBlockNumber), muirGlacierBlock: some(0.toBlockNumber), berlinBlock: some(0.toBlockNumber), londonBlock: some(0.toBlockNumber), mergeForkBlock: some(1735371.toBlockNumber), terminalTotalDifficulty: some(sepoliaTTD), shanghaiTime: some(1_677_557_088.EthTime), ) of HoleskyNet: ChainConfig( consensusType: ConsensusType.POS, chainId: HoleskyNet.ChainId, homesteadBlock: some(0.toBlockNumber), eip150Block: some(0.toBlockNumber), eip155Block: some(0.toBlockNumber), eip158Block: some(0.toBlockNumber), byzantiumBlock: some(0.toBlockNumber), constantinopleBlock: some(0.toBlockNumber), petersburgBlock: some(0.toBlockNumber), istanbulBlock: some(0.toBlockNumber), berlinBlock: some(0.toBlockNumber), londonBlock: some(0.toBlockNumber), mergeForkBlock: some(0.toBlockNumber), terminalTotalDifficulty: some(0.u256), terminalTotalDifficultyPassed: some(true), shanghaiTime: some(1_696_000_704.EthTime), ) else: ChainConfig() proc genesisBlockForNetwork*(id: NetworkId): Genesis {.gcsafe, raises: [ValueError, RlpError].} = result = case id of MainNet: Genesis( nonce: 66.toBlockNonce, extraData: hexToSeqByte("0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"), gasLimit: 5000, difficulty: 17179869184.u256, alloc: decodePrealloc(mainnetAllocData) ) of GoerliNet: Genesis( nonce: 0.toBlockNonce, timestamp: EthTime(0x5c51a607), extraData: hexToSeqByte("0x22466c6578692069732061207468696e6722202d204166726900000000000000e0a2bd4258d2768837baa26a28fe71dc079f84c70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), gasLimit: 0xa00000, difficulty: 1.u256, alloc: decodePrealloc(goerliAllocData) ) of SepoliaNet: Genesis( nonce: 0.toBlockNonce, timestamp: EthTime(0x6159af19), extraData: hexToSeqByte("0x5365706f6c69612c20417468656e732c204174746963612c2047726565636521"), gasLimit: 0x1c9c380, difficulty: 0x20000.u256, alloc: decodePrealloc(sepoliaAllocData) ) of HoleskyNet: Genesis( difficulty: 0x01.u256, gasLimit: 0x17D7840, nonce: 0x1234.toBlockNonce, timestamp: EthTime(1_695_902_100), alloc: decodePrealloc(holeskyAllocData) ) else: Genesis() proc networkParams*(id: NetworkId): NetworkParams {.gcsafe, raises: [ValueError, RlpError].} = result.genesis = genesisBlockForNetwork(id) result.config = chainConfigForNetwork(id) proc `==`*(a, b: ChainId): bool = a.uint64 == b.uint64 proc `==`*(a, b: Genesis): bool = if a.isNil and b.isNil: return true if a.isNil and not b.isNil: return false if not a.isNil and b.isNil: return false a[] == b[] proc `==`*(a, b: ChainConfig): bool = if a.isNil and b.isNil: return true if a.isNil and not b.isNil: return false if not a.isNil and b.isNil: return false a[] == b[]