mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-01-12 07:14:20 +00:00
b32205de7c
* reworked some of the das core specs, pr'd to check whether whether the conflicting type issue is centric to my machine or not * bumped nim-blscurve to 9c6e80c6109133c0af3025654f5a8820282cff05, same as unstable * bumped nim-eth2-scenarios, nim-nat-traversal at par with unstable, added more pathches, made peerdas devnet branch backward compatible, peerdas passing new ssz tests as per alpha3, disabled electra fixture tests, as branch hasn't been rebased for a while * refactor test fixture files * rm: serializeDataColumn * refactor: took data columns extracted from blobs during block proposal to the heap * disable blob broadcast in pd devnet * fix addBlock in message router * fix: data column iterator * added debug checkpoints to check CI * refactor if else conditions * add: updated das core specs to alpha 3, and unit tests pass
411 lines
15 KiB
Nim
411 lines
15 KiB
Nim
# beacon_chain
|
|
# Copyright (c) 2018-2024 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.
|
|
|
|
{.push raises: [].}
|
|
{.used.}
|
|
|
|
import
|
|
# Status libraries
|
|
stew/[byteutils, results], chronicles,
|
|
taskpools,
|
|
# Internals
|
|
../../beacon_chain/spec/[helpers, forks, state_transition_block],
|
|
../../beacon_chain/fork_choice/[fork_choice, fork_choice_types],
|
|
../../beacon_chain/[beacon_chain_db, beacon_clock],
|
|
../../beacon_chain/consensus_object_pools/[
|
|
blockchain_dag, block_clearance, block_quarantine, spec_cache],
|
|
# Third-party
|
|
yaml,
|
|
# Test
|
|
../testutil, ../testdbutil,
|
|
./fixtures_utils, ./os_ops
|
|
|
|
from std/json import
|
|
JsonNode, getBool, getInt, getStr, hasKey, items, len, pairs, `$`, `[]`
|
|
from std/sequtils import mapIt, toSeq
|
|
from std/strutils import contains
|
|
from ../testbcutil import addHeadBlock
|
|
|
|
# Test format described at https://github.com/ethereum/consensus-specs/tree/v1.3.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
|
|
opOnAttesterSlashing
|
|
opInvalidateHash
|
|
opChecks
|
|
|
|
BlobData = object
|
|
blobs: seq[KzgBlob]
|
|
proofs: seq[KzgProof]
|
|
|
|
Operation = object
|
|
valid: bool
|
|
# variant specific fields
|
|
case kind: OpKind
|
|
of opOnTick:
|
|
tick: int
|
|
of opOnAttestation:
|
|
att: phase0.Attestation
|
|
of opOnBlock:
|
|
blck: ForkedSignedBeaconBlock
|
|
blobData: Opt[BlobData]
|
|
of opOnMergeBlock:
|
|
powBlock: PowBlock
|
|
of opOnAttesterSlashing:
|
|
attesterSlashing: phase0.AttesterSlashing
|
|
of opInvalidateHash:
|
|
invalidatedHash: Eth2Digest
|
|
latestValidHash: Eth2Digest
|
|
of opChecks:
|
|
checks: JsonNode
|
|
|
|
proc initialLoad(
|
|
path: string, db: BeaconChainDB,
|
|
StateType, BlockType: typedesc
|
|
): tuple[dag: ChainDAGRef, fkChoice: ref ForkChoice] {.raises: [
|
|
IOError, UnconsumedInput].} =
|
|
let
|
|
forkedState = loadForkedState(
|
|
path/"anchor_state.ssz_snappy",
|
|
StateType.kind)
|
|
|
|
blck = parseTest(
|
|
path/"anchor_block.ssz_snappy",
|
|
SSZ, BlockType)
|
|
|
|
ChainDAGRef.preInit(db, forkedState[])
|
|
|
|
let
|
|
validatorMonitor = newClone(ValidatorMonitor.init())
|
|
dag = ChainDAGRef.init(
|
|
forkedState[].kind.genesisTestRuntimeConfig, db, validatorMonitor, {})
|
|
fkChoice = newClone(ForkChoice.init(
|
|
dag.getFinalizedEpochRef(), dag.finalizedHead.blck,
|
|
ForkChoiceVersion.Pr3431))
|
|
|
|
(dag, fkChoice)
|
|
|
|
proc loadOps(
|
|
path: string,
|
|
fork: ConsensusFork
|
|
): seq[Operation] {.raises: [
|
|
IOError, KeyError, UnconsumedInput, ValueError,
|
|
YamlConstructionError, YamlParserError].} =
|
|
let stepsYAML = os_ops.readFile(path/"steps.yaml")
|
|
let steps = yaml.loadToJson(stepsYAML)
|
|
|
|
result = @[]
|
|
for step in steps[0]:
|
|
var numExtraFields = 0
|
|
|
|
if step.hasKey"tick":
|
|
result.add Operation(kind: opOnTick,
|
|
tick: step["tick"].getInt())
|
|
elif step.hasKey"attestation":
|
|
let filename = step["attestation"].getStr()
|
|
let att = parseTest(
|
|
path/filename & ".ssz_snappy",
|
|
SSZ, phase0.Attestation
|
|
)
|
|
result.add Operation(kind: opOnAttestation,
|
|
att: att)
|
|
elif step.hasKey"block":
|
|
let filename = step["block"].getStr()
|
|
doAssert step.hasKey"blobs" == step.hasKey"proofs"
|
|
withConsensusFork(fork):
|
|
let
|
|
blck = parseTest(
|
|
path/filename & ".ssz_snappy",
|
|
SSZ, consensusFork.SignedBeaconBlock)
|
|
|
|
blobData =
|
|
when consensusFork >= ConsensusFork.Deneb:
|
|
if step.hasKey"blobs":
|
|
numExtraFields += 2
|
|
Opt.some BlobData(
|
|
blobs: distinctBase(parseTest(
|
|
path/(step["blobs"].getStr()) & ".ssz_snappy",
|
|
SSZ, List[KzgBlob, Limit MAX_BLOBS_PER_BLOCK])),
|
|
proofs: step["proofs"].mapIt(
|
|
KzgProof(bytes: fromHex(array[48, byte], it.getStr()))))
|
|
else:
|
|
Opt.none(BlobData)
|
|
else:
|
|
doAssert not step.hasKey"blobs"
|
|
Opt.none(BlobData)
|
|
|
|
result.add Operation(kind: opOnBlock,
|
|
blck: ForkedSignedBeaconBlock.init(blck),
|
|
blobData: blobData)
|
|
elif step.hasKey"attester_slashing":
|
|
let filename = step["attester_slashing"].getStr()
|
|
let attesterSlashing = parseTest(
|
|
path/filename & ".ssz_snappy",
|
|
SSZ, phase0.AttesterSlashing
|
|
)
|
|
result.add Operation(kind: opOnAttesterSlashing,
|
|
attesterSlashing: attesterSlashing)
|
|
elif step.hasKey"payload_status":
|
|
if step["payload_status"]["status"].getStr() == "INVALID":
|
|
result.add Operation(kind: opInvalidateHash,
|
|
valid: true,
|
|
invalidatedHash: Eth2Digest.fromHex(step["block_hash"].getStr()),
|
|
latestValidHash: Eth2Digest.fromHex(
|
|
step["payload_status"]["latest_valid_hash"].getStr()))
|
|
elif step.hasKey"checks":
|
|
result.add Operation(kind: opChecks,
|
|
checks: step["checks"])
|
|
else:
|
|
raiseAssert "Unknown test step: " & $step
|
|
|
|
if step.hasKey"valid":
|
|
doAssert step.len == 2 + numExtraFields
|
|
result[^1].valid = step["valid"].getBool()
|
|
elif not step.hasKey"checks" and not step.hasKey"payload_status":
|
|
doAssert step.len == 1 + numExtraFields
|
|
result[^1].valid = true
|
|
|
|
proc stepOnBlock(
|
|
dag: ChainDAGRef,
|
|
fkChoice: ref ForkChoice,
|
|
verifier: var BatchVerifier,
|
|
state: var ForkedHashedBeaconState,
|
|
stateCache: var StateCache,
|
|
signedBlock: ForkySignedBeaconBlock,
|
|
blobData: Opt[BlobData],
|
|
time: BeaconTime,
|
|
invalidatedHashes: Table[Eth2Digest, Eth2Digest]):
|
|
Result[BlockRef, VerifierError] =
|
|
# 1. Validate blobs
|
|
when typeof(signedBlock).kind >= ConsensusFork.Deneb:
|
|
let kzgCommits = signedBlock.message.body.blob_kzg_commitments.asSeq
|
|
if kzgCommits.len > 0 or blobData.isSome:
|
|
if blobData.isNone or kzgCommits.validate_blobs(
|
|
blobData.get.blobs, blobData.get.proofs).isErr:
|
|
return err(VerifierError.Invalid)
|
|
else:
|
|
doAssert blobData.isNone, "Pre-Deneb test with specified blob data"
|
|
|
|
# 2. Move state to proper slot
|
|
doAssert dag.updateState(
|
|
state,
|
|
dag.getBlockIdAtSlot(time.slotOrZero).expect("block exists"),
|
|
save = false,
|
|
stateCache
|
|
)
|
|
|
|
# 3. Add block to DAG
|
|
const consensusFork = typeof(signedBlock).kind
|
|
|
|
# In normal Nimbus flow, for this (effectively) newPayload-based INVALID, it
|
|
# is checked even before entering the DAG, by the block processor. Currently
|
|
# the optimistic sync test(s) don't include a later-fcU-INVALID case. Whilst
|
|
# this wouldn't be part of this check, presumably, their FC test vector step
|
|
# would also have `true` validity because it'd not be known they weren't, so
|
|
# adding this mock of the block processor is realistic and sufficient.
|
|
when consensusFork >= ConsensusFork.Bellatrix:
|
|
let executionBlockHash =
|
|
signedBlock.message.body.execution_payload.block_hash
|
|
if executionBlockHash in invalidatedHashes:
|
|
# Mocks fork choice INVALID list application. These tests sequence this
|
|
# in a way the block processor does not, specifying each payload_status
|
|
# before the block itself, while Nimbus fork choice treats invalidating
|
|
# a non-existent block root as a no-op and does not remember it for the
|
|
# future.
|
|
let lvh = invalidatedHashes.getOrDefault(
|
|
executionBlockHash, static(default(Eth2Digest)))
|
|
fkChoice[].mark_root_invalid(dag.getEarliestInvalidBlockRoot(
|
|
signedBlock.message.parent_root, lvh, executionBlockHash))
|
|
|
|
return err VerifierError.Invalid
|
|
|
|
let blockAdded = dag.addHeadBlock(verifier, signedBlock) do (
|
|
blckRef: BlockRef, signedBlock: consensusFork.TrustedSignedBeaconBlock,
|
|
epochRef: EpochRef, unrealized: FinalityCheckpoints):
|
|
|
|
# 4. Update fork choice if valid
|
|
let status = fkChoice[].process_block(
|
|
dag, epochRef, blckRef, unrealized, signedBlock.message, time)
|
|
doAssert status.isOk()
|
|
|
|
# 5. Update DAG with new head
|
|
var quarantine = Quarantine.init()
|
|
let newHead = fkChoice[].get_head(dag, time).get()
|
|
dag.updateHead(dag.getBlockRef(newHead).get(), quarantine, [])
|
|
if dag.needStateCachesAndForkChoicePruning():
|
|
dag.pruneStateCachesDAG()
|
|
let pruneRes = fkChoice[].prune()
|
|
doAssert pruneRes.isOk()
|
|
|
|
blockAdded
|
|
|
|
proc stepChecks(
|
|
checks: JsonNode,
|
|
dag: ChainDAGRef,
|
|
fkChoice: ref ForkChoice,
|
|
time: BeaconTime) {.raises: [KeyError].} =
|
|
doAssert checks.len >= 1, "No checks found"
|
|
for check, val in checks:
|
|
if check == "time":
|
|
doAssert time.ns_since_genesis == val.getInt().seconds.nanoseconds()
|
|
doAssert fkChoice.checkpoints.time.slotOrZero == time.slotOrZero
|
|
elif check == "head":
|
|
let headRoot = fkChoice[].get_head(dag, time).get()
|
|
let headRef = dag.getBlockRef(headRoot).get()
|
|
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 == "proposer_boost_root":
|
|
doAssert fkChoice.checkpoints.proposer_boost_root ==
|
|
Eth2Digest.fromHex(val.getStr())
|
|
elif check == "genesis_time":
|
|
# We do not store genesis in fork choice..
|
|
discard
|
|
else:
|
|
raiseAssert "Unsupported check '" & $check & "'"
|
|
|
|
proc doRunTest(
|
|
path: string,
|
|
fork: ConsensusFork
|
|
) {.raises: [
|
|
IOError, KeyError, UnconsumedInput, ValueError,
|
|
YamlConstructionError, YamlParserError].} =
|
|
let db = BeaconChainDB.new("", inMemory = true)
|
|
defer:
|
|
db.close()
|
|
|
|
let
|
|
stores = withConsensusFork(fork):
|
|
initialLoad(
|
|
path, db, consensusFork.BeaconState, consensusFork.BeaconBlock)
|
|
|
|
rng = HmacDrbgContext.new()
|
|
taskpool =
|
|
try:
|
|
Taskpool.new()
|
|
except Exception as exc:
|
|
fatal "Failed to initialize Taskpool", exc = exc.msg
|
|
fail()
|
|
return
|
|
var verifier = BatchVerifier.init(rng, taskpool)
|
|
|
|
let steps = loadOps(path, fork)
|
|
var time = stores.fkChoice.checkpoints.time
|
|
var invalidatedHashes: Table[Eth2Digest, Eth2Digest]
|
|
|
|
let state = newClone(stores.dag.headState)
|
|
var stateCache = StateCache()
|
|
|
|
for step in steps:
|
|
case step.kind
|
|
of opOnTick:
|
|
time = BeaconTime(ns_since_genesis: step.tick.seconds.nanoseconds)
|
|
let status = stores.fkChoice[].update_time(stores.dag, time)
|
|
doAssert status.isOk == step.valid
|
|
of opOnAttestation:
|
|
let status = stores.fkChoice[].on_attestation(
|
|
stores.dag, step.att.data.slot, step.att.data.beacon_block_root,
|
|
toSeq(stores.dag.get_attesting_indices(step.att.asTrusted)), time)
|
|
doAssert status.isOk == step.valid
|
|
of opOnBlock:
|
|
withBlck(step.blck):
|
|
let status = stepOnBlock(
|
|
stores.dag, stores.fkChoice,
|
|
verifier, state[], stateCache,
|
|
forkyBlck, step.blobData, time, invalidatedHashes)
|
|
doAssert status.isOk == step.valid
|
|
of opOnAttesterSlashing:
|
|
let indices =
|
|
check_attester_slashing(state[], step.attesterSlashing, flags = {})
|
|
if indices.isOk:
|
|
for idx in indices.get:
|
|
stores.fkChoice[].process_equivocation(idx)
|
|
doAssert indices.isOk == step.valid
|
|
of opInvalidateHash:
|
|
invalidatedHashes[step.invalidatedHash] = step.latestValidHash
|
|
of opChecks:
|
|
stepChecks(step.checks, stores.dag, stores.fkChoice, time)
|
|
else:
|
|
raiseAssert "Unsupported"
|
|
|
|
proc runTest(suiteName: static[string], path: string, fork: ConsensusFork) =
|
|
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",
|
|
|
|
# TODO on_merge_block
|
|
"too_early_for_merge",
|
|
"too_late_for_merge",
|
|
"block_lookup_failed",
|
|
"all_valid",
|
|
|
|
# TODO intentional reorgs
|
|
"should_override_forkchoice_update__false",
|
|
"should_override_forkchoice_update__true",
|
|
"basic_is_parent_root",
|
|
"basic_is_head_root",
|
|
]
|
|
|
|
test suiteName & " - " & path.relativeTestPathComponent():
|
|
when defined(windows):
|
|
# Some test files have very long paths
|
|
skip()
|
|
else:
|
|
if os_ops.splitPath(path).tail in SKIP:
|
|
skip()
|
|
else:
|
|
doRunTest(path, fork)
|
|
|
|
template fcSuite(suiteName: static[string], testPathElem: static[string]) =
|
|
suite "EF - " & suiteName & preset():
|
|
const presetPath = SszTestsDir/const_preset
|
|
for kind, path in walkDir(presetPath, relative = true, checkDir = true):
|
|
let testsPath = presetPath/path/testPathElem
|
|
if kind != pcDir or not os_ops.dirExists(testsPath):
|
|
continue
|
|
if testsPath.contains("/electra/") or testsPath.contains("\\electra\\"):
|
|
continue
|
|
let fork = forkForPathComponent(path).valueOr:
|
|
raiseAssert "Unknown test fork: " & testsPath
|
|
for kind, path in walkDir(testsPath, relative = true, checkDir = true):
|
|
let basePath = testsPath/path/"pyspec_tests"
|
|
if kind != pcDir:
|
|
continue
|
|
for kind, path in walkDir(basePath, relative = true, checkDir = true):
|
|
runTest(suiteName, basePath/path, fork)
|
|
|
|
from ../../beacon_chain/conf import loadKzgTrustedSetup
|
|
discard loadKzgTrustedSetup() # Required for Deneb tests
|
|
|
|
fcSuite("ForkChoice", "fork_choice")
|
|
fcSuite("Sync", "sync") |