347 lines
11 KiB
Nim
347 lines
11 KiB
Nim
# beacon_chain
|
|
# Copyright (c) 2018-2021 Status Research & Development GmbH
|
|
# Licensed and distributed under either of
|
|
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
|
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
|
|
|
import
|
|
# Standard library
|
|
std/[strformat, tables, options, json, os, strutils],
|
|
# Status libraries
|
|
stew/[results, endians2], snappy, chronicles,
|
|
eth/keys, taskpools,
|
|
# Internals
|
|
../../beacon_chain/spec/[helpers, forks],
|
|
../../beacon_chain/spec/datatypes/[
|
|
base,
|
|
phase0, altair, merge],
|
|
../../beacon_chain/fork_choice/[fork_choice, fork_choice_types],
|
|
../../beacon_chain/beacon_chain_db,
|
|
../../beacon_chain/consensus_object_pools/[
|
|
blockchain_dag, block_quarantine, block_clearance, spec_cache],
|
|
# Third-party
|
|
yaml,
|
|
# Test
|
|
../testutil,
|
|
./fixtures_utils
|
|
|
|
# Test format described at https://github.com/ethereum/consensus-specs/tree/v1.1.0/tests/formats/fork_choice
|
|
# Note that our implementation has been optimized with "ProtoArray"
|
|
# instead of following the spec (in particular the "store").
|
|
|
|
type
|
|
OpKind = enum
|
|
opOnTick
|
|
opOnAttestation
|
|
opOnBlock
|
|
opOnMergeBlock
|
|
opChecks
|
|
|
|
Operation = object
|
|
valid: bool
|
|
# variant specific fields
|
|
case kind*: OpKind
|
|
of opOnTick:
|
|
tick: int
|
|
of opOnAttestation:
|
|
att: Attestation
|
|
of opOnBlock:
|
|
blk: ForkedSignedBeaconBlock
|
|
of opOnMergeBlock:
|
|
powBlock: PowBlock
|
|
of opChecks:
|
|
checks: JsonNode
|
|
|
|
proc initialLoad(
|
|
path: string, db: BeaconChainDB,
|
|
StateType, BlockType: typedesc): tuple[
|
|
dag: ChainDagRef, fkChoice: ref ForkChoice
|
|
] =
|
|
|
|
# TODO: support more than phase 0 genesis
|
|
|
|
let state = newClone(parseTest(
|
|
path/"anchor_state.ssz_snappy",
|
|
SSZ, StateType
|
|
))
|
|
|
|
# TODO stack usage. newClone and assignClone do not seem to
|
|
# prevent temporaries created by case objects
|
|
let forkedState = new ForkedHashedBeaconState
|
|
forkedState.kind = BeaconStateFork.Phase0
|
|
forkedState.phase0Data.data = state[]
|
|
forkedState.phase0Data.root = hash_tree_root(state[])
|
|
|
|
let blk = parseTest(
|
|
path/"anchor_block.ssz_snappy",
|
|
SSZ, BlockType
|
|
)
|
|
|
|
|
|
let signedBlock = ForkedSignedBeaconBlock.init(phase0.SignedBeaconBlock(
|
|
message: blk,
|
|
# signature: - unused as it's trusted
|
|
root: hashTreeRoot(blk)
|
|
))
|
|
|
|
ChainDagRef.preInit(
|
|
db,
|
|
forkedState[], forkedState[],
|
|
asTrusted(signedBlock)
|
|
)
|
|
let dag = ChainDAGRef.init(
|
|
defaultRuntimeConfig,
|
|
db,
|
|
updateFlags = {}
|
|
)
|
|
let fkChoice = newClone(ForkChoice.init(
|
|
dag.getFinalizedEpochRef(),
|
|
dag.finalizedHead.blck
|
|
))
|
|
|
|
(dag, fkChoice)
|
|
|
|
proc loadOps(path: string, fork: BeaconBlockFork): seq[Operation] =
|
|
let stepsYAML = readFile(path/"steps.yaml")
|
|
let steps = yaml.loadToJson(stepsYAML)
|
|
|
|
result = @[]
|
|
for step in steps[0]:
|
|
if step.hasKey"tick":
|
|
result.add Operation(kind: opOnTick, tick: step["tick"].getInt())
|
|
elif step.hasKey"block":
|
|
let filename = step["block"].getStr()
|
|
case fork
|
|
of BeaconBlockFork.Phase0:
|
|
let blk = parseTest(
|
|
path/filename & ".ssz_snappy",
|
|
SSZ, phase0.SignedBeaconBlock
|
|
)
|
|
result.add Operation(kind: opOnBlock,
|
|
blk: ForkedSignedBeaconBlock.init(blk))
|
|
of BeaconBlockFork.Altair:
|
|
let blk = parseTest(
|
|
path/filename & ".ssz_snappy",
|
|
SSZ, altair.SignedBeaconBlock
|
|
)
|
|
result.add Operation(kind: opOnBlock,
|
|
blk: ForkedSignedBeaconBlock.init(blk))
|
|
of BeaconBlockFork.Merge:
|
|
let blk = parseTest(
|
|
path/filename & ".ssz_snappy",
|
|
SSZ, merge.SignedBeaconBlock
|
|
)
|
|
result.add Operation(kind: opOnBlock,
|
|
blk: ForkedSignedBeaconBlock.init(blk))
|
|
elif step.hasKey"attestation":
|
|
let filename = step["attestation"].getStr()
|
|
let att = parseTest(
|
|
path/filename & ".ssz_snappy",
|
|
SSZ, Attestation
|
|
)
|
|
result.add Operation(kind: opOnAttestation,
|
|
att: att)
|
|
elif step.hasKey"checks":
|
|
result.add Operation(kind: opChecks,
|
|
checks: step["checks"])
|
|
else:
|
|
doAssert false, "Unreachable: " & $step
|
|
|
|
if step.hasKey"valid":
|
|
doAssert step.len == 2
|
|
result[^1].valid = step["valid"].getBool()
|
|
elif not step.hasKey"checks":
|
|
doAssert step.len == 1
|
|
result[^1].valid = true
|
|
|
|
proc stepOnBlock(
|
|
dag: ChainDagRef,
|
|
fkChoice: ref ForkChoice,
|
|
quarantine: QuarantineRef,
|
|
state: var StateData,
|
|
stateCache: var StateCache,
|
|
signedBlock: phase0.SignedBeaconBlock | altair.SignedBeaconBlock | merge.SignedBeaconBlock,
|
|
time: Slot): Result[BlockRef, (ValidationResult, BlockError)] =
|
|
# 1. Move state to proper slot.
|
|
dag.updateStateData(
|
|
state,
|
|
dag.head.atSlot(time),
|
|
save = false,
|
|
stateCache
|
|
)
|
|
|
|
# 2. Add block to DAG
|
|
when signedBlock is phase0.SignedBeaconBlock:
|
|
type TrustedBlock = phase0.TrustedSignedBeaconBlock
|
|
elif signedBlock is altair.SignedBeaconBlock:
|
|
type TrustedBlock = altair.TrustedSignedBeaconBlock
|
|
else:
|
|
type TrustedBlock = merge.TrustedSignedBeaconBlock
|
|
|
|
let blockAdded = dag.addRawBlock(quarantine, signedBlock) do (
|
|
blckRef: BlockRef, signedBlock: TrustedBlock, epochRef: EpochRef
|
|
):
|
|
|
|
# 3. Update fork choice if valid
|
|
let status = fkChoice[].process_block(
|
|
dag,
|
|
epochRef,
|
|
blckRef,
|
|
signedBlock.message,
|
|
time
|
|
)
|
|
doAssert status.isOk()
|
|
|
|
return blockAdded
|
|
|
|
proc stepOnAttestation(
|
|
dag: ChainDagRef,
|
|
fkChoice: ref ForkChoice,
|
|
att: Attestation,
|
|
time: Slot): FcResult[void] =
|
|
|
|
let epochRef = dag.getEpochRef(dag.head, time.compute_epoch_at_slot())
|
|
let attesters = epochRef.get_attesting_indices(att.data, att.aggregation_bits)
|
|
|
|
let status = fkChoice[].on_attestation(
|
|
dag,
|
|
att.data.slot, att.data.beacon_block_root, attesters,
|
|
time
|
|
)
|
|
|
|
status
|
|
|
|
proc stepChecks(
|
|
checks: JsonNode,
|
|
dag: ChainDagRef,
|
|
fkChoice: ref ForkChoice,
|
|
time: Slot
|
|
) =
|
|
doAssert checks.len >= 1, "No checks found"
|
|
for check, val in checks:
|
|
if check == "time":
|
|
doAssert time == Slot(val.getInt())
|
|
doAssert fkChoice.checkpoints.time == time
|
|
elif check == "head":
|
|
let headRoot = fkChoice[].get_head(dag, time).get()
|
|
let headRef = dag.getRef(headRoot)
|
|
doAssert headRef.slot == Slot(val["slot"].getInt())
|
|
doAssert headRef.root == Eth2Digest.fromHex(val["root"].getStr())
|
|
elif check == "justified_checkpoint":
|
|
let checkpointRoot = fkChoice.checkpoints.justified.checkpoint.root
|
|
let checkpointEpoch = fkChoice.checkpoints.justified.checkpoint.epoch
|
|
doAssert checkpointEpoch == Epoch(val["epoch"].getInt())
|
|
doAssert checkpointRoot == Eth2Digest.fromHex(val["root"].getStr())
|
|
elif check == "justified_checkpoint_root": # undocumented check
|
|
let checkpointRoot = fkChoice.checkpoints.justified.checkpoint.root
|
|
doAssert checkpointRoot == Eth2Digest.fromHex(val.getStr())
|
|
elif check == "finalized_checkpoint":
|
|
let checkpointRoot = fkChoice.checkpoints.finalized.root
|
|
let checkpointEpoch = fkChoice.checkpoints.finalized.epoch
|
|
doAssert checkpointEpoch == Epoch(val["epoch"].getInt())
|
|
doAssert checkpointRoot == Eth2Digest.fromHex(val["root"].getStr())
|
|
elif check == "best_justified_checkpoint":
|
|
let checkpointRoot = fkChoice.checkpoints.best_justified.root
|
|
let checkpointEpoch = fkChoice.checkpoints.best_justified.epoch
|
|
doAssert checkpointEpoch == Epoch(val["epoch"].getInt())
|
|
doAssert checkpointRoot == Eth2Digest.fromHex(val["root"].getStr())
|
|
elif check == "genesis_time":
|
|
# The fork choice is pruned regularly
|
|
# and does not store the genesis time,
|
|
# hence we check the DAG
|
|
doAssert dag.genesis.slot == Slot(val.getInt())
|
|
else:
|
|
doAssert false, "Unsupported check '" & $check & "'"
|
|
|
|
proc runTest(path: string, fork: BeaconBlockFork) =
|
|
let db = BeaconChainDB.new("", inMemory = true)
|
|
defer:
|
|
db.close()
|
|
|
|
var stores = case fork
|
|
of BeaconBlockFork.Phase0:
|
|
initialLoad(
|
|
path, db,
|
|
phase0.BeaconState, phase0.BeaconBlock
|
|
)
|
|
else:
|
|
doAssert false, "Unsupported fork: " & $fork
|
|
(ChainDAGRef(), (ref ForkChoice)())
|
|
# of BeaconBlockFork.Altair:
|
|
# initialLoad(
|
|
# path, db,
|
|
# # The tests always use phase 0 block for anchor - https://github.com/ethereum/eth2.0-specs/pull/2323
|
|
# # TODO: support altair genesis state
|
|
# altair.BeaconState, phase0.BeaconBlock
|
|
# )
|
|
# of BeaconBlockFork.Merge:
|
|
# initialLoad(
|
|
# path, db,
|
|
# # The tests always use phase 0 block for anchor - https://github.com/ethereum/eth2.0-specs/pull/2323
|
|
# # TODO: support merge genesis state
|
|
# merge.BeaconState, phase0.BeaconBlock
|
|
# )
|
|
|
|
let taskpool = Taskpool.new(numThreads = 1)
|
|
let quarantine = QuarantineRef.init(keys.newRng(), taskpool)
|
|
|
|
let steps = loadOps(path, fork)
|
|
var time = stores.fkChoice.checkpoints.time
|
|
|
|
let state = newClone(stores.dag.headState)
|
|
var stateCache = StateCache()
|
|
|
|
for step in steps:
|
|
case step.kind
|
|
of opOnTick:
|
|
time = Slot(step.tick)
|
|
of opOnBlock:
|
|
withBlck(step.blk):
|
|
let status = stepOnBlock(
|
|
stores.dag, stores.fkChoice,
|
|
quarantine,
|
|
state[], stateCache,
|
|
blck,
|
|
time)
|
|
doAssert status.isOk == step.valid
|
|
of opOnAttestation:
|
|
let status = stepOnAttestation(
|
|
stores.dag, stores.fkChoice,
|
|
step.att, time)
|
|
doAssert status.isOk == step.valid
|
|
of opChecks:
|
|
stepChecks(step.checks, stores.dag, stores.fkChoice, time)
|
|
else:
|
|
doAssert false, "Unsupported"
|
|
|
|
suite "Ethereum Foundation - ForkChoice" & preset():
|
|
const SKIP = [
|
|
# protoArray can handle blocks in the future gracefully
|
|
# spec: https://github.com/ethereum/consensus-specs/blame/v1.1.3/specs/phase0/fork-choice.md#L349
|
|
# test: tests/fork_choice/scenarios/no_votes.nim
|
|
# "Ensure the head is still 4 whilst the justified epoch is 0."
|
|
"on_block_future_block",
|
|
]
|
|
|
|
for fork in [BeaconBlockFork.Phase0]: # TODO: init ChainDAG from Merge/Altair
|
|
let forkStr = toLowerAscii($fork)
|
|
for testKind in ["get_head", "on_block"]:
|
|
let basePath = SszTestsDir/const_preset/forkStr/"fork_choice"/testKind/"pyspec_tests"
|
|
for kind, path in walkDir(basePath, relative = true, checkDir = true):
|
|
test "ForkChoice - " & const_preset/forkStr/"fork_choice"/testKind/"pyspec_tests"/path:
|
|
if const_preset == "minimal":
|
|
# TODO: Minimal tests have long paths issues on Windows
|
|
# and some are testing implementation details:
|
|
# - assertion that input block is not in the future
|
|
# - block slot is later than finalized slot
|
|
# - ...
|
|
# that ProtoArray handles gracefully
|
|
skip()
|
|
elif path in SKIP:
|
|
skip()
|
|
else:
|
|
runTest(basePath/path, fork)
|
|
|
|
|