diff --git a/tools/t8n/helpers.nim b/tools/t8n/helpers.nim index 857cb8faf..c99af1d66 100644 --- a/tools/t8n/helpers.nim +++ b/tools/t8n/helpers.nim @@ -10,13 +10,14 @@ import std/[json, strutils, tables], - stew/byteutils, + stew/[byteutils, results], stint, eth/[common, rlp, keys], ../../nimbus/transaction, ../../nimbus/common/chain_config, ../common/helpers, - ./types + ./types, + ./txpriv export helpers @@ -150,6 +151,9 @@ proc parseEnv*(ctx: var TransContext, n: JsonNode) = omitZero(ctx.env, EthTime, parentTimestamp) optional(ctx.env, UInt256, currentBaseFee) omitZero(ctx.env, Hash256, parentUncleHash) + optional(ctx.env, UInt256, parentBaseFee) + optional(ctx.env, GasInt, parentGasUsed) + optional(ctx.env, GasInt, parentGasLimit) if n.hasKey("blockHashes"): let w = n["blockHashes"] @@ -203,24 +207,79 @@ proc parseTx(n: JsonNode, chainId: ChainID): Transaction = else: tx -proc parseTxs*(ctx: var TransContext, txs: JsonNode, chainId: ChainID) = +proc parseTxLegacy(item: var Rlp): Result[Transaction, string] = + try: + var tx: Transaction + item.readTxLegacy(tx) + return ok(tx) + except RlpError as x: + return err(x.msg) + +proc parseTxTyped(item: var Rlp): Result[Transaction, string] = + try: + var tx: Transaction + var rr = rlpFromBytes(item.read(Blob)) + rr.readTxTyped(tx) + return ok(tx) + except RlpError as x: + return err(x.msg) + +proc parseTxJson(ctx: TransContext, i: int, chainId: ChainId): Result[Transaction, string] = + try: + let n = ctx.txs.n[i] + return ok(parseTx(n, chainId)) + except Exception as x: + return err(x.msg) + +proc parseTxs*(ctx: TransContext, chainId: ChainId): seq[Result[Transaction, string]] = + if ctx.txs.txsType == TxsJson: + let len = ctx.txs.n.len + result = newSeqOfCap[Result[Transaction, string]](len) + for i in 0 ..< len: + result.add ctx.parseTxJson(i, chainId) + return + + if ctx.txs.txsType == TxsRlp: + result = newSeqOfCap[Result[Transaction, string]](ctx.txs.r.listLen) + var rlp = ctx.txs.r + for item in rlp: + if item.isList: + result.add parseTxLegacy(item) + else: + result.add parseTxTyped(item) + return + +proc txList*(ctx: TransContext, chainId: ChainId): seq[Transaction] = + let list = ctx.parseTxs(chainId) + for txRes in list: + if txRes.isOk: + result.add txRes.get + +proc parseTxs*(ctx: var TransContext, txs: JsonNode) = if txs.kind == JNull: return if txs.kind != JArray: - raise newError(ErrorJson, "Transaction list should be a JSON array, got=" & $txs.kind) - for n in txs: - ctx.txs.add parseTx(n, chainId) + raise newError(ErrorJson, + "Transaction list should be a JSON array, got=" & $txs.kind) + ctx.txs = TxsList( + txsType: TxsJson, + n: txs) proc parseTxsRlp*(ctx: var TransContext, hexData: string) = - let data = hexToSeqByte(hexData) - ctx.txs = rlp.decode(data, seq[Transaction]) + let bytes = hexToSeqByte(hexData) + ctx.txs = TxsList( + txsType: TxsRlp, + r: rlpFromBytes(bytes) + ) + if ctx.txs.r.isList.not: + raise newError(ErrorRlp, "RLP Transaction list should be a list") -proc parseInputFromStdin*(ctx: var TransContext, chainId: ChainId) = +proc parseInputFromStdin*(ctx: var TransContext) = let data = stdin.readAll() let n = json.parseJson(data) if n.hasKey("alloc"): ctx.parseAlloc(n["alloc"]) if n.hasKey("env"): ctx.parseEnv(n["env"]) - if n.hasKey("txs"): ctx.parseTxs(n["txs"], chainId) + if n.hasKey("txs"): ctx.parseTxs(n["txs"]) if n.hasKey("txsRlp"): ctx.parseTxsRlp(n["txsRlp"].getStr()) template stripLeadingZeros(value: string): string = @@ -336,3 +395,5 @@ proc `@@`*(x: ExecutionResult): JsonNode = } if x.rejected.len > 0: result["rejected"] = @@(x.rejected) + if x.currentBaseFee.isSome: + result["currentBaseFee"] = @@(x.currentBaseFee) diff --git a/tools/t8n/transition.nim b/tools/t8n/transition.nim index ccbe281aa..42417c556 100644 --- a/tools/t8n/transition.nim +++ b/tools/t8n/transition.nim @@ -1,6 +1,6 @@ import std/[json, strutils, times, tables, os, sets], - eth/[rlp, trie], + eth/[rlp, trie, eip1559], stint, chronicles, stew/results, "."/[config, types, helpers], ../../nimbus/[vm_types, vm_state, transaction], @@ -11,6 +11,7 @@ import ../../nimbus/core/dao, ../../nimbus/core/executor/[process_transaction, executor_helpers] +import stew/byteutils const wrapExceptionEnabled* {.booldefine.} = true stdinSelector = "stdin" @@ -56,7 +57,10 @@ proc dispatchOutput(ctx: var TransContext, conf: T8NConf, res: ExecOutput) = dis.dispatch(conf.outputBaseDir, conf.outputAlloc, "alloc", @@(res.alloc)) dis.dispatch(conf.outputBaseDir, conf.outputResult, "result", @@(res.result)) - let body = @@(rlp.encode(ctx.txs)) + let chainId = conf.stateChainId.ChainId + let txList = ctx.txList(chainId) + + let body = @@(rlp.encode(txList)) dis.dispatch(conf.outputBaseDir, conf.outputBody, "body", body) if dis.stdout.len > 0: @@ -133,8 +137,10 @@ proc exec(ctx: var TransContext, stateReward: Option[UInt256], header: BlockHeader): ExecOutput = + let txList = ctx.parseTxs(vmState.com.chainId) + var - receipts = newSeqOfCap[TxReceipt](ctx.txs.len) + receipts = newSeqOfCap[TxReceipt](txList.len) rejected = newSeq[RejectedTx]() includedTx = newSeq[Transaction]() @@ -143,10 +149,18 @@ proc exec(ctx: var TransContext, vmState.mutateStateDB: db.applyDAOHardFork() - vmState.receipts = newSeqOfCap[Receipt](ctx.txs.len) + vmState.receipts = newSeqOfCap[Receipt](txList.len) vmState.cumulativeGasUsed = 0 - for txIndex, tx in ctx.txs: + for txIndex, txRes in txList: + if txRes.isErr: + rejected.add RejectedTx( + index: txIndex, + error: txRes.error + ) + continue + + let tx = txRes.get var sender: EthAddress if not tx.getSender(sender): rejected.add RejectedTx( @@ -176,7 +190,13 @@ proc exec(ctx: var TransContext, ) includedTx.add tx + # Add mining reward? (-1 means rewards are disabled) if stateReward.isSome and stateReward.get >= 0: + # Add mining reward. The mining reward may be `0`, which only makes a difference in the cases + # where + # - the coinbase suicided, or + # - there are only 'bad' transactions, which aren't executed. In those cases, + # the coinbase gets no txfee, so isn't created, and thus needs to be touched let blockReward = stateReward.get() var mainReward = blockReward for uncle in ctx.env.ommers: @@ -216,7 +236,8 @@ proc exec(ctx: var TransContext, # geth using both vmContext.Difficulty and vmContext.Random # therefore we cannot use vmState.difficulty currentDifficulty: ctx.env.currentDifficulty, - gasUsed : vmState.cumulativeGasUsed + gasUsed : vmState.cumulativeGasUsed, + currentBaseFee: ctx.env.currentBaseFee ) template wrapException(body: untyped) = @@ -261,6 +282,20 @@ proc parseChainConfig(network: string): ChainConfig = except ValueError as e: raise newError(ErrorConfig, e.msg) +proc calcBaseFee(env: EnvStruct): UInt256 = + if env.parentGasUsed.isNone: + raise newError(ErrorConfig, + "'parentBaseFee' exists but missing 'parentGasUsed' in env section") + + if env.parentGasLimit.isNone: + raise newError(ErrorConfig, + "'parentBaseFee' exists but missing 'parentGasLimit' in env section") + + calcEip1599BaseFee( + env.parentGasLimit.get, + env.parentGasUsed.get, + env.parentBaseFee.get) + proc transitionAction*(ctx: var TransContext, conf: T8NConf) = wrapException: var tracerFlags = { @@ -291,7 +326,7 @@ proc transitionAction*(ctx: var TransContext, conf: T8NConf) = if conf.inputAlloc == stdinSelector or conf.inputEnv == stdinSelector or conf.inputTxs == stdinSelector: - ctx.parseInputFromStdin(com.chainId) + ctx.parseInputFromStdin() if conf.inputAlloc != stdinSelector and conf.inputAlloc.len > 0: let n = json.parseFile(conf.inputAlloc) @@ -307,7 +342,7 @@ proc transitionAction*(ctx: var TransContext, conf: T8NConf) = ctx.parseTxsRlp(data.strip(chars={'"'})) else: let n = json.parseFile(conf.inputTxs) - ctx.parseTxs(n, com.chainId) + ctx.parseTxs(n) let uncleHash = if ctx.env.parentUncleHash == Hash256(): EMPTY_UNCLE_HASH @@ -324,7 +359,12 @@ proc transitionAction*(ctx: var TransContext, conf: T8NConf) = # Sanity check, to not `panic` in state_transition if com.isLondon(ctx.env.currentNumber): - if ctx.env.currentBaseFee.isNone: + if ctx.env.currentBaseFee.isSome: + # Already set, currentBaseFee has precedent over parentBaseFee. + discard + elif ctx.env.parentBaseFee.isSome: + ctx.env.currentBaseFee = some(calcBaseFee(ctx.env)) + else: raise newError(ErrorConfig, "EIP-1559 config but missing 'currentBaseFee' in env section") if com.forkGTE(MergeFork): diff --git a/tools/t8n/txpriv.nim b/tools/t8n/txpriv.nim new file mode 100644 index 000000000..854cb9a86 --- /dev/null +++ b/tools/t8n/txpriv.nim @@ -0,0 +1,96 @@ +import + eth/common + +from stew/objects + import checkedEnumAssign + +# these procs are duplicates of nim-eth/eth_types_rlp.nim +# both `readTxLegacy` and `readTxTyped` are exported here + +template read[T](rlp: var Rlp, val: var T)= + val = rlp.read(type val) + +proc read[T](rlp: var Rlp, val: var Option[T])= + if rlp.blobLen != 0: + val = some(rlp.read(T)) + else: + rlp.skipElem + +proc readTxLegacy*(rlp: var Rlp, tx: var Transaction)= + tx.txType = TxLegacy + rlp.tryEnterList() + rlp.read(tx.nonce) + rlp.read(tx.gasPrice) + rlp.read(tx.gasLimit) + rlp.read(tx.to) + rlp.read(tx.value) + rlp.read(tx.payload) + rlp.read(tx.V) + rlp.read(tx.R) + rlp.read(tx.S) + +proc readTxEip2930(rlp: var Rlp, tx: var Transaction)= + tx.txType = TxEip2930 + rlp.tryEnterList() + tx.chainId = rlp.read(uint64).ChainId + rlp.read(tx.nonce) + rlp.read(tx.gasPrice) + rlp.read(tx.gasLimit) + rlp.read(tx.to) + rlp.read(tx.value) + rlp.read(tx.payload) + rlp.read(tx.accessList) + rlp.read(tx.V) + rlp.read(tx.R) + rlp.read(tx.S) + +proc readTxEip1559(rlp: var Rlp, tx: var Transaction)= + tx.txType = TxEip1559 + rlp.tryEnterList() + tx.chainId = rlp.read(uint64).ChainId + rlp.read(tx.nonce) + rlp.read(tx.maxPriorityFee) + rlp.read(tx.maxFee) + rlp.read(tx.gasLimit) + rlp.read(tx.to) + rlp.read(tx.value) + rlp.read(tx.payload) + rlp.read(tx.accessList) + rlp.read(tx.V) + rlp.read(tx.R) + rlp.read(tx.S) + +proc readTxTyped*(rlp: var Rlp, tx: var Transaction) {.inline.} = + # EIP-2718: We MUST decode the first byte as a byte, not `rlp.read(int)`. + # If decoded with `rlp.read(int)`, bad transaction data (from the network) + # or even just incorrectly framed data for other reasons fails with + # any of these misleading error messages: + # - "Message too large to fit in memory" + # - "Number encoded with a leading zero" + # - "Read past the end of the RLP stream" + # - "Small number encoded in a non-canonical way" + # - "Attempt to read an Int value past the RLP end" + # - "The RLP contains a larger than expected Int value" + if not rlp.isSingleByte: + if not rlp.hasData: + raise newException(MalformedRlpError, + "Transaction expected but source RLP is empty") + raise newException(MalformedRlpError, + "TypedTransaction type byte is out of range, must be 0x00 to 0x7f") + let txType = rlp.getByteValue + rlp.position += 1 + + var txVal: TxType + if checkedEnumAssign(txVal, txType): + case txVal: + of TxEip2930: + rlp.readTxEip2930(tx) + return + of TxEip1559: + rlp.readTxEip1559(tx) + return + else: + discard + + raise newException(UnsupportedRlpError, + "TypedTransaction type must be 1 or 2 in this version, got " & $txType) diff --git a/tools/t8n/types.nim b/tools/t8n/types.nim index 0f77dd624..dcdd69901 100644 --- a/tools/t8n/types.nim +++ b/tools/t8n/types.nim @@ -9,7 +9,7 @@ # according to those terms. import - std/[tables], + std/[tables, json], eth/common, ../../nimbus/common/chain_config, ../common/types @@ -40,10 +40,24 @@ type ommers*: seq[Ommer] currentBaseFee*: Option[UInt256] parentUncleHash*: Hash256 + parentBaseFee*: Option[UInt256] + parentGasUsed*: Option[GasInt] + parentGasLimit*: Option[GasInt] + + TxsType* = enum + TxsNone + TxsRlp + TxsJson + + TxsList* = object + case txsType*: TxsType + of TxsRlp: r*: Rlp + of TxsJson: n*: JsonNode + else: discard TransContext* = object alloc*: GenesisAlloc - txs*: seq[Transaction] + txs*: TxsList env*: EnvStruct RejectedTx* = object @@ -75,6 +89,7 @@ type rejected*: seq[RejectedTx] currentDifficulty*: Option[DifficultyInt] gasUsed*: GasInt + currentBaseFee*: Option[UInt256] const ErrorEVM* = 2.T8NExitCode