nimbus-eth1/tests/test_blockchain_json.nim

549 lines
18 KiB
Nim
Raw Normal View History

2019-09-03 15:06:43 +00:00
# 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,
2019-09-03 15:06:43 +00:00
options,
2020-04-15 11:09:49 +00:00
eth/[common, rlp], eth/trie/[db, trie_defs],
stew/endians2, nimcrypto,
2019-12-07 15:37:31 +00:00
./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],
2019-09-07 08:38:44 +00:00
../nimbus/utils/header,
../nimbus/p2p/[executor, validate],
../nimbus/chain_config,
../stateless/[tree_from_witness, witness_types]
2019-09-03 15:06:43 +00:00
type
SealEngine = enum
NoProof
Ethash
TestBlock = object
goodBlock: bool
blockRLP : Blob
hasException: bool
2019-09-03 15:06:43 +00:00
Tester = object
lastBlockHash: Hash256
genesisHeader: BlockHeader
blocks : seq[TestBlock]
sealEngine : Option[SealEngine]
debugMode : bool
trace : bool
vmState : BaseVMState
debugData : JsonNode
network : string
2019-09-03 15:06:43 +00:00
var cacheByEpoch: EpochHashCache
cacheByEpoch.initEpochHashCache
2019-09-10 12:52:36 +00:00
proc testFixture(node: JsonNode, testStatusIMPL: var TestStatus, debugMode = false, trace = false)
2019-09-03 15:06:43 +00:00
func normalizeNumber(n: JsonNode): JsonNode =
let str = n.getStr
if str == "0x":
result = newJString("0x0")
elif str == "0x0":
result = n
elif str == "0x00":
2019-09-03 15:06:43 +00:00
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 parseBlocks(blocks: JsonNode): seq[TestBlock] =
2019-09-03 15:06:43 +00:00
for fixture in blocks:
var t: TestBlock
2019-09-03 15:06:43 +00:00
for key, value in fixture:
case key
of "blockHeader":
# header is absent in bad block
t.goodBlock = true
2019-09-03 15:06:43 +00:00
of "rlp":
fixture.fromJson "rlp", t.blockRLP
of "transactions", "uncleHeaders",
"blocknumber", "chainname", "chainnetwork":
discard
else:
doAssert("expectException" in key)
t.hasException = true
2019-09-03 15:06:43 +00:00
result.add t
func vmConfiguration(network: string, c: var ChainConfig) =
const
H = high(BlockNumber)
Zero = 0.toBlockNumber
Five = 5.toBlockNumber
proc assignNumber(c: var ChainConfig,
fork: Fork, n: BlockNumber) =
var number: array[Fork, BlockNumber]
var z = low(Fork)
while z < fork:
number[z] = Zero
z = z.succ
number[fork] = n
z = high(Fork)
while z > fork:
number[z] = H
z = z.pred
c.daoForkSupport = false
c.homesteadBlock = number[FkHomestead]
c.daoForkBlock = number[FkHomestead]
c.eip150Block = number[FkTangerine]
c.eip155Block = number[FkSpurious]
c.eip158Block = number[FkSpurious]
c.byzantiumBlock = number[FkByzantium]
c.constantinopleBlock = number[FkConstantinople]
c.petersburgBlock = number[FkPetersburg]
c.istanbulBlock = number[FkIstanbul]
c.muirGlacierBlock = number[FkBerlin]
c.berlinBlock = number[FkBerlin]
2020-04-12 12:02:03 +00:00
2019-09-04 07:32:17 +00:00
case network
2020-04-12 12:02:03 +00:00
of "EIP150":
c.assignNumber(FkTangerine, Zero)
2020-04-12 12:02:03 +00:00
of "ConstantinopleFix":
c.assignNumber(FkPetersburg, Zero)
2020-04-12 12:02:03 +00:00
of "Homestead":
c.assignNumber(FkHomestead, Zero)
2020-04-12 12:02:03 +00:00
of "Frontier":
c.assignNumber(FkFrontier, Zero)
2020-04-12 12:02:03 +00:00
of "Byzantium":
c.assignNumber(FkByzantium, Zero)
2020-04-12 12:02:03 +00:00
of "EIP158ToByzantiumAt5":
c.assignNumber(FkByzantium, Five)
2020-04-12 12:02:03 +00:00
of "EIP158":
c.assignNumber(FkSpurious, Zero)
2020-04-12 12:02:03 +00:00
of "HomesteadToDaoAt5":
c.assignNumber(FkHomestead, Zero)
c.daoForkBlock = Five
2020-04-12 12:02:03 +00:00
c.daoForkSupport = true
of "Constantinople":
c.assignNumber(FkConstantinople, Zero)
2020-04-12 12:02:03 +00:00
of "HomesteadToEIP150At5":
c.assignNumber(FkTangerine, Five)
2020-04-12 12:02:03 +00:00
of "FrontierToHomesteadAt5":
c.assignNumber(FkHomestead, Five)
2020-04-12 12:02:03 +00:00
of "ByzantiumToConstantinopleFixAt5":
c.assignNumber(FkPetersburg, Five)
c.constantinopleBlock = Five
2020-04-12 12:02:03 +00:00
of "Istanbul":
c.assignNumber(FkIstanbul, Zero)
2021-01-11 07:54:11 +00:00
of "Berlin":
c.assignNumber(FkBerlin, Zero)
2019-09-04 07:32:17 +00:00
else:
2021-01-11 07:54:11 +00:00
raise newException(ValueError, "unsupported network " & network)
2019-09-04 07:32:17 +00:00
2019-09-03 16:41:01 +00:00
proc parseTester(fixture: JsonNode, testStatusIMPL: var TestStatus): Tester =
result.blocks = parseBlocks(fixture["blocks"])
2019-09-03 16:41:01 +00:00
fixture.fromJson "lastblockhash", result.lastBlockHash
result.genesisHeader = parseHeader(fixture["genesisBlockHeader"], testStatusIMPL)
2019-09-03 16:41:01 +00:00
if "genesisRLP" in fixture:
var genesisRLP: Blob
fixture.fromJson "genesisRLP", genesisRLP
let genesisBlock = EthBlock(header: result.genesisHeader)
2019-09-03 16:41:01 +00:00
check genesisRLP == rlp.encode(genesisBlock)
else:
var goodBlock = true
for h in result.blocks:
goodBlock = goodBlock and h.goodBlock
check goodBlock == false
2019-09-03 16:41:01 +00:00
if "sealEngine" in fixture:
result.sealEngine = some(parseEnum[SealEngine](fixture["sealEngine"].getStr))
2020-04-12 12:02:03 +00:00
result.network = fixture["network"].getStr
2019-09-03 16:41:01 +00:00
proc blockWitness(vmState: BaseVMState, chainDB: BaseChainDB) =
let rootHash = vmState.accountDb.rootHash
let witness = vmState.buildWitness()
let fork = vmState.fork
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")
func validateBlockUnchanged(a, b: EthBlock): bool =
2019-09-07 10:32:06 +00:00
result = rlp.encode(a) == rlp.encode(b)
proc validateBlock(chainDB: BaseChainDB;
ethBlock: EthBlock; checkSealOK: bool): bool =
let rc = chainDB.validateKinship(
ethBlock.header, ethBlock.uncles, checkSealOK, cacheByEpoch)
if rc.isErr:
debugEcho "invalid block: " & rc.error
rc.isOk
2019-09-07 10:09:23 +00:00
2019-09-10 12:52:36 +00:00
proc importBlock(tester: var Tester, chainDB: BaseChainDB,
preminedBlock: EthBlock, tb: TestBlock, checkSeal, validation: bool): EthBlock =
2019-09-10 12:52:36 +00:00
2019-09-05 09:07:08 +00:00
let parentHeader = chainDB.getBlockHeader(preminedBlock.header.parentHash)
2020-04-12 10:33:17 +00:00
let baseHeaderForImport = generateHeaderFromParentHeader(chainDB.config,
parentHeader,
2020-04-12 12:02:03 +00:00
preminedBlock.header.coinbase,
2019-09-25 13:05:33 +00:00
some(preminedBlock.header.timestamp),
some(preminedBlock.header.gasLimit),
@[]
)
2019-09-07 08:38:44 +00:00
2019-09-07 10:32:06 +00:00
deepCopy(result, preminedBlock)
2019-09-10 12:52:36 +00:00
let tracerFlags: set[TracerFlags] = if tester.trace: {TracerFlags.EnableTracing} else : {}
tester.vmState = newBaseVMState(parentHeader.stateRoot, baseHeaderForImport, chainDB, tracerFlags)
2019-09-09 06:05:16 +00:00
let body = BlockBody(
transactions: result.txs,
uncles: result.uncles
)
let res = processBlock(chainDB, result.header, body, tester.vmState)
if res == ValidationResult.Error:
if not (tb.hasException or (not tb.goodBlock)):
raise newException(ValidationError, "process block validation")
else:
if tester.vmState.generateWitness():
blockWitness(tester.vmState, chainDB)
2019-11-28 10:02:11 +00:00
result.header.stateRoot = tester.vmState.blockHeader.stateRoot
2019-09-09 05:27:17 +00:00
result.header.parentHash = parentHeader.hash
2019-09-23 14:35:48 +00:00
result.header.difficulty = baseHeaderForImport.difficulty
2019-09-07 08:38:44 +00:00
2019-09-07 10:32:06 +00:00
if validation:
2019-09-09 06:11:09 +00:00
if not validateBlockUnchanged(result, preminedBlock):
raise newException(ValidationError, "block changed")
if not validateBlock(chainDB, result, checkSeal):
2019-09-07 10:32:06 +00:00
raise newException(ValidationError, "invalid block")
2019-09-05 09:07:08 +00:00
discard chainDB.persistHeaderToDb(preminedBlock.header)
2019-09-04 07:32:17 +00:00
proc applyFixtureBlockToChain(tester: var Tester, tb: TestBlock,
chainDB: BaseChainDB, checkSeal, validation: bool): (EthBlock, EthBlock, Blob) =
2020-04-12 12:02:03 +00:00
# we hack the ChainConfig here and let it works with calcDifficulty
vmConfiguration(tester.network, chainDB.config)
2020-04-12 12:02:03 +00:00
2019-09-04 07:32:17 +00:00
var
preminedBlock = rlp.decode(tb.blockRLP, EthBlock)
minedBlock = tester.importBlock(chainDB, preminedBlock, tb, checkSeal, validation)
2019-09-04 07:32:17 +00:00
rlpEncodedMinedBlock = rlp.encode(minedBlock)
result = (preminedBlock, minedBlock, rlpEncodedMinedBlock)
2019-09-09 05:27:17 +00:00
func shouldCheckSeal(tester: Tester): bool =
2019-09-09 06:05:16 +00:00
if tester.sealEngine.isSome:
result = tester.sealEngine.get() != NoProof
2019-09-09 05:27:17 +00:00
2019-09-24 13:18:48 +00:00
proc collectDebugData(tester: var Tester) =
if tester.vmState.isNil:
return
2019-09-24 13:18:48 +00:00
let vmState = tester.vmState
let tracingResult = if tester.trace: vmState.getTracingResult() else: %[]
tester.debugData.add %{
"blockNumber": %($vmState.blockNumber),
"structLogs": tracingResult,
}
2019-09-10 12:52:36 +00:00
proc runTester(tester: var Tester, chainDB: BaseChainDB, testStatusIMPL: var TestStatus) =
discard chainDB.persistHeaderToDb(tester.genesisHeader)
check chainDB.getCanonicalHead().blockHash == tester.genesisHeader.blockHash
2019-09-09 05:27:17 +00:00
let checkSeal = tester.shouldCheckSeal
2019-09-03 16:41:01 +00:00
2019-09-24 13:18:48 +00:00
if tester.debugMode:
tester.debugData = newJArray()
for idx, testBlock in tester.blocks:
if testBlock.goodBlock:
2019-09-25 11:43:16 +00:00
try:
2020-04-15 11:09:49 +00:00
let (preminedBlock, _, _) = tester.applyFixtureBlockToChain(
testBlock, chainDB, checkSeal, validation = false) # we manually validate below
check validateBlock(chainDB, preminedBlock, checkSeal) == true
2019-09-25 11:43:16 +00:00
except:
debugEcho "FATAL ERROR(WE HAVE BUG): ", getCurrentExceptionMsg()
2019-09-07 10:50:34 +00:00
else:
var noError = true
try:
let (_, _, _) = tester.applyFixtureBlockToChain(testBlock,
chainDB, checkSeal, validation = true)
except ValueError, ValidationError, BlockNotFound, RlpError:
2019-09-07 10:50:34 +00:00
# failure is expected on this bad block
check (testBlock.hasException or (not testBlock.goodBlock))
2019-09-07 10:50:34 +00:00
noError = false
if tester.debugMode:
tester.debugData.add %{
"exception": %($getCurrentException().name),
"msg": %getCurrentExceptionMsg()
}
2019-09-07 10:50:34 +00:00
# Block should have caused a validation error
check noError == false
2019-09-03 15:06:43 +00:00
2019-09-24 13:18:48 +00:00
if tester.debugMode:
tester.collectDebugData()
2019-09-10 12:52:36 +00:00
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, vmState: BaseVMState, accountList: JsonNode): JsonNode =
2019-09-10 12:52:36 +00:00
var accounts = newJObject()
var i = 0
for ac, _ in accountList:
let account = ethAddressFromHex(ac)
accounts[$account] = dumpAccount(vmState.readOnlyStateDB, account, "acc" & $i)
inc i
%{
2019-09-24 13:18:48 +00:00
"debugData": tester.debugData,
2019-09-10 12:52:36 +00:00
"accounts": accounts
}
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
let debugData = if vmState.isNil:
%{"debugData": tester.debugData}
else:
dumpDebugData(tester, vmState, accountList)
2019-09-10 12:52:36 +00:00
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) =
2019-09-05 09:07:08 +00:00
# 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
2020-04-12 12:02:03 +00:00
let specifyIndex = test_config.getConfiguration().index
2019-09-10 12:52:36 +00:00
var fixtureIndex = 0
var fixtureTested = false
2019-09-05 09:07:08 +00:00
2019-09-03 15:06:43 +00:00
for fixtureName, fixture in node:
2019-09-10 12:52:36 +00:00
inc fixtureIndex
if specifyIndex > 0 and fixtureIndex != specifyIndex:
continue
2019-09-04 07:32:17 +00:00
var tester = parseTester(fixture, testStatusIMPL)
2020-04-12 12:02:03 +00:00
var chainDB = newBaseChainDB(newMemoryDb(), pruneTrie = test_config.getConfiguration().pruning)
2019-09-03 15:06:43 +00:00
var vmState = newBaseVMState(emptyRlpHash,
tester.genesisHeader, chainDB)
2019-09-03 15:06:43 +00:00
vmState.generateWitness = true
2019-09-03 15:06:43 +00:00
vmState.mutateStateDB:
setupStateDB(fixture["pre"], db)
db.persist()
2019-09-03 15:06:43 +00:00
let obtainedHash = $(vmState.readOnlyStateDB.rootHash)
check obtainedHash == $(tester.genesisHeader.stateRoot)
2019-09-03 15:06:43 +00:00
2019-09-07 09:47:06 +00:00
tester.debugMode = debugMode
2019-09-10 12:52:36 +00:00
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:
2020-07-21 06:15:06 +00:00
echo fixtureName, " ERROR: ", E.msg
2019-09-10 12:52:36 +00:00
success = false
2019-09-25 11:43:16 +00:00
if tester.debugMode:
tester.dumpDebugData(fixture, fixtureName, fixtureIndex, success)
2019-09-10 12:52:36 +00:00
fixtureTested = true
check success == true
if not fixtureTested:
2020-04-12 12:02:03 +00:00
echo test_config.getConfiguration().testSubject, " not tested at all, wrong index?"
if specifyIndex <= 0 or specifyIndex > node.len:
echo "Maximum subtest available: ", node.len
2019-09-03 15:06:43 +00:00
2019-12-06 05:26:56 +00:00
proc blockchainJsonMain*(debugMode = false) =
const
legacyFolder = "eth_tests" / "LegacyTests" / "Constantinople" / "BlockchainTests"
newFolder = "eth_tests" / "BlockchainTests"
2021-01-14 14:33:18 +00:00
let config = test_config.getConfiguration()
if config.testSubject == "" or not debugMode:
2019-09-07 09:47:06 +00:00
# run all test fixtures
2021-01-14 14:33:18 +00:00
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)
2019-09-07 09:47:06 +00:00
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
2019-09-07 09:47:06 +00:00
let n = json.parseFile(path / config.testSubject)
var testStatusIMPL: TestStatus
testFixture(n, testStatusIMPL, debugMode = true, config.trace)
2019-09-07 09:47:06 +00:00
when isMainModule:
var message: string
## Processing command line arguments
2020-04-12 12:02:03 +00:00
if test_config.processArguments(message) != test_config.Success:
2019-09-07 09:47:06 +00:00
echo message
quit(QuitFailure)
else:
if len(message) > 0:
echo message
quit(QuitSuccess)
disableParamFiltering()
2019-12-06 05:26:56 +00:00
blockchainJsonMain(true)
2019-09-07 09:47:06 +00:00
2019-09-03 15:06:43 +00:00
# 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
# genesisHeader -> every fixture has it
2019-09-03 15:06:43 +00:00
# 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