Unify tx validation (#2777)

This commit is contained in:
andri lim 2024-10-26 14:19:48 +07:00 committed by GitHub
parent a1c34efed7
commit cee4368075
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 134 additions and 194 deletions

View File

@ -13,8 +13,10 @@
import
std/[sequtils, sets, strformat],
../db/ledger,
".."/[transaction, common/common],
".."/[errors],
../common/common,
../transaction/call_types,
../errors,
../transaction,
../utils/utils,
"."/[dao, eip4844, gaslimit, withdrawals],
./pow/[difficulty, header],
@ -182,6 +184,35 @@ proc validateUncles(com: CommonRef; header: Header;
# Public function, extracted from executor
# ------------------------------------------------------------------------------
proc validateLegacySignatureForm(tx: Transaction, fork: EVMFork): bool =
var
vMin = 27'u64
vMax = 28'u64
if tx.V >= EIP155_CHAIN_ID_OFFSET:
let chainId = (tx.V - EIP155_CHAIN_ID_OFFSET) div 2
vMin = 35 + (2 * chainId)
vMax = vMin + 1
var isValid = tx.R >= UInt256.one
isValid = isValid and tx.S >= UInt256.one
isValid = isValid and tx.V >= vMin
isValid = isValid and tx.V <= vMax
isValid = isValid and tx.S < SECPK1_N
isValid = isValid and tx.R < SECPK1_N
if fork >= FkHomestead:
isValid = isValid and tx.S < SECPK1_N div 2
isValid
proc validateEip2930SignatureForm(tx: Transaction): bool =
var isValid = tx.V == 0'u64 or tx.V == 1'u64
isValid = isValid and tx.S >= UInt256.one
isValid = isValid and tx.S < SECPK1_N
isValid = isValid and tx.R < SECPK1_N
isValid
func gasCost*(tx: Transaction): UInt256 =
if tx.txType >= TxEip4844:
tx.gasLimit.u256 * tx.maxFeePerGas.u256 + tx.getTotalBlobGas.u256 * tx.maxFeePerBlobGas
@ -228,6 +259,13 @@ proc validateTxBasic*(
return err("invalid tx: access list storage keys len exceeds MAX_ACCESS_LIST_STORAGE_KEYS. " &
&"index={i}, len={acl.storageKeys.len}")
if tx.txType == TxLegacy:
if not validateLegacySignatureForm(tx, fork):
return err("invalid tx: invalid legacy signature form")
else:
if not validateEip2930SignatureForm(tx):
return err("invalid tx: invalid post EIP-2930 signature form")
if tx.txType >= TxEip4844:
if tx.to.isNone:
return err("invalid tx: destination must be not empty")
@ -248,10 +286,10 @@ proc validateTxBasic*(
proc validateTransaction*(
roDB: ReadOnlyStateDB; ## Parent accounts environment for transaction
tx: Transaction; ## tx to validate
sender: Address; ## tx.recoverSender
sender: Address; ## tx.recoverSender
maxLimit: GasInt; ## gasLimit from block header
baseFee: UInt256; ## baseFee from block header
excessBlobGas: uint64; ## excessBlobGas from parent block header
excessBlobGas: uint64; ## excessBlobGas from parent block header
fork: EVMFork): Result[void, string] =
? validateTxBasic(tx, fork)

View File

@ -6,134 +6,11 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
./[constants, errors],
./common/evmforks,
./evm/interpreter/gas_costs,
./[constants],
eth/common/[addresses, keys, transactions, transactions_rlp, transaction_utils]
export addresses, keys, transactions
proc toWordSize(size: GasInt): GasInt =
# Round input to the nearest bigger multiple of 32
# tx validation will ensure the value is not too big
if size > GasInt.high-31:
return (GasInt.high shr 5) + 1
(size + 31) shr 5
func intrinsicGas*(data: openArray[byte], fork: EVMFork): GasInt =
result = GasInt(gasFees[fork][GasTransaction])
for i in data:
if i == 0:
result += GasInt(gasFees[fork][GasTXDataZero])
else:
result += GasInt(gasFees[fork][GasTXDataNonZero])
proc intrinsicGas*(tx: Transaction, fork: EVMFork): GasInt =
# Compute the baseline gas cost for this transaction. This is the amount
# of gas needed to send this transaction (but that is not actually used
# for computation)
result = tx.payload.intrinsicGas(fork)
if tx.contractCreation:
result += GasInt(gasFees[fork][GasTXCreate])
if fork >= FkShanghai:
# cannot use wordCount here, it will raise unlisted exception
let numWords = toWordSize(GasInt tx.payload.len)
result += GasInt(gasFees[fork][GasInitcodeWord]) * numWords
if tx.txType > TxLegacy:
result += GasInt(tx.accessList.len) * ACCESS_LIST_ADDRESS_COST
var numKeys = 0
for n in tx.accessList:
inc(numKeys, n.storageKeys.len)
result += GasInt(numKeys) * ACCESS_LIST_STORAGE_KEY_COST
proc validateTxLegacy(tx: Transaction, fork: EVMFork) =
var
vMin = 27'u64
vMax = 28'u64
if tx.V >= EIP155_CHAIN_ID_OFFSET:
let chainId = (tx.V - EIP155_CHAIN_ID_OFFSET) div 2
vMin = 35 + (2 * chainId)
vMax = vMin + 1
var isValid = tx.R >= UInt256.one
isValid = isValid and tx.S >= UInt256.one
isValid = isValid and tx.V >= vMin
isValid = isValid and tx.V <= vMax
isValid = isValid and tx.S < SECPK1_N
isValid = isValid and tx.R < SECPK1_N
if fork >= FkHomestead:
isValid = isValid and tx.S < SECPK1_N div 2
if not isValid:
raise newException(ValidationError, "Invalid legacy transaction")
proc validateTxEip2930(tx: Transaction) =
var isValid = tx.V == 0'u64 or tx.V == 1'u64
isValid = isValid and tx.S >= UInt256.one
isValid = isValid and tx.S < SECPK1_N
isValid = isValid and tx.R < SECPK1_N
if not isValid:
raise newException(ValidationError, "Invalid typed transaction")
proc validateTxEip4844(tx: Transaction) =
validateTxEip2930(tx)
var isValid = tx.payload.len <= MAX_CALLDATA_SIZE
isValid = isValid and tx.accessList.len <= MAX_ACCESS_LIST_SIZE
for acl in tx.accessList:
isValid = isValid and
(acl.storageKeys.len <= MAX_ACCESS_LIST_STORAGE_KEYS)
isValid = isValid and
tx.versionedHashes.len <= MAX_BLOBS_PER_BLOCK
for bv in tx.versionedHashes:
isValid = isValid and
bv.data[0] == VERSIONED_HASH_VERSION_KZG
if not isValid:
raise newException(ValidationError, "Invalid EIP-4844 transaction")
proc validateTxEip7702(tx: Transaction) =
validateTxEip2930(tx)
if tx.authorizationList.len == 0:
raise newException(ValidationError, "Invalid EIP-7702 transaction")
proc validate*(tx: Transaction, fork: EVMFork) =
# TODO it doesn't seem like this function is called from anywhere except tests
# which feels like it might be a problem (?)
# parameters pass validation rules
if tx.intrinsicGas(fork) > tx.gasLimit:
raise newException(ValidationError, "Insufficient gas")
if fork >= FkShanghai and tx.contractCreation and tx.payload.len > EIP3860_MAX_INITCODE_SIZE:
raise newException(ValidationError, "Initcode size exceeds max")
# check signature validity
# TODO a validation function like this should probably be returning the sender
# since recovering the public key accounts for ~10% of block processing
# time (at the time of writing)
let sender = tx.recoverSender().valueOr:
raise newException(ValidationError, "Invalid signature or failed message verification")
case tx.txType
of TxLegacy:
validateTxLegacy(tx, fork)
of TxEip4844:
validateTxEip4844(tx)
of TxEip2930, TxEip1559:
validateTxEip2930(tx)
of TxEip7702:
validateTxEip7702(tx)
proc signTransaction*(tx: Transaction, privateKey: PrivateKey, eip155 = true): Transaction =
result = tx
result.signature = result.sign(privateKey, eip155)

View File

@ -18,7 +18,8 @@ import
../db/ledger,
../common/evmforks,
../core/eip4844,
./host_types
./host_types,
./call_types
import ../evm/computation except fromEvmc, toEvmc
@ -30,38 +31,8 @@ else:
import
../evm/state_transactions
type
# Standard call parameters.
CallParams* = object
vmState*: BaseVMState # Chain, database, state, block, fork.
origin*: Opt[HostAddress] # Default origin is `sender`.
gasPrice*: GasInt # Gas price for this call.
gasLimit*: GasInt # Maximum gas available for this call.
sender*: HostAddress # Sender account.
to*: HostAddress # Recipient (ignored when `isCreate`).
isCreate*: bool # True if this is a contract creation.
value*: HostValue # Value sent from sender to recipient.
input*: seq[byte] # Input data.
accessList*: AccessList # EIP-2930 (Berlin) tx access list.
versionedHashes*: seq[VersionedHash] # EIP-4844 (Cancun) blob versioned hashes
noIntrinsic*: bool # Don't charge intrinsic gas.
noAccessList*: bool # Don't initialise EIP-2929 access list.
noGasCharge*: bool # Don't charge sender account for gas.
noRefund*: bool # Don't apply gas refund/burn rule.
sysCall*: bool # System call or ordinary call
# Standard call result. (Some fields are beyond what EVMC can return,
# and must only be used from tests because they will not always be set).
CallResult* = object
error*: string # Something if the call failed.
gasUsed*: GasInt # Gas used by the call.
contractAddress*: Address # Created account (when `isCreate`).
output*: seq[byte] # Output data.
stack*: EvmStack # EVM stack on return (for test only).
memory*: EvmMemory # EVM memory on return (for test only).
func isError*(cr: CallResult): bool =
cr.error.len > 0
export
call_types
proc hostToComputationMessage*(msg: EvmcMessage): Message =
Message(
@ -78,33 +49,6 @@ proc hostToComputationMessage*(msg: EvmcMessage): Message =
flags: msg.flags
)
func intrinsicGas*(call: CallParams, vmState: BaseVMState): GasInt {.inline.} =
# Compute the baseline gas cost for this transaction. This is the amount
# of gas needed to send this transaction (but that is not actually used
# for computation).
let fork = vmState.fork
var gas = gasFees[fork][GasTransaction]
# EIP-2 (Homestead) extra intrinsic gas for contract creations.
if call.isCreate:
gas += gasFees[fork][GasTXCreate]
if fork >= FkShanghai:
gas += (gasFees[fork][GasInitcodeWord] * call.input.len.wordCount)
# Input data cost, reduced in EIP-2028 (Istanbul).
let gasZero = gasFees[fork][GasTXDataZero]
let gasNonZero = gasFees[fork][GasTXDataNonZero]
for b in call.input:
gas += (if b == 0: gasZero else: gasNonZero)
# EIP-2930 (Berlin) intrinsic gas for transaction access list.
if fork >= FkBerlin:
for account in call.accessList:
gas += ACCESS_LIST_ADDRESS_COST
gas += GasInt(account.storageKeys.len) * ACCESS_LIST_STORAGE_KEY_COST
return gas.GasInt
proc initialAccessListEIP2929(call: CallParams) =
# EIP2929 initial access list.
let vmState = call.vmState
@ -143,7 +87,7 @@ proc setupHost(call: CallParams): TransactionHost =
var intrinsicGas: GasInt = 0
if not call.noIntrinsic:
intrinsicGas = intrinsicGas(call, vmState)
intrinsicGas = intrinsicGas(call, vmState.fork)
let host = TransactionHost(
vmState: vmState,

View File

@ -116,7 +116,7 @@ proc rpcEstimateGas*(args: TransactionArgs,
hi = gasCap
cap = hi
let intrinsicGas = intrinsicGas(params, vmState)
let intrinsicGas = intrinsicGas(params, fork)
# Create a helper to check if a gas allowance results in an executable transaction
proc executable(gasLimit: GasInt): EvmResult[bool] =

View File

@ -0,0 +1,82 @@
# Nimbus
# Copyright (c) 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
eth/common/transactions,
../common/evmforks,
../evm/types,
../evm/internals,
./host_types
type
# Standard call parameters.
CallParams* = object
vmState*: BaseVMState # Chain, database, state, block, fork.
origin*: Opt[HostAddress] # Default origin is `sender`.
gasPrice*: GasInt # Gas price for this call.
gasLimit*: GasInt # Maximum gas available for this call.
sender*: HostAddress # Sender account.
to*: HostAddress # Recipient (ignored when `isCreate`).
isCreate*: bool # True if this is a contract creation.
value*: HostValue # Value sent from sender to recipient.
input*: seq[byte] # Input data.
accessList*: AccessList # EIP-2930 (Berlin) tx access list.
versionedHashes*: seq[VersionedHash] # EIP-4844 (Cancun) blob versioned hashes
noIntrinsic*: bool # Don't charge intrinsic gas.
noAccessList*: bool # Don't initialise EIP-2929 access list.
noGasCharge*: bool # Don't charge sender account for gas.
noRefund*: bool # Don't apply gas refund/burn rule.
sysCall*: bool # System call or ordinary call
# Standard call result. (Some fields are beyond what EVMC can return,
# and must only be used from tests because they will not always be set).
CallResult* = object
error*: string # Something if the call failed.
gasUsed*: GasInt # Gas used by the call.
contractAddress*: Address # Created account (when `isCreate`).
output*: seq[byte] # Output data.
stack*: EvmStack # EVM stack on return (for test only).
memory*: EvmMemory # EVM memory on return (for test only).
template isCreate(tx: Transaction): bool =
tx.contractCreation
template input(tx: Transaction): auto =
tx.payload
func isError*(cr: CallResult): bool =
cr.error.len > 0
func intrinsicGas*(call: CallParams | Transaction, fork: EVMFork): GasInt =
# Compute the baseline gas cost for this transaction. This is the amount
# of gas needed to send this transaction (but that is not actually used
# for computation).
var gas = gasFees[fork][GasTransaction]
# EIP-2 (Homestead) extra intrinsic gas for contract creations.
if call.isCreate:
gas += gasFees[fork][GasTXCreate]
if fork >= FkShanghai:
gas += (gasFees[fork][GasInitcodeWord] * call.input.len.wordCount)
# Input data cost, reduced in EIP-2028 (Istanbul).
let gasZero = gasFees[fork][GasTXDataZero]
let gasNonZero = gasFees[fork][GasTXDataNonZero]
for b in call.input:
gas += (if b == 0: gasZero else: gasNonZero)
# EIP-2930 (Berlin) intrinsic gas for transaction access list.
if fork >= FkBerlin:
for account in call.accessList:
gas += ACCESS_LIST_ADDRESS_COST
gas += GasInt(account.storageKeys.len) * ACCESS_LIST_STORAGE_KEY_COST
return gas.GasInt

View File

@ -14,7 +14,8 @@ import
eth/rlp,
./test_helpers,
eth/common/transaction_utils,
../nimbus/[errors, transaction],
../nimbus/transaction,
../nimbus/core/validate,
../nimbus/utils/utils
const
@ -33,9 +34,7 @@ proc txHash(tx: Transaction): string =
rlpHash(tx).toHex()
proc testTxByFork(tx: Transaction, forkData: JsonNode, forkName: string, testStatusIMPL: var TestStatus) =
try:
tx.validate(nameToFork[forkName])
except ValidationError:
tx.validateTxBasic(nameToFork[forkName]).isOkOr:
return
if forkData.len > 0 and "sender" in forkData: