887 lines
30 KiB
Nim
887 lines
30 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
|
|
unittest2, json, os, tables, strutils, sets, strformat, times,
|
|
options,
|
|
eth/[common, rlp], eth/trie/[db, trie_defs],
|
|
ethash, stew/endians2, nimcrypto,
|
|
./test_helpers, ./test_allowed_to_fail,
|
|
../premix/parser, test_config,
|
|
../nimbus/vm/interpreter/vm_forks,
|
|
../nimbus/[vm_state, utils, vm_types, errors, transaction, constants],
|
|
../nimbus/db/[db_chain, accounts_cache],
|
|
../nimbus/utils/header,
|
|
../nimbus/p2p/[executor, dao],
|
|
../nimbus/config,
|
|
../stateless/[tree_from_witness, witness_types]
|
|
|
|
type
|
|
SealEngine = enum
|
|
NoProof
|
|
Ethash
|
|
|
|
VMConfig = array[2, tuple[blockNumber: int, fork: Fork]]
|
|
|
|
PlainBlock = object
|
|
header: BlockHeader
|
|
transactions: seq[Transaction]
|
|
uncles: seq[BlockHeader]
|
|
|
|
TesterBlock = object
|
|
blockHeader: Option[BlockHeader]
|
|
transactions: seq[Transaction]
|
|
uncles: seq[BlockHeader]
|
|
blockNumber: Option[int]
|
|
chainName: Option[string]
|
|
chainNetwork: Option[Fork]
|
|
exceptions: seq[(string, string)]
|
|
headerRLP: Blob
|
|
|
|
Tester = object
|
|
lastBlockHash: Hash256
|
|
genesisBlockHeader: BlockHeader
|
|
blocks: seq[TesterBlock]
|
|
sealEngine: Option[SealEngine]
|
|
vmConfig: VMConfig
|
|
good: bool
|
|
debugMode: bool
|
|
trace: bool
|
|
vmState: BaseVMState
|
|
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 =
|
|
let str = n.getStr
|
|
# paranoid checks
|
|
doAssert n.kind == Jstring
|
|
doAssert str[0] == '0' and str[1] == 'x'
|
|
# real normalization
|
|
# strip leading 0
|
|
if str == "0x":
|
|
result = newJString("0x0")
|
|
elif str == "0x0":
|
|
result = n
|
|
elif str == "0x00":
|
|
result = newJString("0x0")
|
|
elif str[2] == '0':
|
|
var i = 2
|
|
while str[i] == '0':
|
|
inc i
|
|
result = newJString("0x" & str.substr(i))
|
|
else:
|
|
result = n
|
|
|
|
func normalizeData(n: JsonNode): JsonNode =
|
|
if n.getStr() == "":
|
|
result = newJString("0x")
|
|
else:
|
|
result = n
|
|
|
|
func normalizeBlockHeader(node: JsonNode): JsonNode =
|
|
for k, v in node:
|
|
case k
|
|
of "bloom": node["logsBloom"] = v
|
|
of "coinbase": node["miner"] = v
|
|
of "uncleHash": node["sha3Uncles"] = v
|
|
of "receiptTrie": node["receiptsRoot"] = v
|
|
of "transactionsTrie": node["transactionsRoot"] = v
|
|
of "number", "difficulty", "gasUsed",
|
|
"gasLimit", "timestamp":
|
|
node[k] = normalizeNumber(v)
|
|
of "extraData":
|
|
node[k] = normalizeData(v)
|
|
else: discard
|
|
result = node
|
|
|
|
proc parseHeader(blockHeader: JsonNode, testStatusIMPL: var TestStatus): BlockHeader =
|
|
result = normalizeBlockHeader(blockHeader).parseBlockHeader
|
|
var blockHash: Hash256
|
|
blockHeader.fromJson "hash", blockHash
|
|
check blockHash == hash(result)
|
|
|
|
proc parseTx*(n: JsonNode): Transaction =
|
|
|
|
for k, v in n:
|
|
case k
|
|
of "nonce", "gasPrice", "gasLimit", "value":
|
|
n[k] = normalizeNumber(v)
|
|
of "to":
|
|
let str = v.getStr
|
|
if str.len > 2 and str[1] != 'x':
|
|
n[k] = newJString("0x" & str)
|
|
of "v", "r", "s":
|
|
n[k] = normalizeNumber(v)
|
|
else:
|
|
discard
|
|
|
|
n.fromJson "nonce", result.accountNonce
|
|
n.fromJson "gasPrice", result.gasPrice
|
|
n.fromJson "gasLimit", result.gasLimit
|
|
result.isContractCreation = n["to"].getStr == ""
|
|
if not result.isContractCreation:
|
|
n.fromJson "to", result.to
|
|
n.fromJson "value", result.value
|
|
n.fromJson "data", result.payload
|
|
n.fromJson "v", result.V
|
|
n.fromJson "r", result.R
|
|
n.fromJson "s", result.S
|
|
|
|
proc parseBlocks(blocks: JsonNode, testStatusIMPL: var TestStatus): seq[TesterBlock] =
|
|
result = @[]
|
|
|
|
for fixture in blocks:
|
|
var t: TesterBlock
|
|
for key, value in fixture:
|
|
case key
|
|
of "blockHeader":
|
|
t.blockHeader = some(parseHeader(fixture["blockHeader"], testStatusIMPL))
|
|
of "blocknumber":
|
|
let numberStr = value.getStr
|
|
if numberStr.len >= 2 and numberStr[1] == 'x':
|
|
fixture[key] = normalizeNumber(value)
|
|
var number: int
|
|
fixture.fromJson "blocknumber", number
|
|
t.blockNumber = some(number)
|
|
else:
|
|
t.blockNumber = some(parseInt(numberStr))
|
|
of "chainname":
|
|
t.chainName = some(value.getStr)
|
|
of "chainnetwork":
|
|
t.chainNetWork = some(parseEnum[Fork](value.getStr))
|
|
of "rlp":
|
|
fixture.fromJson "rlp", t.headerRLP
|
|
of "transactions":
|
|
for tx in value:
|
|
t.transactions.add parseTx(tx)
|
|
of "uncleHeaders":
|
|
t.uncles = @[]
|
|
for uncle in value:
|
|
t.uncles.add parseHeader(uncle, testStatusIMPL)
|
|
else:
|
|
t.exceptions.add( (key, value.getStr) )
|
|
|
|
if t.blockHeader.isSome:
|
|
let h = t.blockHeader.get()
|
|
check calcTxRoot(t.transactions) == h.txRoot
|
|
let enc = rlp.encode(t.uncles)
|
|
check keccakHash(enc) == h.ommersHash
|
|
|
|
result.add t
|
|
|
|
func vmConfiguration(network: string, c: var ChainConfig): VMConfig =
|
|
|
|
c.homesteadBlock = high(BlockNumber)
|
|
c.daoForkBlock = high(BlockNumber)
|
|
c.daoForkSupport = false
|
|
c.eip150Block = high(BlockNumber)
|
|
c.eip158Block = high(BlockNumber)
|
|
c.byzantiumBlock = high(BlockNumber)
|
|
c.constantinopleBlock = high(BlockNumber)
|
|
c.petersburgBlock = high(BlockNumber)
|
|
c.istanbulBlock = high(BlockNumber)
|
|
c.muirGlacierBlock = high(BlockNumber)
|
|
|
|
case network
|
|
of "EIP150":
|
|
result = [(0, FkTangerine), (0, FkTangerine)]
|
|
c.eip150Block = 0.toBlockNumber
|
|
of "ConstantinopleFix":
|
|
result = [(0, FkPetersburg), (0, FkPetersburg)]
|
|
c.petersburgBlock = 0.toBlockNumber
|
|
of "Homestead":
|
|
result = [(0, FkHomestead), (0, FkHomestead)]
|
|
c.homesteadBlock = 0.toBlockNumber
|
|
of "Frontier":
|
|
result = [(0, FkFrontier), (0, FkFrontier)]
|
|
#c.frontierBlock = 0.toBlockNumber
|
|
of "Byzantium":
|
|
result = [(0, FkByzantium), (0, FkByzantium)]
|
|
c.byzantiumBlock = 0.toBlockNumber
|
|
of "EIP158ToByzantiumAt5":
|
|
result = [(0, FkSpurious), (5, FkByzantium)]
|
|
c.eip158Block = 0.toBlockNumber
|
|
c.byzantiumBlock = 5.toBlockNumber
|
|
of "EIP158":
|
|
result = [(0, FkSpurious), (0, FkSpurious)]
|
|
c.eip158Block = 0.toBlockNumber
|
|
of "HomesteadToDaoAt5":
|
|
result = [(0, FkHomestead), (5, FkHomestead)]
|
|
c.homesteadBlock = 0.toBlockNumber
|
|
c.daoForkBlock = 5.toBlockNumber
|
|
c.daoForkSupport = true
|
|
of "Constantinople":
|
|
result = [(0, FkConstantinople), (0, FkConstantinople)]
|
|
c.constantinopleBlock = 0.toBlockNumber
|
|
of "HomesteadToEIP150At5":
|
|
result = [(0, FkHomestead), (5, FkTangerine)]
|
|
c.homesteadBlock = 0.toBlockNumber
|
|
c.eip150Block = 5.toBlockNumber
|
|
of "FrontierToHomesteadAt5":
|
|
result = [(0, FkFrontier), (5, FkHomestead)]
|
|
#c.frontierBlock = 0.toBlockNumber
|
|
c.homesteadBlock = 5.toBlockNumber
|
|
of "ByzantiumToConstantinopleFixAt5":
|
|
result = [(0, FkByzantium), (5, FkPetersburg)]
|
|
c.byzantiumBlock = 0.toBlockNumber
|
|
c.petersburgBlock = 5.toBlockNumber
|
|
of "Istanbul":
|
|
result = [(0, FkIstanbul), (0, FkIstanbul)]
|
|
c.istanbulBlock = 0.toBlockNumber
|
|
of "Berlin":
|
|
result = [(0, FkBerlin), (0, FkBerlin)]
|
|
c.berlinBlock = 0.toBlockNumber
|
|
else:
|
|
raise newException(ValueError, "unsupported network " & network)
|
|
|
|
func vmConfigToFork(vmConfig: VMConfig, blockNumber: Uint256): Fork =
|
|
if blockNumber >= vmConfig[1].blockNumber.u256: return vmConfig[1].fork
|
|
if blockNumber >= vmConfig[0].blockNumber.u256: return vmConfig[0].fork
|
|
raise newException(ValueError, "unreachable code")
|
|
|
|
proc parseTester(fixture: JsonNode, testStatusIMPL: var TestStatus): Tester =
|
|
result.good = true
|
|
fixture.fromJson "lastblockhash", result.lastBlockHash
|
|
result.genesisBlockHeader = parseHeader(fixture["genesisBlockHeader"], testStatusIMPL)
|
|
|
|
if "genesisRLP" in fixture:
|
|
var genesisRLP: Blob
|
|
fixture.fromJson "genesisRLP", genesisRLP
|
|
let genesisBlock = PlainBlock(header: result.genesisBlockHeader)
|
|
check genesisRLP == rlp.encode(genesisBlock)
|
|
|
|
if "sealEngine" in fixture:
|
|
result.sealEngine = some(parseEnum[SealEngine](fixture["sealEngine"].getStr))
|
|
result.network = fixture["network"].getStr
|
|
|
|
try:
|
|
result.blocks = parseBlocks(fixture["blocks"], testStatusIMPL)
|
|
except ValueError:
|
|
result.good = false
|
|
|
|
# TODO: implement missing VM
|
|
#if result.network in ["HomesteadToDaoAt5"]:
|
|
#result.good = false
|
|
|
|
proc blockWitness(vmState: BaseVMState, fork: Fork, chainDB: BaseChainDB) =
|
|
let rootHash = vmState.accountDb.rootHash
|
|
let witness = vmState.buildWitness()
|
|
let flags = if fork >= FKSpurious: {wfEIP170} else: {}
|
|
|
|
# build tree from witness
|
|
var db = newMemoryDB()
|
|
when defined(useInputStream):
|
|
var input = memoryInput(witness)
|
|
var tb = initTreeBuilder(input, db, flags)
|
|
else:
|
|
var tb = initTreeBuilder(witness, db, flags)
|
|
let root = tb.buildTree()
|
|
|
|
# compare the result
|
|
if root != rootHash:
|
|
raise newException(ValidationError, "Invalid trie generated from block witness")
|
|
|
|
proc assignBlockRewards(minedBlock: PlainBlock, vmState: BaseVMState, fork: Fork, chainDB: BaseChainDB) =
|
|
let blockReward = blockRewards[fork]
|
|
var mainReward = blockReward
|
|
if minedBlock.header.ommersHash != EMPTY_UNCLE_HASH:
|
|
let h = vmState.chainDB.persistUncles(minedBlock.uncles)
|
|
if h != minedBlock.header.ommersHash:
|
|
raise newException(ValidationError, "Uncle hash mismatch")
|
|
for uncle in minedBlock.uncles:
|
|
var uncleReward = uncle.blockNumber.u256 + 8.u256
|
|
uncleReward -= minedBlock.header.blockNumber.u256
|
|
uncleReward = uncleReward * blockReward
|
|
uncleReward = uncleReward div 8.u256
|
|
vmState.mutateStateDB:
|
|
db.addBalance(uncle.coinbase, uncleReward)
|
|
mainReward += blockReward div 32.u256
|
|
|
|
# Reward beneficiary
|
|
vmState.mutateStateDB:
|
|
db.addBalance(minedBlock.header.coinbase, mainReward)
|
|
if vmState.generateWitness:
|
|
db.collectWitnessData()
|
|
db.persist()
|
|
|
|
let stateDb = vmState.accountDb
|
|
if minedBlock.header.stateRoot != stateDb.rootHash:
|
|
raise newException(ValidationError, "wrong state root in block")
|
|
|
|
let bloom = createBloom(vmState.receipts)
|
|
if minedBlock.header.bloom != bloom:
|
|
raise newException(ValidationError, "wrong bloom")
|
|
|
|
let receiptRoot = calcReceiptRoot(vmState.receipts)
|
|
if minedBlock.header.receiptRoot != receiptRoot:
|
|
raise newException(ValidationError, "wrong receiptRoot")
|
|
|
|
let txRoot = calcTxRoot(minedBlock.transactions)
|
|
if minedBlock.header.txRoot != txRoot:
|
|
raise newException(ValidationError, "wrong txRoot")
|
|
|
|
if vmState.generateWitness:
|
|
blockWitness(vmState, fork, chainDB)
|
|
|
|
proc processBlock(chainDB: BaseChainDB, vmState: BaseVMState, minedBlock: PlainBlock, fork: Fork) =
|
|
var dbTx = chainDB.db.beginTransaction()
|
|
defer: dbTx.dispose()
|
|
|
|
if chainDB.config.daoForkSupport and minedBlock.header.blockNumber == chainDB.config.daoForkBlock:
|
|
vmState.mutateStateDB:
|
|
db.applyDAOHardFork()
|
|
|
|
vmState.receipts = newSeq[Receipt](minedBlock.transactions.len)
|
|
vmState.cumulativeGasUsed = 0
|
|
|
|
for txIndex, tx in minedBlock.transactions:
|
|
var sender: EthAddress
|
|
if tx.getSender(sender):
|
|
discard processTransaction(tx, sender, vmState, fork)
|
|
else:
|
|
raise newException(ValidationError, "could not get sender")
|
|
vmState.receipts[txIndex] = makeReceipt(vmState, fork)
|
|
|
|
if vmState.cumulativeGasUsed != minedBlock.header.gasUsed:
|
|
let diff = vmState.cumulativeGasUsed - minedBlock.header.gasUsed
|
|
raise newException(ValidationError, &"wrong gas used in header expected={minedBlock.header.gasUsed}, actual={vmState.cumulativeGasUsed}, diff={diff}")
|
|
|
|
assignBlockRewards(minedBlock, vmState, fork, vmState.chainDB)
|
|
|
|
# `applyDeletes = false`
|
|
# preserve previous block stateRoot
|
|
# while still benefits from trie pruning
|
|
dbTx.commit(applyDeletes = false)
|
|
|
|
func validateBlockUnchanged(a, b: PlainBlock): 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: PlainBlock, 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: PlainBlock): bool =
|
|
result = currBlock.header.blockNumber == 0.u256 and currBlock.header.parentHash == GENESIS_PARENT_HASH
|
|
|
|
proc validateBlock(chainDB: BaseChainDB, currBlock: PlainBlock, 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: PlainBlock, fork: Fork, checkSeal, validation = true): PlainBlock =
|
|
|
|
let parentHeader = chainDB.getBlockHeader(preminedBlock.header.parentHash)
|
|
let baseHeaderForImport = generateHeaderFromParentHeader(chainDB.config,
|
|
parentHeader,
|
|
preminedBlock.header.coinbase,
|
|
some(preminedBlock.header.timestamp),
|
|
some(preminedBlock.header.gasLimit),
|
|
@[]
|
|
)
|
|
|
|
deepCopy(result, preminedBlock)
|
|
let tracerFlags: set[TracerFlags] = if tester.trace: {TracerFlags.EnableTracing} else : {}
|
|
tester.vmState = newBaseVMState(parentHeader.stateRoot, baseHeaderForImport, chainDB, tracerFlags)
|
|
|
|
processBlock(chainDB, tester.vmState, result, fork)
|
|
|
|
result.header.stateRoot = tester.vmState.blockHeader.stateRoot
|
|
result.header.parentHash = parentHeader.hash
|
|
result.header.difficulty = baseHeaderForImport.difficulty
|
|
|
|
if validation:
|
|
if not validateBlockUnchanged(result, preminedBlock):
|
|
raise newException(ValidationError, "block changed")
|
|
if not validateBlock(chainDB, result, checkSeal):
|
|
raise newException(ValidationError, "invalid block")
|
|
|
|
discard chainDB.persistHeaderToDb(preminedBlock.header)
|
|
|
|
proc applyFixtureBlockToChain(tester: var Tester, tb: TesterBlock,
|
|
chainDB: BaseChainDB, checkSeal, validation = true): (PlainBlock, PlainBlock, Blob) =
|
|
|
|
# we hack the ChainConfig here and let it works with calcDifficulty
|
|
tester.vmConfig = vmConfiguration(tester.network, chainDB.config)
|
|
|
|
var
|
|
preminedBlock = rlp.decode(tb.headerRLP, PlainBlock)
|
|
fork = vmConfigToFork(tester.vmConfig, preminedBlock.header.blockNumber)
|
|
minedBlock = tester.importBlock(chainDB, preminedBlock, fork, checkSeal, validation)
|
|
rlpEncodedMinedBlock = rlp.encode(minedBlock)
|
|
result = (preminedBlock, minedBlock, rlpEncodedMinedBlock)
|
|
|
|
func shouldCheckSeal(tester: Tester): bool =
|
|
if tester.sealEngine.isSome:
|
|
result = tester.sealEngine.get() != NoProof
|
|
|
|
proc collectDebugData(tester: var Tester) =
|
|
let vmState = tester.vmState
|
|
let tracingResult = if tester.trace: vmState.getTracingResult() else: %[]
|
|
tester.debugData.add %{
|
|
"blockNumber": %($vmState.blockNumber),
|
|
"structLogs": tracingResult,
|
|
}
|
|
|
|
proc runTester(tester: var Tester, chainDB: BaseChainDB, testStatusIMPL: var TestStatus) =
|
|
discard chainDB.persistHeaderToDb(tester.genesisBlockHeader)
|
|
check chainDB.getCanonicalHead().blockHash == tester.genesisBlockHeader.blockHash
|
|
let checkSeal = tester.shouldCheckSeal
|
|
|
|
if tester.debugMode:
|
|
tester.debugData = newJArray()
|
|
|
|
for idx, testerBlock in tester.blocks:
|
|
let shouldBeGoodBlock = testerBlock.blockHeader.isSome
|
|
|
|
if shouldBeGoodBlock:
|
|
try:
|
|
let (preminedBlock, _, _) = tester.applyFixtureBlockToChain(
|
|
testerBlock, chainDB, checkSeal, validation = false) # we manually validate below
|
|
check validateBlock(chainDB, preminedBlock, checkSeal) == true
|
|
except:
|
|
debugEcho "FATAL ERROR(WE HAVE BUG): ", getCurrentExceptionMsg()
|
|
|
|
else:
|
|
var noError = true
|
|
try:
|
|
let (_, _, _) = tester.applyFixtureBlockToChain(testerBlock,
|
|
chainDB, checkSeal, validation = true)
|
|
except ValueError, ValidationError, BlockNotFound, MalformedRlpError, RlpTypeMismatch:
|
|
# failure is expected on this bad block
|
|
noError = false
|
|
|
|
# Block should have caused a validation error
|
|
check noError == false
|
|
|
|
if tester.debugMode:
|
|
tester.collectDebugData()
|
|
|
|
proc dumpAccount(accountDb: ReadOnlyStateDB, address: EthAddress, name: string): JsonNode =
|
|
result = %{
|
|
"name": %name,
|
|
"address": %($address),
|
|
"nonce": %toHex(accountDb.getNonce(address)),
|
|
"balance": %accountDb.getBalance(address).toHex(),
|
|
"codehash": %($accountDb.getCodeHash(address)),
|
|
"storageRoot": %($accountDb.getStorageRoot(address))
|
|
}
|
|
|
|
proc dumpDebugData(tester: Tester, fixture: JsonNode, fixtureName: string, fixtureIndex: int, success: bool) =
|
|
let accountList = if fixture["postState"].kind == JObject: fixture["postState"] else: fixture["pre"]
|
|
let vmState = tester.vmState
|
|
var accounts = newJObject()
|
|
var i = 0
|
|
for ac, _ in accountList:
|
|
let account = ethAddressFromHex(ac)
|
|
accounts[$account] = dumpAccount(vmState.readOnlyStateDB, account, "acc" & $i)
|
|
inc i
|
|
|
|
let debugData = %{
|
|
"debugData": tester.debugData,
|
|
"accounts": accounts
|
|
}
|
|
|
|
let status = if success: "_success" else: "_failed"
|
|
writeFile("debug_" & fixtureName & "_" & $fixtureIndex & status & ".json", debugData.pretty())
|
|
|
|
proc testFixture(node: JsonNode, testStatusIMPL: var TestStatus, debugMode = false, trace = false) =
|
|
# 1 - mine the genesis block
|
|
# 2 - loop over blocks:
|
|
# - apply transactions
|
|
# - mine block
|
|
# 3 - diff resulting state with expected state
|
|
# 4 - check that all previous blocks were valid
|
|
let specifyIndex = test_config.getConfiguration().index
|
|
var fixtureIndex = 0
|
|
var fixtureTested = false
|
|
|
|
for fixtureName, fixture in node:
|
|
inc fixtureIndex
|
|
if specifyIndex > 0 and fixtureIndex != specifyIndex:
|
|
continue
|
|
|
|
var tester = parseTester(fixture, testStatusIMPL)
|
|
var chainDB = newBaseChainDB(newMemoryDb(), pruneTrie = test_config.getConfiguration().pruning)
|
|
|
|
if not tester.good: continue
|
|
|
|
var vmState = newBaseVMState(emptyRlpHash,
|
|
tester.genesisBlockHeader, chainDB)
|
|
|
|
vmState.generateWitness = true
|
|
|
|
vmState.mutateStateDB:
|
|
setupStateDB(fixture["pre"], db)
|
|
db.persist()
|
|
|
|
let obtainedHash = $(vmState.readOnlyStateDB.rootHash)
|
|
check obtainedHash == $(tester.genesisBlockHeader.stateRoot)
|
|
|
|
tester.debugMode = debugMode
|
|
tester.trace = trace
|
|
|
|
var success = true
|
|
try:
|
|
tester.runTester(chainDB, testStatusIMPL)
|
|
|
|
let latestBlockHash = chainDB.getCanonicalHead().blockHash
|
|
if latestBlockHash != tester.lastBlockHash:
|
|
verifyStateDB(fixture["postState"], tester.vmState.readOnlyStateDB)
|
|
except ValidationError as E:
|
|
echo fixtureName, " ERROR: ", E.msg
|
|
success = false
|
|
|
|
if tester.debugMode:
|
|
tester.dumpDebugData(fixture, fixtureName, fixtureIndex, success)
|
|
|
|
fixtureTested = true
|
|
check success == true
|
|
|
|
if not fixtureTested:
|
|
echo test_config.getConfiguration().testSubject, " not tested at all, wrong index?"
|
|
if specifyIndex <= 0 or specifyIndex > node.len:
|
|
echo "Maximum subtest available: ", node.len
|
|
|
|
proc blockchainJsonMain*(debugMode = false) =
|
|
const
|
|
legacyFolder = "eth_tests" / "LegacyTests" / "Constantinople" / "BlockchainTests"
|
|
newFolder = "eth_tests" / "BlockChainTests"
|
|
|
|
let config = test_config.getConfiguration()
|
|
if config.testSubject == "" or not debugMode:
|
|
# run all test fixtures
|
|
if config.legacy:
|
|
suite "block chain json tests":
|
|
jsonTest(legacyFolder, "BlockchainTests", testFixture, skipBCTests)
|
|
else:
|
|
suite "new block chain json tests":
|
|
jsonTest(newFolder, "newBlockchainTests", testFixture, skipNewBCTests)
|
|
else:
|
|
# execute single test in debug mode
|
|
if config.testSubject.len == 0:
|
|
echo "missing test subject"
|
|
quit(QuitFailure)
|
|
|
|
let folder = if config.legacy: legacyFolder else: newFolder
|
|
let path = "tests" / "fixtures" / folder
|
|
let n = json.parseFile(path / config.testSubject)
|
|
var testStatusIMPL: TestStatus
|
|
testFixture(n, testStatusIMPL, debugMode = true, config.trace)
|
|
|
|
when isMainModule:
|
|
var message: string
|
|
|
|
## Processing command line arguments
|
|
if test_config.processArguments(message) != test_config.Success:
|
|
echo message
|
|
quit(QuitFailure)
|
|
else:
|
|
if len(message) > 0:
|
|
echo message
|
|
quit(QuitSuccess)
|
|
|
|
blockchainJsonMain(true)
|
|
|
|
# lastBlockHash -> every fixture has it, hash of a block header
|
|
# genesisRLP -> NOT every fixture has it, rlp bytes of genesis block header
|
|
# _info -> every fixture has it, can be omitted
|
|
# pre, postState -> every fixture has it, prestate and post state
|
|
# genesisBlockHeader -> every fixture has it
|
|
# network -> every fixture has it
|
|
# # EIP150 247
|
|
# # ConstantinopleFix 286
|
|
# # Homestead 256
|
|
# # Frontier 396
|
|
# # Byzantium 263
|
|
# # EIP158ToByzantiumAt5 1
|
|
# # EIP158 233
|
|
# # HomesteadToDaoAt5 4
|
|
# # Constantinople 285
|
|
# # HomesteadToEIP150At5 1
|
|
# # FrontierToHomesteadAt5 7
|
|
# # ByzantiumToConstantinopleFixAt5 1
|
|
|
|
# sealEngine -> NOT every fixture has it
|
|
# # NoProof 1709
|
|
# # Ethash 112
|
|
|
|
# blocks -> every fixture has it, an array of blocks ranging from 1 block to 303 blocks
|
|
# # transactions 6230 can be empty
|
|
# # # to 6089 -> "" if contractCreation
|
|
# # # value 6089
|
|
# # # gasLimit 6089 -> "gas"
|
|
# # # s 6089
|
|
# # # r 6089
|
|
# # # gasPrice 6089
|
|
# # # v 6089
|
|
# # # data 6089 -> "input"
|
|
# # # nonce 6089
|
|
# # blockHeader 6230 can be not present, e.g. bad rlp
|
|
# # uncleHeaders 6230 can be empty
|
|
|
|
# # rlp 6810 has rlp but no blockheader, usually has exception
|
|
# # blocknumber 2733
|
|
# # chainname 1821 -> 'A' to 'H', and 'AA' to 'DD'
|
|
# # chainnetwork 21 -> all values are "Frontier"
|
|
# # expectExceptionALL 420
|
|
# # # UncleInChain 55
|
|
# # # InvalidTimestamp 42
|
|
# # # InvalidGasLimit 42
|
|
# # # InvalidNumber 42
|
|
# # # InvalidDifficulty 35
|
|
# # # InvalidBlockNonce 28
|
|
# # # InvalidUncleParentHash 26
|
|
# # # ExtraDataTooBig 21
|
|
# # # InvalidStateRoot 21
|
|
# # # ExtraDataIncorrect 19
|
|
# # # UnknownParent 16
|
|
# # # TooMuchGasUsed 14
|
|
# # # InvalidReceiptsStateRoot 9
|
|
# # # InvalidUnclesHash 7
|
|
# # # UncleIsBrother 7
|
|
# # # UncleTooOld 7
|
|
# # # InvalidTransactionsRoot 7
|
|
# # # InvalidGasUsed 7
|
|
# # # InvalidLogBloom 7
|
|
# # # TooManyUncles 7
|
|
# # # OutOfGasIntrinsic 1
|
|
# # expectExceptionEIP150 17
|
|
# # # TooMuchGasUsed 7
|
|
# # # InvalidReceiptsStateRoot 7
|
|
# # # InvalidStateRoot 3
|
|
# # expectExceptionByzantium 17
|
|
# # # InvalidStateRoot 10
|
|
# # # TooMuchGasUsed 7
|
|
# # expectExceptionHomestead 17
|
|
# # # InvalidReceiptsStateRoot 7
|
|
# # # BlockGasLimitReached 7
|
|
# # # InvalidStateRoot 3
|
|
# # expectExceptionConstantinople 14
|
|
# # # InvalidStateRoot 7
|
|
# # # TooMuchGasUsed 7
|
|
# # expectExceptionEIP158 14
|
|
# # # TooMuchGasUsed 7
|
|
# # # InvalidReceiptsStateRoot 7
|
|
# # expectExceptionFrontier 14
|
|
# # # InvalidReceiptsStateRoot 7
|
|
# # # BlockGasLimitReached 7
|
|
# # expectExceptionConstantinopleFix 14
|
|
# # # InvalidStateRoot 7
|
|
# # # TooMuchGasUsed 7
|