diff --git a/nimbus/core/executor/process_transaction.nim b/nimbus/core/executor/process_transaction.nim index aa91e9a7f..5a4d7ffd5 100644 --- a/nimbus/core/executor/process_transaction.nim +++ b/nimbus/core/executor/process_transaction.nim @@ -11,6 +11,7 @@ {.push raises: [].} import + std/strutils, ../../common/common, ../../db/accounts_cache, ../../transaction/call_evm, @@ -19,7 +20,6 @@ import ../../vm_types, ../../evm/async/operations, ../validate, - chronicles, chronos, stew/results @@ -38,18 +38,18 @@ proc commitOrRollbackDependingOnGasUsed( vmState: BaseVMState, accTx: SavePoint, header: BlockHeader, tx: Transaction, gasBurned: GasInt, priorityFee: GasInt): - Result[GasInt, void] {.raises: [].} = + Result[GasInt, string] {.raises: [].} = # Make sure that the tx does not exceed the maximum cumulative limit as # set in the block header. Again, the eip-1559 reference does not mention # an early stop. It would rather detect differing values for the block # header `gasUsed` and the `vmState.cumulativeGasUsed` at a later stage. if header.gasLimit < vmState.cumulativeGasUsed + gasBurned: - vmState.stateDB.rollback(accTx) - debug "invalid tx: block header gasLimit reached", - maxLimit = header.gasLimit, - gasUsed = vmState.cumulativeGasUsed, - addition = gasBurned - return err() + try: + vmState.stateDB.rollback(accTx) + return err("invalid tx: block header gasLimit reached. gasLimit=$1, gasUsed=$2, addition=$3" % [ + $header.gasLimit, $vmState.cumulativeGasUsed, $gasBurned]) + except ValueError as ex: + return err(ex.msg) else: # Accept transaction and collect mining fee. vmState.stateDB.commit(accTx) @@ -66,15 +66,12 @@ proc asyncProcessTransactionImpl( tx: Transaction; ## Transaction to validate sender: EthAddress; ## tx.getSender or tx.ecRecover header: BlockHeader; ## Header for the block containing the current tx - fork: EVMFork): Future[Result[GasInt,void]] + fork: EVMFork): Future[Result[GasInt, string]] # wildcard exception, wrapped below {.async, gcsafe.} = ## Modelled after `https://eips.ethereum.org/EIPS/eip-1559#specification`_ ## which provides a backward compatible framwork for EIP1559. - #trace "Sender", sender - #trace "txHash", rlpHash = ty.rlpHash - let roDB = vmState.readOnlyStateDB baseFee256 = header.eip1559BaseFee(fork) @@ -83,7 +80,7 @@ proc asyncProcessTransactionImpl( priorityFee = min(tx.maxPriorityFee, tx.maxFee - baseFee) # Return failure unless explicitely set `ok()` - var res: Result[GasInt,void] = err() + var res: Result[GasInt, string] = err("") await ifNecessaryGetAccounts(vmState, @[sender, vmState.coinbase()]) if tx.to.isSome: @@ -91,10 +88,9 @@ proc asyncProcessTransactionImpl( # buy gas, then the gas goes into gasMeter if vmState.gasPool < tx.gasLimit: - debug "gas limit reached", - gasLimit = vmState.gasPool, - gasNeeded = tx.gasLimit - return + return err("gas limit reached. gasLimit=$1, gasNeeded=$2" % [ + $vmState.gasPool, $tx.gasLimit]) + vmState.gasPool -= tx.gasLimit # Actually, the eip-1559 reference does not mention an early exit. @@ -103,7 +99,8 @@ proc asyncProcessTransactionImpl( # before leaving is crucial for some unit tests that us a direct/deep call # of the `processTransaction()` function. So there is no `return err()` # statement, here. - if roDB.validateTransaction(tx, sender, header.gasLimit, baseFee256, fork): + let txRes = roDB.validateTransaction(tx, sender, header.gasLimit, baseFee256, fork) + if txRes.isOk: # Execute the transaction. let @@ -111,6 +108,8 @@ proc asyncProcessTransactionImpl( gasBurned = tx.txCallEvm(sender, vmState, fork) res = commitOrRollbackDependingOnGasUsed(vmState, accTx, header, tx, gasBurned, priorityFee) + else: + res = err(txRes.error) if vmState.generateWitness: vmState.stateDB.collectWitnessData() @@ -129,7 +128,7 @@ proc asyncProcessTransaction*( tx: Transaction; ## Transaction to validate sender: EthAddress; ## tx.getSender or tx.ecRecover header: BlockHeader; ## Header for the block containing the current tx - fork: EVMFork): Future[Result[GasInt,void]] + fork: EVMFork): Future[Result[GasInt,string]] {.async, gcsafe.} = ## Process the transaction, write the results to accounts db. The function ## returns the amount of gas burned if executed. @@ -140,7 +139,7 @@ proc asyncProcessTransaction*( vmState: BaseVMState; ## Parent accounts environment for transaction tx: Transaction; ## Transaction to validate sender: EthAddress; ## tx.getSender or tx.ecRecover - header: BlockHeader): Future[Result[GasInt,void]] + header: BlockHeader): Future[Result[GasInt,string]] {.async, gcsafe.} = ## Variant of `asyncProcessTransaction()` with `*fork* derived ## from the `vmState` argument. @@ -152,7 +151,7 @@ proc processTransaction*( tx: Transaction; ## Transaction to validate sender: EthAddress; ## tx.getSender or tx.ecRecover header: BlockHeader; ## Header for the block containing the current tx - fork: EVMFork): Result[GasInt,void] + fork: EVMFork): Result[GasInt,string] {.gcsafe, raises: [CatchableError].} = return waitFor(vmState.asyncProcessTransaction(tx, sender, header, fork)) @@ -160,7 +159,7 @@ proc processTransaction*( vmState: BaseVMState; ## Parent accounts environment for transaction tx: Transaction; ## Transaction to validate sender: EthAddress; ## tx.getSender or tx.ecRecover - header: BlockHeader): Result[GasInt,void] + header: BlockHeader): Result[GasInt,string] {.gcsafe, raises: [CatchableError].} = return waitFor(vmState.asyncProcessTransaction(tx, sender, header)) diff --git a/nimbus/core/tx_pool/tx_tasks/tx_classify.nim b/nimbus/core/tx_pool/tx_tasks/tx_classify.nim index be1ad4a2d..51ba6568a 100644 --- a/nimbus/core/tx_pool/tx_tasks/tx_classify.nim +++ b/nimbus/core/tx_pool/tx_tasks/tx_classify.nim @@ -234,7 +234,7 @@ proc classifyValidatePacked*(xp: TxPoolRef; xp.chain.limits.trgLimit tx = item.tx.eip1559TxNormalization(xp.chain.baseFee.GasInt, fork) - roDB.validateTransaction(tx, item.sender, gasLimit, baseFee, fork) + roDB.validateTransaction(tx, item.sender, gasLimit, baseFee, fork).isOk proc classifyPacked*(xp: TxPoolRef; gasBurned, moreBurned: GasInt): bool = ## Classifier for *packing* (i.e. adding up `gasUsed` values after executing diff --git a/nimbus/core/validate.nim b/nimbus/core/validate.nim index ef11b3751..cfc60acf6 100644 --- a/nimbus/core/validate.nim +++ b/nimbus/core/validate.nim @@ -9,21 +9,16 @@ # according to those terms. import - std/[sequtils, sets, times], + std/[sequtils, sets, times, strutils], ../common/common, ../db/accounts_cache, ".."/[errors, transaction, vm_state, vm_types], "."/[dao, eip4844, gaslimit, withdrawals], ./pow/[difficulty, header], ./pow, - chronicles, + nimcrypto/utils, stew/[objects, results] -# chronicles stuff -when loggingEnabled or enabledLogLevel > NONE: - import - nimcrypto/utils - from stew/byteutils import nil @@ -51,31 +46,28 @@ func isGenesis(header: BlockHeader): bool = # ------------------------------------------------------------------------------ proc validateSeal(pow: PowRef; header: BlockHeader): Result[void,string] = - let (expMixDigest, miningValue) = try: - pow.getPowDigest(header) + try: + let (expMixDigest, miningValue) = pow.getPowDigest(header) + + if expMixDigest != header.mixDigest: + let + miningHash = header.getPowSpecs.miningHash + (size, cachedHash) = try: pow.getPowCacheLookup(header.blockNumber) + except KeyError: return err("Unknown block") + except CatchableError as e: return err(e.msg) + return err("mixHash mismatch. actual=$1, expected=$2," & + " blockNumber=$3, miningHash=$4, nonce=$5, difficulty=$6," & + " size=$7, cachedHash=$8" % [ + $header.mixDigest, $expMixDigest, $header.blockNumber, + $miningHash, header.nonce.toHex, $header.difficulty, + $size, $cachedHash]) + + let value = UInt256.fromBytesBE(miningValue.data) + if value > UInt256.high div header.difficulty: + return err("mining difficulty error") + except CatchableError as err: - return err("test") - - if expMixDigest != header.mixDigest: - let - miningHash = header.getPowSpecs.miningHash - (size, cachedHash) = try: pow.getPowCacheLookup(header.blockNumber) - except KeyError: return err("Unknown block") - except CatchableError as e: return err(e.msg) - debug "mixHash mismatch", - actual = header.mixDigest, - expected = expMixDigest, - blockNumber = header.blockNumber, - miningHash = miningHash, - nonce = header.nonce.toHex, - difficulty = header.difficulty, - size = size, - cachedHash = cachedHash - return err("mixHash mismatch") - - let value = UInt256.fromBytesBE(miningValue.data) - if value > UInt256.high div header.difficulty: - return err("mining difficulty error") + return err(err.msg) ok() @@ -249,22 +241,19 @@ proc validateTransaction*( sender: EthAddress; ## tx.getSender or tx.ecRecover maxLimit: GasInt; ## gasLimit from block header baseFee: UInt256; ## baseFee from block header - fork: EVMFork): bool = + fork: EVMFork): Result[void, string] = let balance = roDB.getBalance(sender) nonce = roDB.getNonce(sender) if tx.txType == TxEip2930 and fork < FkBerlin: - debug "invalid tx: Eip2930 Tx type detected before Berlin" - return false + return err("invalid tx: Eip2930 Tx type detected before Berlin") if tx.txType == TxEip1559 and fork < FkLondon: - debug "invalid tx: Eip1559 Tx type detected before London" - return false + return err("invalid tx: Eip1559 Tx type detected before London") if fork >= FkShanghai and tx.contractCreation and tx.payload.len > EIP3860_MAX_INITCODE_SIZE: - debug "invalid tx: initcode size exceeds maximum" - return false + return err("invalid tx: initcode size exceeds maximum") # Note that the following check bears some plausibility but is _not_ # covered by the eip-1559 reference (sort of) pseudo code, for details @@ -281,81 +270,66 @@ proc validateTransaction*( # # The parallel lowGasLimit.json test never triggers the case checked below # as the paricular transaction is omitted (the txs list is just set empty.) - if maxLimit < tx.gasLimit: - debug "invalid tx: block header gasLimit exceeded", - maxLimit, - gasLimit = tx.gasLimit - return false + try: + if maxLimit < tx.gasLimit: + return err("invalid tx: block header gasLimit exceeded. maxLimit=$1, gasLimit=$2" % [ + $maxLimit, $tx.gasLimit]) - # ensure that the user was willing to at least pay the base fee - if tx.maxFee < baseFee.truncate(int64): - debug "invalid tx: maxFee is smaller than baseFee", - maxFee = tx.maxFee, - baseFee - return false + # ensure that the user was willing to at least pay the base fee + if tx.maxFee < baseFee.truncate(int64): + return err("invalid tx: maxFee is smaller than baseFee. maxFee=$1, baseFee=$2" % [ + $tx.maxFee, $baseFee]) - # The total must be the larger of the two - if tx.maxFee < tx.maxPriorityFee: - debug "invalid tx: maxFee is smaller than maPriorityFee", - maxFee = tx.maxFee, - maxPriorityFee = tx.maxPriorityFee - return false + # The total must be the larger of the two + if tx.maxFee < tx.maxPriorityFee: + return err("invalid tx: maxFee is smaller than maPriorityFee. maxFee=$1, maxPriorityFee=$2" % [ + $tx.maxFee, $tx.maxPriorityFee]) - # the signer must be able to fully afford the transaction - let gasCost = if tx.txType >= TxEip1559: - tx.gasLimit.u256 * tx.maxFee.u256 - else: - tx.gasLimit.u256 * tx.gasPrice.u256 + # the signer must be able to fully afford the transaction + let gasCost = if tx.txType >= TxEip1559: + tx.gasLimit.u256 * tx.maxFee.u256 + else: + tx.gasLimit.u256 * tx.gasPrice.u256 - if balance < gasCost: - debug "invalid tx: not enough cash for gas", - available = balance, - require = gasCost - return false + if balance < gasCost: + return err("invalid tx: not enough cash for gas. avail=$1, require=$2" % [ + $balance, $gasCost]) - if balance - gasCost < tx.value: - debug "invalid tx: not enough cash to send", - available=balance, - availableMinusGas=balance-gasCost, - require=tx.value - return false + if balance - gasCost < tx.value: + return err("invalid tx: not enough cash to send. avail=$1, availMinusGas=$2, require=$3" % [ + $balance, $(balance-gasCost), $tx.value]) - if tx.gasLimit < tx.intrinsicGas(fork): - debug "invalid tx: not enough gas to perform calculation", - available=tx.gasLimit, - require=tx.intrinsicGas(fork) - return false + if tx.gasLimit < tx.intrinsicGas(fork): + return err("invalid tx: not enough gas to perform calculation. avail=$1, require=$2" % [ + $tx.gasLimit, $tx.intrinsicGas(fork)]) - if tx.nonce != nonce: - debug "invalid tx: account nonce mismatch", - txNonce=tx.nonce, - accountNonce=nonce - return false + if tx.nonce != nonce: + return err("invalid tx: account nonce mismatch. txNonce=$1, accNonce=$2" % [ + $tx.nonce, $nonce]) - if tx.nonce == high(uint64): - debug "invalid tx: nonce at maximum" - return false + if tx.nonce == high(uint64): + return err("invalid tx: nonce at maximum") - # EIP-3607 Reject transactions from senders with deployed code - # The EIP spec claims this attack never happened before - # Clients might choose to disable this rule for RPC calls like - # `eth_call` and `eth_estimateGas` - # EOA = Externally Owned Account - let codeHash = roDB.getCodeHash(sender) - if codeHash != EMPTY_SHA3: - debug "invalid tx: sender is not an EOA", - sender=sender.toHex, - codeHash=codeHash.data.toHex - return false + # EIP-3607 Reject transactions from senders with deployed code + # The EIP spec claims this attack never happened before + # Clients might choose to disable this rule for RPC calls like + # `eth_call` and `eth_estimateGas` + # EOA = Externally Owned Account + let codeHash = roDB.getCodeHash(sender) + if codeHash != EMPTY_SHA3: + return err("invalid tx: sender is not an EOA. sender=$1, codeHash=$2" % [ + sender.toHex, codeHash.data.toHex]) + except CatchableError as ex: + return err(ex.msg) - true + ok() proc validateTransaction*( vmState: BaseVMState; ## Parent accounts environment for transaction tx: Transaction; ## tx to validate sender: EthAddress; ## tx.getSender or tx.ecRecover header: BlockHeader; ## Header for the block containing the current tx - fork: EVMFork): bool = + fork: EVMFork): Result[void, string] = ## Variant of `validateTransaction()` let roDB = vmState.readOnlyStateDB diff --git a/tools/t8n/transition.nim b/tools/t8n/transition.nim index e985a4ae3..1c4ae9d4a 100644 --- a/tools/t8n/transition.nim +++ b/tools/t8n/transition.nim @@ -228,7 +228,7 @@ proc exec(ctx: var TransContext, if rc.isErr: rejected.add RejectedTx( index: txIndex, - error: "processTransaction failed" + error: rc.error ) continue