nimbus-eth1/nimbus/p2p/validate.nim

358 lines
12 KiB
Nim
Raw Normal View History

# 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
../constants,
../db/[db_chain, accounts_cache],
../errors,
../transaction,
../utils,
../utils/header,
../vm_state,
../vm_types,
../vm_types2,
chronicles,
eth/[common, rlp],
eth/trie/trie_defs,
ethash,
nimcrypto,
options,
sets,
stew/endians2,
strutils,
tables,
times
type
MiningHeader = object
parentHash : Hash256
ommersHash : Hash256
coinbase : EthAddress
stateRoot : Hash256
txRoot : Hash256
receiptRoot : Hash256
bloom : common.BloomFilter
difficulty : DifficultyInt
blockNumber : BlockNumber
gasLimit : GasInt
gasUsed : GasInt
timestamp : EthTime
extraData : Blob
Hash512 = MDigest[512]
CacheByEpoch* = OrderedTableRef[uint64, seq[Hash512]]
const
CACHE_MAX_ITEMS = 10
# ------------------------------------------------------------------------------
# Private functions
# ------------------------------------------------------------------------------
proc mkCacheBytes(blockNumber: uint64): seq[Hash512] =
mkcache(getCacheSize(blockNumber), getSeedhash(blockNumber))
proc findFirstIndex(tab: CacheByEpoch): uint64 =
# Kludge: OrderedTable[] still misses a proper API
for key in tab.keys:
result = key
break
proc getCache(cacheByEpoch: CacheByEpoch; blockNumber: uint64): seq[Hash512] =
# TODO: this is very inefficient
let epochIndex = blockNumber div EPOCH_LENGTH
# Get the cache if already generated, marking it as recently used
if epochIndex in cacheByEpoch:
let c = cacheByEpoch[epochIndex]
cacheByEpoch.del(epochIndex) # pop and append at end
cacheByEpoch[epochIndex] = c
return c
# Limit memory usage for cache
if cacheByEpoch.len >= CACHE_MAX_ITEMS:
cacheByEpoch.del(cacheByEpoch.findFirstIndex)
# Generate the cache if it was not already in memory
# Simulate requesting mkcache by block number: multiply index by epoch length
var c = mkCacheBytes(epochIndex * EPOCH_LENGTH)
cacheByEpoch[epochIndex] = c
result = system.move(c)
func cacheHash(x: openArray[Hash512]): Hash256 =
var ctx: keccak256
ctx.init()
for a in x:
ctx.update(a.data[0].unsafeAddr, uint(a.data.len))
ctx.finish result.data
ctx.clear()
proc checkPOW(cacheByEpoch: CacheByEpoch;
blockNumber: Uint256; miningHash, mixHash: Hash256,
nonce: BlockNonce, difficulty: DifficultyInt) =
let blockNumber = blockNumber.truncate(uint64)
let cache = cacheByEpoch.getCache(blockNumber)
let size = getDataSize(blockNumber)
let miningOutput = hashimotoLight(
size, cache, miningHash, uint64.fromBytesBE(nonce))
if miningOutput.mixDigest != mixHash:
echo "actual: ", miningOutput.mixDigest
echo "expected: ", mixHash
echo "blockNumber: ", blockNumber
echo "miningHash: ", miningHash
echo "nonce: ", nonce.toHex
echo "difficulty: ", difficulty
echo "size: ", size
echo "cache hash: ", cacheHash(cache)
raise newException(ValidationError, "mixHash mismatch")
let value = Uint256.fromBytesBE(miningOutput.value.data)
if value > Uint256.high div difficulty:
raise newException(ValidationError, "mining difficulty error")
func toMiningHeader(header: BlockHeader): MiningHeader =
result.parentHash = header.parentHash
result.ommersHash = header.ommersHash
result.coinbase = header.coinbase
result.stateRoot = header.stateRoot
result.txRoot = header.txRoot
result.receiptRoot = header.receiptRoot
result.bloom = header.bloom
result.difficulty = header.difficulty
result.blockNumber = header.blockNumber
result.gasLimit = header.gasLimit
result.gasUsed = header.gasUsed
result.timestamp = header.timestamp
result.extraData = header.extraData
func hash(header: MiningHeader): Hash256 =
keccakHash(rlp.encode(header))
proc validateSeal(cacheByEpoch: CacheByEpoch; header: BlockHeader) =
let miningHeader = header.toMiningHeader
let miningHash = miningHeader.hash
cacheByEpoch.checkPOW(header.blockNumber, miningHash,
header.mixDigest, header.nonce, header.difficulty)
# ------------------------------------------------------------------------------
# Puplic function, extracted from executor
# ------------------------------------------------------------------------------
proc newCacheByEpoch*(): CacheByEpoch =
newOrderedTable[uint64, seq[Hash512]]()
proc validateTransaction*(vmState: BaseVMState, tx: Transaction,
sender: EthAddress, fork: Fork): bool =
let balance = vmState.readOnlyStateDB.getBalance(sender)
let nonce = vmState.readOnlyStateDB.getNonce(sender)
if vmState.cumulativeGasUsed + tx.gasLimit > vmState.blockHeader.gasLimit:
debug "invalid tx: block header gasLimit reached",
maxLimit=vmState.blockHeader.gasLimit,
gasUsed=vmState.cumulativeGasUsed,
addition=tx.gasLimit
return
let gasCost = tx.gasLimit.u256 * tx.gasPrice.u256
if gasCost > balance:
debug "invalid tx: not enough cash for gas",
available=balance,
require=gasCost
return
if tx.value > balance - gasCost:
debug "invalid tx: not enough cash to send",
available=balance,
availableMinusGas=balance-gasCost,
require=tx.value
return
if tx.gasLimit < tx.intrinsicGas(fork):
debug "invalid tx: not enough gas to perform calculation",
available=tx.gasLimit,
require=tx.intrinsicGas(fork)
return
if tx.nonce != nonce:
debug "invalid tx: account nonce mismatch",
txNonce=tx.nonce,
accountNonce=nonce
return
result = true
# ------------------------------------------------------------------------------
# Public functions, extracted from test_blockchain_json
# ------------------------------------------------------------------------------
func validateGasLimit*(gasLimit, parentGasLimit: GasInt) =
if gasLimit < GAS_LIMIT_MINIMUM:
raise newException(ValidationError, "Gas limit is below minimum")
if gasLimit > GAS_LIMIT_MAXIMUM:
raise newException(ValidationError, "Gas limit is above maximum")
let diff = gasLimit - parentGasLimit
if diff > (parentGasLimit div GAS_LIMIT_ADJUSTMENT_FACTOR):
raise newException(
ValidationError, "Gas limit difference to parent is too big")
proc validateHeader*(header, parentHeader: BlockHeader;
checkSeal: bool; cacheByEpoch: CacheByEpoch) =
if header.extraData.len > 32:
raise newException(
ValidationError, "BlockHeader.extraData larger than 32 bytes")
validateGasLimit(header.gasLimit, parentHeader.gasLimit)
if header.blockNumber != parentHeader.blockNumber + 1:
raise newException(
ValidationError, "Blocks must be numbered consecutively.")
if header.timestamp.toUnix <= parentHeader.timestamp.toUnix:
raise newException(
ValidationError, "timestamp must be strictly later than parent")
if checkSeal:
cacheByEpoch.validateSeal(header)
func validateUncle*(currBlock, uncle, uncleParent: BlockHeader) =
if uncle.blockNumber >= currBlock.blockNumber:
raise newException(
ValidationError, "uncle block number larger than current block number")
if uncle.blockNumber != uncleParent.blockNumber + 1:
raise newException(
ValidationError, "Uncle number is not one above ancestor's number")
if uncle.timestamp.toUnix < uncleParent.timestamp.toUnix:
raise newException(
ValidationError, "Uncle timestamp is before ancestor's timestamp")
if uncle.gasUsed > uncle.gasLimit:
raise newException(ValidationError, "Uncle's gas usage is above the limit")
proc validateGasLimit*(chainDB: BaseChainDB, header: BlockHeader) =
let parentHeader = chainDB.getBlockHeader(header.parentHash)
let (lowBound, highBound) = gasLimitBounds(parentHeader)
if header.gasLimit < lowBound:
raise newException(ValidationError, "The gas limit is too low")
elif header.gasLimit > highBound:
raise newException(ValidationError, "The gas limit is too high")
proc validateUncles*(chainDB: BaseChainDB; currBlock: EthBlock;
checkSeal: bool; cacheByEpoch: CacheByEpoch) =
let hasUncles = currBlock.uncles.len > 0
let shouldHaveUncles = currBlock.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
elif hasUncles and not shouldHaveUncles:
raise newException(
ValidationError,
"Block has uncles but header suggests uncles should be empty")
elif shouldHaveUncles and not hasUncles:
raise newException(
ValidationError,
"Header suggests block should have uncles but block has none")
# Check for duplicates
var uncleSet = initHashSet[Hash256]()
for uncle in currBlock.uncles:
let uncleHash = uncle.hash
if uncleHash in uncleSet:
raise newException(ValidationError, "Block contains duplicate uncles")
else:
uncleSet.incl uncleHash
let recentAncestorHashes = chainDB.getAncestorsHashes(
MAX_UNCLE_DEPTH + 1, currBlock.header)
let recentUncleHashes = chainDB.getUncleHashes(recentAncestorHashes)
let blockHash =currBlock.header.hash
for uncle in currBlock.uncles:
let uncleHash = uncle.hash
if uncleHash == blockHash:
raise newException(ValidationError, "Uncle has same hash as block")
# ensure the uncle has not already been included.
if uncleHash in recentUncleHashes:
raise newException(ValidationError, "Duplicate uncle")
# ensure that the uncle is not one of the canonical chain blocks.
if uncleHash in recentAncestorHashes:
raise newException(ValidationError, "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 == currBlock.header.parentHash):
raise newException(ValidationError, "Uncle's parent is not an ancestor")
# Now perform VM level validation of the uncle
if checkSeal:
cacheByEpoch.validateSeal(uncle)
let uncleParent = chainDB.getBlockHeader(uncle.parentHash)
validateUncle(currBlock.header, uncle, uncleParent)
func isGenesis*(currBlock: EthBlock): bool =
result = currBlock.header.blockNumber == 0.u256 and
currBlock.header.parentHash == GENESIS_PARENT_HASH
proc validateBlock*(chainDB: BaseChainDB; currBlock: EthBlock;
checkSeal: bool; cacheByEpoch: CacheByEpoch): bool =
if currBlock.isGenesis:
if currBlock.header.extraData.len > 32:
raise newException(
ValidationError, "BlockHeader.extraData larger than 32 bytes")
return true
let parentHeader = chainDB.getBlockHeader(currBlock.header.parentHash)
validateHeader(currBlock.header, parentHeader, checkSeal, cacheByEpoch)
if currBlock.uncles.len > MAX_UNCLES:
raise newException(ValidationError, "Number of uncles exceed limit.")
if not chainDB.exists(currBlock.header.stateRoot):
raise newException(
ValidationError, "`state_root` was not found in the db.")
validateUncles(chainDB, currBlock, checkSeal, cacheByEpoch)
validateGaslimit(chainDB, currBlock.header)
result = true
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------