nimbus-eth1/nimbus/p2p/validate.nim

414 lines
14 KiB
Nim

# Nimbus
# Copyright (c) 2018 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
import
std/[sequtils, sets, times],
../constants,
../db/[db_chain, accounts_cache],
../transaction,
../utils/[difficulty, header, pow],
".."/[vm_state, vm_types, forks, errors],
./dao,
./gaslimit,
chronicles,
eth/[common, rlp],
nimcrypto/utils,
stew/[objects, results, endians2]
from stew/byteutils
import nil
export
pow.PowRef,
pow.new,
results
{.push raises: [Defect].}
const
daoForkBlockExtraData* =
byteutils.hexToByteArray[13](DAOForkBlockExtra).toSeq
# ------------------------------------------------------------------------------
# Private Helpers
# ------------------------------------------------------------------------------
func isGenesis(header: BlockHeader): bool =
header.blockNumber == 0.u256 and
header.parentHash == GENESIS_PARENT_HASH
# ------------------------------------------------------------------------------
# Pivate validator functions
# ------------------------------------------------------------------------------
proc validateSeal(pow: PowRef; header: BlockHeader): Result[void,string] =
let (expMixDigest, miningValue) = try:
pow.getPowDigest(header)
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")
ok()
proc validateHeader(db: BaseChainDB; header, parentHeader: BlockHeader;
numTransactions: int; checkSealOK: bool;
ttdReached: bool; pow: PowRef): Result[void,string] =
template inDAOExtraRange(blockNumber: BlockNumber): bool =
# EIP-799
# Blocks with block numbers in the range [1_920_000, 1_920_009]
# MUST have DAOForkBlockExtra
let DAOHigh = db.config.daoForkBlock + DAOForkExtraRange.u256
db.config.daoForkBlock <= blockNumber and
blockNumber < DAOHigh
if header.extraData.len > 32:
return err("BlockHeader.extraData larger than 32 bytes")
if header.gasUsed == 0 and 0 < numTransactions:
return err("zero gasUsed but tranactions present");
if header.gasUsed < 0 or header.gasUsed > header.gasLimit:
return err("gasUsed should be non negative and smaller or equal gasLimit")
if header.blockNumber != parentHeader.blockNumber + 1:
return err("Blocks must be numbered consecutively")
if header.timestamp.toUnix <= parentHeader.timestamp.toUnix:
return err("timestamp must be strictly later than parent")
if db.config.daoForkSupport and inDAOExtraRange(header.blockNumber):
if header.extraData != daoForkBlockExtraData:
return err("header extra data should be marked DAO")
if ttdReached:
# EIP-4399 and EIP-3675
# no need to check mixDigest because EIP-4399 override this field
# checking rule
if not header.difficulty.isZero:
return err("Non-zero difficulty in a post-merge block")
if not header.nonce.isZeroMemory:
return err("Non-zero nonce in a post-merge block")
if header.ommersHash != EMPTY_UNCLE_HASH:
return err("Invalid ommers hash in a post-merge block")
else:
let calcDiffc = db.config.calcDifficulty(header.timestamp, parentHeader)
if header.difficulty < calcDiffc:
return err("provided header difficulty is too low")
if checkSealOK:
return pow.validateSeal(header)
result = ok()
func validateUncle(currBlock, uncle, uncleParent: BlockHeader):
Result[void,string] =
if uncle.blockNumber >= currBlock.blockNumber:
return err("uncle block number larger than current block number")
if uncle.blockNumber != uncleParent.blockNumber + 1:
return err("Uncle number is not one above ancestor's number")
if uncle.timestamp.toUnix < uncleParent.timestamp.toUnix:
return err("Uncle timestamp is before ancestor's timestamp")
if uncle.gasUsed > uncle.gasLimit:
return err("Uncle's gas usage is above the limit")
result = ok()
proc validateUncles(chainDB: BaseChainDB; header: BlockHeader;
uncles: seq[BlockHeader]; checkSealOK: bool;
pow: PowRef): Result[void,string] =
let hasUncles = uncles.len > 0
let shouldHaveUncles = header.ommersHash != EMPTY_UNCLE_HASH
if not hasUncles and not shouldHaveUncles:
# optimization to avoid loading ancestors from DB, since the block has
# no uncles
return ok()
if hasUncles and not shouldHaveUncles:
return err("Block has uncles but header suggests uncles should be empty")
if shouldHaveUncles and not hasUncles:
return err("Header suggests block should have uncles but block has none")
# Check for duplicates
var uncleSet = initHashSet[Hash256]()
for uncle in uncles:
let uncleHash = uncle.blockHash
if uncleHash in uncleSet:
return err("Block contains duplicate uncles")
else:
uncleSet.incl uncleHash
let recentAncestorHashes = try:
chainDB.getAncestorsHashes(MAX_UNCLE_DEPTH + 1, header)
except CatchableError as err:
return err("Block not present in database")
let recentUncleHashes = try:
chainDB.getUncleHashes(recentAncestorHashes)
except CatchableError as err:
return err("Ancenstors not present in database")
let blockHash = header.blockHash
for uncle in uncles:
let uncleHash = uncle.blockHash
if uncleHash == blockHash:
return err("Uncle has same hash as block")
# ensure the uncle has not already been included.
if uncleHash in recentUncleHashes:
return err("Duplicate uncle")
# ensure that the uncle is not one of the canonical chain blocks.
if uncleHash in recentAncestorHashes:
return err("Uncle cannot be an ancestor")
# ensure that the uncle was built off of one of the canonical chain
# blocks.
if (uncle.parentHash notin recentAncestorHashes) or
(uncle.parentHash == header.parentHash):
return err("Uncle's parent is not an ancestor")
# check uncle against own parent
var parent: BlockHeader
if not chainDB.getBlockHeader(uncle.parentHash,parent):
return err("Uncle's parent has gone missing")
if uncle.timestamp <= parent.timestamp:
return err("Uncle's parent must me older")
# Now perform VM level validation of the uncle
if checkSealOK:
result = pow.validateSeal(uncle)
if result.isErr:
return
let uncleParent = try:
chainDB.getBlockHeader(uncle.parentHash)
except BlockNotFound:
return err("Uncle parent not found")
result = validateUncle(header, uncle, uncleParent)
if result.isErr:
return
result = chainDB.validateGasLimitOrBaseFee(uncle, uncleParent)
if result.isErr:
return
result = ok()
# ------------------------------------------------------------------------------
# Public function, extracted from executor
# ------------------------------------------------------------------------------
proc validateTransaction*(
roDB: ReadOnlyStateDB; ## Parent accounts environment for transaction
tx: Transaction; ## tx to validate
sender: EthAddress; ## tx.getSender or tx.ecRecover
maxLimit: GasInt; ## gasLimit from block header
baseFee: UInt256; ## baseFee from block header
fork: Fork): bool =
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
if tx.txType == TxEip1559 and fork < FkLondon:
debug "invalid tx: Eip1559 Tx type detected before London"
return false
# Note that the following check bears some plausibility but is _not_
# covered by the eip-1559 reference (sort of) pseudo code, for details
# see `https://eips.ethereum.org/EIPS/eip-1559#specification`_
#
# Rather this check is needed for surviving the post-London unit test
# eth_tests/GeneralStateTests/stEIP1559/lowGasLimit.json which seems to
# be sourced and generated from
# eth_tests/src/GeneralStateTestsFiller/stEIP1559/lowGasLimitFiller.yml
#
# Interestingly, the hive tests do not use this particular test but rather
# eth_tests/BlockchainTests/GeneralStateTests/stEIP1559/lowGasLimit.json
# from a parallel tests series which look like somehow expanded versions.
#
# 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
# 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
# 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 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 < tx.value:
debug "invalid tx: not enough cash to send",
available=balance,
availableMinusGas=balance-gasCost,
require=tx.value
return false
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.nonce != nonce:
debug "invalid tx: account nonce mismatch",
txNonce=tx.nonce,
accountNonce=nonce
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:
debug "invalid tx: sender is not an EOA",
sender=sender.toHex,
codeHash=codeHash.data.toHex
return false
true
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: Fork): bool =
## Variant of `validateTransaction()`
let
roDB = vmState.readOnlyStateDB
gasLimit = header.gasLimit
baseFee = header.baseFee
roDB.validateTransaction(tx, sender, gasLimit, baseFee, fork)
# ------------------------------------------------------------------------------
# Public functions, extracted from test_blockchain_json
# ------------------------------------------------------------------------------
proc validateHeaderAndKinship*(
chainDB: BaseChainDB;
header: BlockHeader;
uncles: seq[BlockHeader];
numTransactions: int;
checkSealOK: bool;
ttdReached: bool;
pow: PowRef): Result[void, string] =
if header.isGenesis:
if header.extraData.len > 32:
return err("BlockHeader.extraData larger than 32 bytes")
return ok()
let parent = try:
chainDB.getBlockHeader(header.parentHash)
except CatchableError as err:
return err("Failed to load block header from DB")
result = chainDB.validateHeader(
header, parent, numTransactions, checkSealOK, ttdReached, pow)
if result.isErr:
return
if uncles.len > MAX_UNCLES:
return err("Number of uncles exceed limit.")
if not chainDB.exists(header.stateRoot):
return err("`state_root` was not found in the db.")
if not ttdReached:
result = chainDB.validateUncles(header, uncles, checkSealOK, pow)
if result.isOk:
result = chainDB.validateGasLimitOrBaseFee(header, parent)
proc validateHeaderAndKinship*(
chainDB: BaseChainDB;
header: BlockHeader;
body: BlockBody;
checkSealOK: bool;
ttdReached: bool;
pow: PowRef): Result[void, string] {.gcsafe,raises: [CatchableError, Defect].} =
chainDB.validateHeaderAndKinship(
header, body.uncles, body.transactions.len, checkSealOK, ttdReached, pow)
proc validateHeaderAndKinship*(
chainDB: BaseChainDB;
ethBlock: EthBlock;
checkSealOK: bool;
ttdReached: bool;
pow: PowRef): Result[void,string] =
chainDB.validateHeaderAndKinship(
ethBlock.header, ethBlock.uncles, ethBlock.txs.len,
checkSealOK, ttdReached, pow)
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------