diff --git a/nimbus/core/validate.nim b/nimbus/core/validate.nim index 1c66c9f78..e7933e5e9 100644 --- a/nimbus/core/validate.nim +++ b/nimbus/core/validate.nim @@ -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) diff --git a/nimbus/transaction.nim b/nimbus/transaction.nim index 4b4177bc4..39cff73e6 100644 --- a/nimbus/transaction.nim +++ b/nimbus/transaction.nim @@ -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) diff --git a/nimbus/transaction/call_common.nim b/nimbus/transaction/call_common.nim index fbfdf8e0b..b20e058d2 100644 --- a/nimbus/transaction/call_common.nim +++ b/nimbus/transaction/call_common.nim @@ -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, diff --git a/nimbus/transaction/call_evm.nim b/nimbus/transaction/call_evm.nim index 4b17ea50d..31e2c052a 100644 --- a/nimbus/transaction/call_evm.nim +++ b/nimbus/transaction/call_evm.nim @@ -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] = diff --git a/nimbus/transaction/call_types.nim b/nimbus/transaction/call_types.nim new file mode 100644 index 000000000..0b33a6d30 --- /dev/null +++ b/nimbus/transaction/call_types.nim @@ -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 diff --git a/tests/test_transaction_json.nim b/tests/test_transaction_json.nim index 5060d7b6d..367bfc99f 100644 --- a/tests/test_transaction_json.nim +++ b/tests/test_transaction_json.nim @@ -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: