bubble up validate transaction error message

This commit is contained in:
jangko 2023-06-12 12:01:48 +07:00
parent 8700d8b1e1
commit 10aabd8ca1
No known key found for this signature in database
GPG Key ID: 31702AE10541E6B9
4 changed files with 94 additions and 121 deletions

View File

@ -11,6 +11,7 @@
{.push raises: [].} {.push raises: [].}
import import
std/strutils,
../../common/common, ../../common/common,
../../db/accounts_cache, ../../db/accounts_cache,
../../transaction/call_evm, ../../transaction/call_evm,
@ -19,7 +20,6 @@ import
../../vm_types, ../../vm_types,
../../evm/async/operations, ../../evm/async/operations,
../validate, ../validate,
chronicles,
chronos, chronos,
stew/results stew/results
@ -38,18 +38,18 @@ proc commitOrRollbackDependingOnGasUsed(
vmState: BaseVMState, accTx: SavePoint, vmState: BaseVMState, accTx: SavePoint,
header: BlockHeader, tx: Transaction, header: BlockHeader, tx: Transaction,
gasBurned: GasInt, priorityFee: GasInt): 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 # 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 # 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 # an early stop. It would rather detect differing values for the block
# header `gasUsed` and the `vmState.cumulativeGasUsed` at a later stage. # header `gasUsed` and the `vmState.cumulativeGasUsed` at a later stage.
if header.gasLimit < vmState.cumulativeGasUsed + gasBurned: if header.gasLimit < vmState.cumulativeGasUsed + gasBurned:
vmState.stateDB.rollback(accTx) try:
debug "invalid tx: block header gasLimit reached", vmState.stateDB.rollback(accTx)
maxLimit = header.gasLimit, return err("invalid tx: block header gasLimit reached. gasLimit=$1, gasUsed=$2, addition=$3" % [
gasUsed = vmState.cumulativeGasUsed, $header.gasLimit, $vmState.cumulativeGasUsed, $gasBurned])
addition = gasBurned except ValueError as ex:
return err() return err(ex.msg)
else: else:
# Accept transaction and collect mining fee. # Accept transaction and collect mining fee.
vmState.stateDB.commit(accTx) vmState.stateDB.commit(accTx)
@ -66,15 +66,12 @@ proc asyncProcessTransactionImpl(
tx: Transaction; ## Transaction to validate tx: Transaction; ## Transaction to validate
sender: EthAddress; ## tx.getSender or tx.ecRecover sender: EthAddress; ## tx.getSender or tx.ecRecover
header: BlockHeader; ## Header for the block containing the current tx 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 # wildcard exception, wrapped below
{.async, gcsafe.} = {.async, gcsafe.} =
## Modelled after `https://eips.ethereum.org/EIPS/eip-1559#specification`_ ## Modelled after `https://eips.ethereum.org/EIPS/eip-1559#specification`_
## which provides a backward compatible framwork for EIP1559. ## which provides a backward compatible framwork for EIP1559.
#trace "Sender", sender
#trace "txHash", rlpHash = ty.rlpHash
let let
roDB = vmState.readOnlyStateDB roDB = vmState.readOnlyStateDB
baseFee256 = header.eip1559BaseFee(fork) baseFee256 = header.eip1559BaseFee(fork)
@ -83,7 +80,7 @@ proc asyncProcessTransactionImpl(
priorityFee = min(tx.maxPriorityFee, tx.maxFee - baseFee) priorityFee = min(tx.maxPriorityFee, tx.maxFee - baseFee)
# Return failure unless explicitely set `ok()` # Return failure unless explicitely set `ok()`
var res: Result[GasInt,void] = err() var res: Result[GasInt, string] = err("")
await ifNecessaryGetAccounts(vmState, @[sender, vmState.coinbase()]) await ifNecessaryGetAccounts(vmState, @[sender, vmState.coinbase()])
if tx.to.isSome: if tx.to.isSome:
@ -91,10 +88,9 @@ proc asyncProcessTransactionImpl(
# buy gas, then the gas goes into gasMeter # buy gas, then the gas goes into gasMeter
if vmState.gasPool < tx.gasLimit: if vmState.gasPool < tx.gasLimit:
debug "gas limit reached", return err("gas limit reached. gasLimit=$1, gasNeeded=$2" % [
gasLimit = vmState.gasPool, $vmState.gasPool, $tx.gasLimit])
gasNeeded = tx.gasLimit
return
vmState.gasPool -= tx.gasLimit vmState.gasPool -= tx.gasLimit
# Actually, the eip-1559 reference does not mention an early exit. # 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 # before leaving is crucial for some unit tests that us a direct/deep call
# of the `processTransaction()` function. So there is no `return err()` # of the `processTransaction()` function. So there is no `return err()`
# statement, here. # 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. # Execute the transaction.
let let
@ -111,6 +108,8 @@ proc asyncProcessTransactionImpl(
gasBurned = tx.txCallEvm(sender, vmState, fork) gasBurned = tx.txCallEvm(sender, vmState, fork)
res = commitOrRollbackDependingOnGasUsed(vmState, accTx, header, tx, gasBurned, priorityFee) res = commitOrRollbackDependingOnGasUsed(vmState, accTx, header, tx, gasBurned, priorityFee)
else:
res = err(txRes.error)
if vmState.generateWitness: if vmState.generateWitness:
vmState.stateDB.collectWitnessData() vmState.stateDB.collectWitnessData()
@ -129,7 +128,7 @@ proc asyncProcessTransaction*(
tx: Transaction; ## Transaction to validate tx: Transaction; ## Transaction to validate
sender: EthAddress; ## tx.getSender or tx.ecRecover sender: EthAddress; ## tx.getSender or tx.ecRecover
header: BlockHeader; ## Header for the block containing the current tx header: BlockHeader; ## Header for the block containing the current tx
fork: EVMFork): Future[Result[GasInt,void]] fork: EVMFork): Future[Result[GasInt,string]]
{.async, gcsafe.} = {.async, gcsafe.} =
## Process the transaction, write the results to accounts db. The function ## Process the transaction, write the results to accounts db. The function
## returns the amount of gas burned if executed. ## returns the amount of gas burned if executed.
@ -140,7 +139,7 @@ proc asyncProcessTransaction*(
vmState: BaseVMState; ## Parent accounts environment for transaction vmState: BaseVMState; ## Parent accounts environment for transaction
tx: Transaction; ## Transaction to validate tx: Transaction; ## Transaction to validate
sender: EthAddress; ## tx.getSender or tx.ecRecover sender: EthAddress; ## tx.getSender or tx.ecRecover
header: BlockHeader): Future[Result[GasInt,void]] header: BlockHeader): Future[Result[GasInt,string]]
{.async, gcsafe.} = {.async, gcsafe.} =
## Variant of `asyncProcessTransaction()` with `*fork* derived ## Variant of `asyncProcessTransaction()` with `*fork* derived
## from the `vmState` argument. ## from the `vmState` argument.
@ -152,7 +151,7 @@ proc processTransaction*(
tx: Transaction; ## Transaction to validate tx: Transaction; ## Transaction to validate
sender: EthAddress; ## tx.getSender or tx.ecRecover sender: EthAddress; ## tx.getSender or tx.ecRecover
header: BlockHeader; ## Header for the block containing the current tx header: BlockHeader; ## Header for the block containing the current tx
fork: EVMFork): Result[GasInt,void] fork: EVMFork): Result[GasInt,string]
{.gcsafe, raises: [CatchableError].} = {.gcsafe, raises: [CatchableError].} =
return waitFor(vmState.asyncProcessTransaction(tx, sender, header, fork)) return waitFor(vmState.asyncProcessTransaction(tx, sender, header, fork))
@ -160,7 +159,7 @@ proc processTransaction*(
vmState: BaseVMState; ## Parent accounts environment for transaction vmState: BaseVMState; ## Parent accounts environment for transaction
tx: Transaction; ## Transaction to validate tx: Transaction; ## Transaction to validate
sender: EthAddress; ## tx.getSender or tx.ecRecover sender: EthAddress; ## tx.getSender or tx.ecRecover
header: BlockHeader): Result[GasInt,void] header: BlockHeader): Result[GasInt,string]
{.gcsafe, raises: [CatchableError].} = {.gcsafe, raises: [CatchableError].} =
return waitFor(vmState.asyncProcessTransaction(tx, sender, header)) return waitFor(vmState.asyncProcessTransaction(tx, sender, header))

View File

@ -234,7 +234,7 @@ proc classifyValidatePacked*(xp: TxPoolRef;
xp.chain.limits.trgLimit xp.chain.limits.trgLimit
tx = item.tx.eip1559TxNormalization(xp.chain.baseFee.GasInt, fork) 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 = proc classifyPacked*(xp: TxPoolRef; gasBurned, moreBurned: GasInt): bool =
## Classifier for *packing* (i.e. adding up `gasUsed` values after executing ## Classifier for *packing* (i.e. adding up `gasUsed` values after executing

View File

@ -9,21 +9,16 @@
# according to those terms. # according to those terms.
import import
std/[sequtils, sets, times], std/[sequtils, sets, times, strutils],
../common/common, ../common/common,
../db/accounts_cache, ../db/accounts_cache,
".."/[errors, transaction, vm_state, vm_types], ".."/[errors, transaction, vm_state, vm_types],
"."/[dao, eip4844, gaslimit, withdrawals], "."/[dao, eip4844, gaslimit, withdrawals],
./pow/[difficulty, header], ./pow/[difficulty, header],
./pow, ./pow,
chronicles, nimcrypto/utils,
stew/[objects, results] stew/[objects, results]
# chronicles stuff
when loggingEnabled or enabledLogLevel > NONE:
import
nimcrypto/utils
from stew/byteutils from stew/byteutils
import nil import nil
@ -51,31 +46,28 @@ func isGenesis(header: BlockHeader): bool =
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
proc validateSeal(pow: PowRef; header: BlockHeader): Result[void,string] = proc validateSeal(pow: PowRef; header: BlockHeader): Result[void,string] =
let (expMixDigest, miningValue) = try: try:
pow.getPowDigest(header) 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: except CatchableError as err:
return err("test") return err(err.msg)
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")
ok() ok()
@ -249,22 +241,19 @@ proc validateTransaction*(
sender: EthAddress; ## tx.getSender or tx.ecRecover sender: EthAddress; ## tx.getSender or tx.ecRecover
maxLimit: GasInt; ## gasLimit from block header maxLimit: GasInt; ## gasLimit from block header
baseFee: UInt256; ## baseFee from block header baseFee: UInt256; ## baseFee from block header
fork: EVMFork): bool = fork: EVMFork): Result[void, string] =
let let
balance = roDB.getBalance(sender) balance = roDB.getBalance(sender)
nonce = roDB.getNonce(sender) nonce = roDB.getNonce(sender)
if tx.txType == TxEip2930 and fork < FkBerlin: if tx.txType == TxEip2930 and fork < FkBerlin:
debug "invalid tx: Eip2930 Tx type detected before Berlin" return err("invalid tx: Eip2930 Tx type detected before Berlin")
return false
if tx.txType == TxEip1559 and fork < FkLondon: if tx.txType == TxEip1559 and fork < FkLondon:
debug "invalid tx: Eip1559 Tx type detected before London" return err("invalid tx: Eip1559 Tx type detected before London")
return false
if fork >= FkShanghai and tx.contractCreation and tx.payload.len > EIP3860_MAX_INITCODE_SIZE: if fork >= FkShanghai and tx.contractCreation and tx.payload.len > EIP3860_MAX_INITCODE_SIZE:
debug "invalid tx: initcode size exceeds maximum" return err("invalid tx: initcode size exceeds maximum")
return false
# Note that the following check bears some plausibility but is _not_ # Note that the following check bears some plausibility but is _not_
# covered by the eip-1559 reference (sort of) pseudo code, for details # 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 # The parallel lowGasLimit.json test never triggers the case checked below
# as the paricular transaction is omitted (the txs list is just set empty.) # as the paricular transaction is omitted (the txs list is just set empty.)
if maxLimit < tx.gasLimit: try:
debug "invalid tx: block header gasLimit exceeded", if maxLimit < tx.gasLimit:
maxLimit, return err("invalid tx: block header gasLimit exceeded. maxLimit=$1, gasLimit=$2" % [
gasLimit = tx.gasLimit $maxLimit, $tx.gasLimit])
return false
# ensure that the user was willing to at least pay the base fee # ensure that the user was willing to at least pay the base fee
if tx.maxFee < baseFee.truncate(int64): if tx.maxFee < baseFee.truncate(int64):
debug "invalid tx: maxFee is smaller than baseFee", return err("invalid tx: maxFee is smaller than baseFee. maxFee=$1, baseFee=$2" % [
maxFee = tx.maxFee, $tx.maxFee, $baseFee])
baseFee
return false
# The total must be the larger of the two # The total must be the larger of the two
if tx.maxFee < tx.maxPriorityFee: if tx.maxFee < tx.maxPriorityFee:
debug "invalid tx: maxFee is smaller than maPriorityFee", return err("invalid tx: maxFee is smaller than maPriorityFee. maxFee=$1, maxPriorityFee=$2" % [
maxFee = tx.maxFee, $tx.maxFee, $tx.maxPriorityFee])
maxPriorityFee = tx.maxPriorityFee
return false
# the signer must be able to fully afford the transaction # the signer must be able to fully afford the transaction
let gasCost = if tx.txType >= TxEip1559: let gasCost = if tx.txType >= TxEip1559:
tx.gasLimit.u256 * tx.maxFee.u256 tx.gasLimit.u256 * tx.maxFee.u256
else: else:
tx.gasLimit.u256 * tx.gasPrice.u256 tx.gasLimit.u256 * tx.gasPrice.u256
if balance < gasCost: if balance < gasCost:
debug "invalid tx: not enough cash for gas", return err("invalid tx: not enough cash for gas. avail=$1, require=$2" % [
available = balance, $balance, $gasCost])
require = gasCost
return false
if balance - gasCost < tx.value: if balance - gasCost < tx.value:
debug "invalid tx: not enough cash to send", return err("invalid tx: not enough cash to send. avail=$1, availMinusGas=$2, require=$3" % [
available=balance, $balance, $(balance-gasCost), $tx.value])
availableMinusGas=balance-gasCost,
require=tx.value
return false
if tx.gasLimit < tx.intrinsicGas(fork): if tx.gasLimit < tx.intrinsicGas(fork):
debug "invalid tx: not enough gas to perform calculation", return err("invalid tx: not enough gas to perform calculation. avail=$1, require=$2" % [
available=tx.gasLimit, $tx.gasLimit, $tx.intrinsicGas(fork)])
require=tx.intrinsicGas(fork)
return false
if tx.nonce != nonce: if tx.nonce != nonce:
debug "invalid tx: account nonce mismatch", return err("invalid tx: account nonce mismatch. txNonce=$1, accNonce=$2" % [
txNonce=tx.nonce, $tx.nonce, $nonce])
accountNonce=nonce
return false
if tx.nonce == high(uint64): if tx.nonce == high(uint64):
debug "invalid tx: nonce at maximum" return err("invalid tx: nonce at maximum")
return false
# EIP-3607 Reject transactions from senders with deployed code # EIP-3607 Reject transactions from senders with deployed code
# The EIP spec claims this attack never happened before # The EIP spec claims this attack never happened before
# Clients might choose to disable this rule for RPC calls like # Clients might choose to disable this rule for RPC calls like
# `eth_call` and `eth_estimateGas` # `eth_call` and `eth_estimateGas`
# EOA = Externally Owned Account # EOA = Externally Owned Account
let codeHash = roDB.getCodeHash(sender) let codeHash = roDB.getCodeHash(sender)
if codeHash != EMPTY_SHA3: if codeHash != EMPTY_SHA3:
debug "invalid tx: sender is not an EOA", return err("invalid tx: sender is not an EOA. sender=$1, codeHash=$2" % [
sender=sender.toHex, sender.toHex, codeHash.data.toHex])
codeHash=codeHash.data.toHex except CatchableError as ex:
return false return err(ex.msg)
true ok()
proc validateTransaction*( proc validateTransaction*(
vmState: BaseVMState; ## Parent accounts environment for transaction vmState: BaseVMState; ## Parent accounts environment for transaction
tx: Transaction; ## tx to validate tx: Transaction; ## tx to validate
sender: EthAddress; ## tx.getSender or tx.ecRecover sender: EthAddress; ## tx.getSender or tx.ecRecover
header: BlockHeader; ## Header for the block containing the current tx header: BlockHeader; ## Header for the block containing the current tx
fork: EVMFork): bool = fork: EVMFork): Result[void, string] =
## Variant of `validateTransaction()` ## Variant of `validateTransaction()`
let let
roDB = vmState.readOnlyStateDB roDB = vmState.readOnlyStateDB

View File

@ -228,7 +228,7 @@ proc exec(ctx: var TransContext,
if rc.isErr: if rc.isErr:
rejected.add RejectedTx( rejected.add RejectedTx(
index: txIndex, index: txIndex,
error: "processTransaction failed" error: rc.error
) )
continue continue