diff --git a/nimbus/p2p/executor.nim b/nimbus/p2p/executor.nim index 8e0accc40..ed7f63e9f 100644 --- a/nimbus/p2p/executor.nim +++ b/nimbus/p2p/executor.nim @@ -2,51 +2,11 @@ import options, sets, eth/[common, bloom, trie/db], chronicles, nimcrypto, ../db/[db_chain, accounts_cache], ../utils, ../constants, ../transaction, - ../vm_state, ../vm_types, ../vm_state_transactions, - ../vm_computation, ../vm_message, ../vm_precompiles, + ../vm_state, ../vm_types, ../vm_types2, - ./dao, ../config, + ./dao, ./validate, ../config, ../transaction/call_evm -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 proc processTransaction*(tx: Transaction, sender: EthAddress, vmState: BaseVMState, fork: Fork): GasInt = ## Process the transaction, write the results to db. diff --git a/nimbus/p2p/validate.nim b/nimbus/p2p/validate.nim new file mode 100644 index 000000000..401858771 --- /dev/null +++ b/nimbus/p2p/validate.nim @@ -0,0 +1,345 @@ +# 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] + +const + CACHE_MAX_ITEMS = 10 + +var cacheByEpoch = initOrderedTable[uint64, seq[Hash512]]() + +# ------------------------------------------------------------------------------ +# Private functions +# ------------------------------------------------------------------------------ + +proc mkCacheBytes(blockNumber: uint64): seq[Hash512] = + mkcache(getCacheSize(blockNumber), getSeedhash(blockNumber)) + + +proc getCache(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 + + # Generate the cache if it was not already in memory + # Simulate requesting mkcache by block number: multiply index by epoch length + let c = mkCacheBytes(epochIndex * EPOCH_LENGTH) + cacheByEpoch[epochIndex] = c + + # Limit memory usage for cache + if cacheByEpoch.len > CACHE_MAX_ITEMS: + cacheByEpoch.del(epochIndex) + + shallowCopy(result, 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(blockNumber: Uint256, miningHash, mixHash: Hash256, + nonce: BlockNonce, difficulty: DifficultyInt) = + let blockNumber = blockNumber.truncate(uint64) + let cache = blockNumber.getCache() + + 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(header: BlockHeader) = + let miningHeader = header.toMiningHeader + let miningHash = miningHeader.hash + + checkPOW(header.blockNumber, miningHash, + header.mixDigest, header.nonce, header.difficulty) + +# ------------------------------------------------------------------------------ +# Puplic function, extracted from executor +# ------------------------------------------------------------------------------ + +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) = + 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: + 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) = + 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: + 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): 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) + + 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) + validateGaslimit(chainDB, currBlock.header) + + result = true + +# ------------------------------------------------------------------------------ +# End +# ------------------------------------------------------------------------------ diff --git a/tests/test_blockchain_json.nim b/tests/test_blockchain_json.nim index 21b6b7ca1..25004e57c 100644 --- a/tests/test_blockchain_json.nim +++ b/tests/test_blockchain_json.nim @@ -6,17 +6,17 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - unittest2, json, os, tables, strutils, sets, strformat, times, + unittest2, json, os, tables, strutils, sets, options, eth/[common, rlp], eth/trie/[db, trie_defs], - ethash, stew/endians2, nimcrypto, + stew/endians2, nimcrypto, ./test_helpers, ./test_allowed_to_fail, ../premix/parser, test_config, ../nimbus/[vm_state, utils, vm_types, errors, transaction, constants, vm_types2], ../nimbus/db/[db_chain, accounts_cache], ../nimbus/utils/header, - ../nimbus/p2p/[executor, dao], - ../nimbus/[config, chain_config], + ../nimbus/p2p/[executor, validate], + ../nimbus/chain_config, ../stateless/[tree_from_witness, witness_types] type @@ -40,21 +40,6 @@ type debugData : JsonNode network : string - 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 - proc testFixture(node: JsonNode, testStatusIMPL: var TestStatus, debugMode = false, trace = false) func normalizeNumber(n: JsonNode): JsonNode = @@ -229,211 +214,6 @@ proc blockWitness(vmState: BaseVMState, chainDB: BaseChainDB) = func validateBlockUnchanged(a, b: EthBlock): bool = result = rlp.encode(a) == rlp.encode(b) -type Hash512 = MDigest[512] -var cacheByEpoch = initOrderedTable[uint64, seq[Hash512]]() -const CACHE_MAX_ITEMS = 10 - -proc mkCacheBytes(blockNumber: uint64): seq[Hash512] = - mkcache(getCacheSize(blockNumber), getSeedhash(blockNumber)) - -proc getCache(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 - - # Generate the cache if it was not already in memory - # Simulate requesting mkcache by block number: multiply index by epoch length - let c = mkCacheBytes(epochIndex * EPOCH_LENGTH) - cacheByEpoch[epochIndex] = c - - # Limit memory usage for cache - if cacheByEpoch.len > CACHE_MAX_ITEMS: - cacheByEpoch.del(epochIndex) - - shallowCopy(result, 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(blockNumber: Uint256, miningHash, mixHash: Hash256, nonce: BlockNonce, difficulty: DifficultyInt) = - let blockNumber = blockNumber.truncate(uint64) - let cache = blockNumber.getCache() - - 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(header: BlockHeader) = - let miningHeader = header.toMiningHeader - let miningHash = miningHeader.hash - - checkPOW(header.blockNumber, miningHash, - header.mixDigest, header.nonce, header.difficulty) - -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) = - 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: - 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) = - 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: - 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): 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) - - 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) - validateGaslimit(chainDB, currBlock.header) - - result = true proc importBlock(tester: var Tester, chainDB: BaseChainDB, preminedBlock: EthBlock, tb: TestBlock, checkSeal, validation: bool): EthBlock =