mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-02-03 07:45:18 +00:00
Unify tx validation (#2777)
This commit is contained in:
parent
a1c34efed7
commit
cee4368075
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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] =
|
||||
|
82
nimbus/transaction/call_types.nim
Normal file
82
nimbus/transaction/call_types.nim
Normal 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
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user