# Nimbus # Copyright (c) 2021-2024 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, times, macros], eth/[common, rlp, p2p], stint, stew/[byteutils], json_serialization, chronicles, json_serialization/stew/results, json_serialization/lexer, "."/[genesis_alloc, hardforks] export hardforks type Genesis* = ref object nonce* : BlockNonce timestamp* : EthTime extraData* : seq[byte] gasLimit* : GasInt difficulty* : DifficultyInt mixHash* : Hash256 coinbase* : EthAddress alloc* : GenesisAlloc number* : BlockNumber gasUser* : GasInt parentHash* : Hash256 baseFeePerGas*: Opt[UInt256] # EIP-1559 blobGasUsed* : Opt[uint64] # EIP-4844 excessBlobGas*: Opt[uint64] # EIP-4844 parentBeaconBlockRoot*: Opt[Hash256] # EIP-4788 GenesisAlloc* = Table[EthAddress, GenesisAccount] GenesisStorage* = Table[UInt256, UInt256] GenesisAccount* = object code* : seq[byte] storage*: GenesisStorage balance*: UInt256 nonce* : AccountNonce NetworkParams* = object config* : ChainConfig genesis*: Genesis const CustomNet* = 0.NetworkId # these are public network id MainNet* = 1.NetworkId SepoliaNet* = 11155111.NetworkId HoleskyNet* = 17000.NetworkId createJsonFlavor JGenesis, automaticObjectSerialization = false, requireAllFields = false, omitOptionalFields = true, allowUnknownFields = true, skipNullFields = true template derefType(T: type): untyped = typeof(T()[]) NetworkParams.useDefaultReaderIn JGenesis GenesisAccount.useDefaultReaderIn JGenesis derefType(Genesis).useDefaultReaderIn JGenesis derefType(ChainConfig).useDefaultReaderIn JGenesis # ------------------------------------------------------------------------------ # Private helper functions # ------------------------------------------------------------------------------ # used by chronicles json writer proc writeValue(writer: var JsonWriter, value: Opt[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()` :) func 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 template wrapError(body: untyped) = try: body except ValueError as ex: raiseUnexpectedValue(reader, ex.msg) proc readValue(reader: var JsonReader[JGenesis], value: var UInt256) {.gcsafe, raises: [SerializationError, IOError].} = ## Mixin for `JGenesis.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) let tokKind = reader.tokKind if tokKind == JsonValueKind.Number: try: reader.customIntValueIt: accu = accu * 10 + it.u256 except CatchableError: ok = false elif tokKind == JsonValueKind.String: try: var (sLen, base) = (0, 10) reader.customStringValueIt: 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 proc readValue(reader: var JsonReader[JGenesis], value: var ChainId) {.gcsafe, raises: [SerializationError, IOError].} = value = reader.readValue(int).ChainId proc readValue(reader: var JsonReader[JGenesis], value: var Hash256) {.gcsafe, raises: [SerializationError, IOError].} = value = Hash256.fromHex(reader.readValue(string)) proc readValue(reader: var JsonReader[JGenesis], value: var BlockNonce) {.gcsafe, raises: [SerializationError, IOError].} = wrapError: value = fromHex[uint64](reader.readValue(string)).toBlockNonce # genesis timestamp is in hex/dec proc readValue(reader: var JsonReader[JGenesis], value: var EthTime) {.gcsafe, raises: [SerializationError, IOError].} = wrapError: let data = reader.readValue(string) if data.len > 2 and data[1] == 'x': value = fromHex[int64](data).EthTime else: # TODO: use safer uint64 parser value = parseInt(data).EthTime # but shanghaiTime and cancunTime in config is in int literal proc readValue(reader: var JsonReader[JGenesis], value: var Opt[EthTime]) {.gcsafe, raises: [IOError, JsonReaderError].} = if reader.tokKind == JsonValueKind.Null: reset value reader.parseNull() else: # both readValue(GasInt/AccountNonce) will be called if # we use readValue(int64/uint64) let val = EthTime reader.parseInt(uint64) value = Opt.some val proc readValue(reader: var JsonReader[JGenesis], value: var seq[byte]) {.gcsafe, raises: [SerializationError, IOError].} = wrapError: value = hexToSeqByte(reader.readValue(string)) proc readValue(reader: var JsonReader[JGenesis], value: var EthAddress) {.gcsafe, raises: [SerializationError, IOError].} = wrapError: value = parseAddress(reader.readValue(string)) proc readValue(reader: var JsonReader[JGenesis], value: var uint64) {.gcsafe, raises: [SerializationError, IOError].} = wrapError: if reader.tokKind == JsonValueKind.Number: value = reader.parseInt(uint64) else: let data = reader.readValue(string) if data.len > 2 and data[1] == 'x': value = fromHex[uint64](data) else: # TODO: use safer uint64 parser value = parseInt(data).uint64 proc readValue(reader: var JsonReader[JGenesis], value: var GenesisStorage) {.gcsafe, raises: [SerializationError, IOError].} = wrapError: for key in reader.readObjectFields: value[UInt256.fromHex(key)] = reader.readValue(UInt256) proc readValue(reader: var JsonReader[JGenesis], value: var GenesisAlloc) {.gcsafe, raises: [SerializationError, IOError].} = wrapError: for key in reader.readObjectFields: value[parseAddress(key)] = reader.readValue(GenesisAccount) 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 # ------------------------------------------------------------------------------ func 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 proc parseGenesis*(data: string): Genesis {.gcsafe.} = try: result = JGenesis.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 = JGenesis.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 = JGenesis.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 = JGenesis.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 = JGenesis.decode(data, GenesisAlloc, allowUnknownFields = true) except JsonReaderError as e: error "Invalid genesis config file format", msg=e.formatMsg("") return false return true func 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.BlockNumber, # 2015-09-07 21:33:09 UTC homesteadBlock: Opt.some(1_150_000.BlockNumber), # 2016-03-14 18:49:53 UTC daoForkBlock: Opt.some(1_920_000.BlockNumber), # 2016-07-20 13:20:40 UTC daoForkSupport: true, eip150Block: Opt.some(2_463_000.BlockNumber), # 2016-10-18 13:19:31 UTC eip150Hash: toDigest("2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0"), eip155Block: Opt.some(2_675_000.BlockNumber), # Same as EIP-158 eip158Block: Opt.some(2_675_000.BlockNumber), # 2016-11-22 16:15:44 UTC byzantiumBlock: Opt.some(4_370_000.BlockNumber), # 2017-10-16 05:22:11 UTC constantinopleBlock: Opt.some(7_280_000.BlockNumber), # Skipped on Mainnet petersburgBlock: Opt.some(7_280_000.BlockNumber), # 2019-02-28 19:52:04 UTC istanbulBlock: Opt.some(9_069_000.BlockNumber), # 2019-12-08 00:25:09 UTC muirGlacierBlock: Opt.some(9_200_000.BlockNumber), # 2020-01-02 08:30:49 UTC berlinBlock: Opt.some(12_244_000.BlockNumber), # 2021-04-15 10:07:03 UTC londonBlock: Opt.some(12_965_000.BlockNumber), # 2021-08-05 12:33:42 UTC arrowGlacierBlock: Opt.some(13_773_000.BlockNumber), # 2021-12-09 19:55:23 UTC grayGlacierBlock: Opt.some(15_050_000.BlockNumber), # 2022-06-30 10:54:04 UTC terminalTotalDifficulty: Opt.some(mainNetTTD), shanghaiTime: Opt.some(1_681_338_455.EthTime), # 2023-04-12 10:27:35 UTC cancunTime: Opt.some(1_710_338_135.EthTime), # 2024-03-13 13:55:35 UTC ) of SepoliaNet: const sepoliaTTD = parse("17000000000000000",UInt256) ChainConfig( consensusType: ConsensusType.POW, chainId: SepoliaNet.ChainId, homesteadBlock: Opt.some(0.BlockNumber), daoForkSupport: false, eip150Block: Opt.some(0.BlockNumber), eip150Hash: toDigest("0000000000000000000000000000000000000000000000000000000000000000"), eip155Block: Opt.some(0.BlockNumber), eip158Block: Opt.some(0.BlockNumber), byzantiumBlock: Opt.some(0.BlockNumber), constantinopleBlock: Opt.some(0.BlockNumber), petersburgBlock: Opt.some(0.BlockNumber), istanbulBlock: Opt.some(0.BlockNumber), muirGlacierBlock: Opt.some(0.BlockNumber), berlinBlock: Opt.some(0.BlockNumber), londonBlock: Opt.some(0.BlockNumber), mergeForkBlock: Opt.some(1735371.BlockNumber), terminalTotalDifficulty: Opt.some(sepoliaTTD), shanghaiTime: Opt.some(1_677_557_088.EthTime), cancunTime: Opt.some(1_706_655_072.EthTime), # 2024-01-30 22:51:12 ) of HoleskyNet: ChainConfig( consensusType: ConsensusType.POS, chainId: HoleskyNet.ChainId, homesteadBlock: Opt.some(0.BlockNumber), eip150Block: Opt.some(0.BlockNumber), eip155Block: Opt.some(0.BlockNumber), eip158Block: Opt.some(0.BlockNumber), byzantiumBlock: Opt.some(0.BlockNumber), constantinopleBlock: Opt.some(0.BlockNumber), petersburgBlock: Opt.some(0.BlockNumber), istanbulBlock: Opt.some(0.BlockNumber), berlinBlock: Opt.some(0.BlockNumber), londonBlock: Opt.some(0.BlockNumber), mergeForkBlock: Opt.some(0.BlockNumber), terminalTotalDifficulty: Opt.some(0.u256), terminalTotalDifficultyPassed: Opt.some(true), shanghaiTime: Opt.some(1_696_000_704.EthTime), cancunTime: Opt.some(1_707_305_664.EthTime), # 2024-02-07 11:34:24 ) else: ChainConfig() func 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 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() func networkParams*(id: NetworkId): NetworkParams {.gcsafe, raises: [ValueError, RlpError].} = result.genesis = genesisBlockForNetwork(id) result.config = chainConfigForNetwork(id) func `==`*(a, b: ChainId): bool = a.uint64 == b.uint64 func `==`*(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[] func `==`*(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[]