From 8ebd496fbeef03b4fd6837b7211fd1bd8d2cf2b3 Mon Sep 17 00:00:00 2001 From: tersec Date: Fri, 4 Jun 2021 10:38:00 +0000 Subject: [PATCH] Altair transition tests (#2624) * Working Altair transition tests * with fixed upstream test vectors, remove state root workaround * switch upgrade_to_altair() to returning a reference * remove test_state_transition * fix invalid fork state/block combinations error messages * avoid memory copies by reintroducing state_transition_slots(var SomeHashedBeaconState) --- AllTests-mainnet.md | 11 +- FixtureAll-mainnet.md | 12 +- .../block_clearance.nim | 3 +- beacon_chain/spec/beaconstate.nim | 10 +- beacon_chain/spec/state_transition.nim | 229 +++++++++++++++--- beacon_chain/spec/state_transition_block.nim | 13 + ncli/ncli_db.nim | 6 +- tests/all_tests.nim | 1 - .../all_altair_fixtures_require_ssz.nim | 3 +- tests/official/altair/test_fixture_fork.nim | 2 +- .../altair/test_fixture_transition.nim | 89 +++++++ tests/test_state_transition.nim | 131 ---------- 12 files changed, 326 insertions(+), 184 deletions(-) create mode 100644 tests/official/altair/test_fixture_transition.nim delete mode 100644 tests/test_state_transition.nim diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index 5f5bcd787..72e3fd69a 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -113,15 +113,6 @@ OK: 4/4 Fail: 0/4 Skip: 0/4 + updateStateData sanity [Preset: mainnet] OK ``` OK: 7/7 Fail: 0/7 Skip: 0/7 -## Block processing [Preset: mainnet] -```diff -+ Attestation gets processed at epoch [Preset: mainnet] OK -+ Passes from genesis state, empty block [Preset: mainnet] OK -+ Passes from genesis state, no block [Preset: mainnet] OK -+ Passes through epoch update, empty block [Preset: mainnet] OK -+ Passes through epoch update, no block [Preset: mainnet] OK -``` -OK: 5/5 Fail: 0/5 Skip: 0/5 ## BlockRef and helpers [Preset: mainnet] ```diff + epochAncestor sanity [Preset: mainnet] OK @@ -317,4 +308,4 @@ OK: 3/3 Fail: 0/3 Skip: 0/3 OK: 1/1 Fail: 0/1 Skip: 0/1 ---TOTAL--- -OK: 179/187 Fail: 0/187 Skip: 8/187 +OK: 174/182 Fail: 0/182 Skip: 8/182 diff --git a/FixtureAll-mainnet.md b/FixtureAll-mainnet.md index 065da4bc6..0ed72fd2e 100644 --- a/FixtureAll-mainnet.md +++ b/FixtureAll-mainnet.md @@ -2,6 +2,14 @@ FixtureAll-mainnet === ## ```diff ++ Official - Altair - Transition - normal_transition [Preset: mainnet] OK ++ Official - Altair - Transition - transition_missing_first_post_block [Preset: mainnet] OK ++ Official - Altair - Transition - transition_missing_last_pre_fork_block [Preset: mainnet] OK ++ Official - Altair - Transition - transition_only_blocks_post_fork [Preset: mainnet] OK ++ Official - Altair - Transition - transition_with_finality [Preset: mainnet] OK ++ Official - Altair - Transition - transition_with_no_attestations_until_after_fork [Preset: OK ++ Official - Altair - Transition - transition_with_random_half_participation [Preset: mainne OK ++ Official - Altair - Transition - transition_with_random_three_quarters_participation [Pres OK + Rewards - all_balances_too_low_for_reward [Preset: mainnet] OK + Rewards - duplicate_attestations_at_later_slots [Preset: mainnet] OK + Rewards - empty [Preset: mainnet] OK @@ -257,7 +265,7 @@ FixtureAll-mainnet + fork_random_low_balances OK + fork_random_misc_balances OK ``` -OK: 254/254 Fail: 0/254 Skip: 0/254 +OK: 262/262 Fail: 0/262 Skip: 0/262 ## Official - Altair - Epoch Processing - Effective balance updates [Preset: mainnet] ```diff + Effective balance updates - effective_balance_hysteresis [Preset: mainnet] OK @@ -475,4 +483,4 @@ OK: 1/1 Fail: 0/1 Skip: 0/1 OK: 27/27 Fail: 0/27 Skip: 0/27 ---TOTAL--- -OK: 385/385 Fail: 0/385 Skip: 0/385 +OK: 393/393 Fail: 0/393 Skip: 0/393 diff --git a/beacon_chain/consensus_object_pools/block_clearance.nim b/beacon_chain/consensus_object_pools/block_clearance.nim index 6afb9fc0b..736389ef8 100644 --- a/beacon_chain/consensus_object_pools/block_clearance.nim +++ b/beacon_chain/consensus_object_pools/block_clearance.nim @@ -172,10 +172,9 @@ proc checkStateTransition( blck = shortLog(signedBlock.message) blockRoot = shortLog(signedBlock.root) - var rewards: RewardInfo if not state_transition_block( dag.runtimePreset, dag.clearanceState.data, signedBlock, - cache, rewards, dag.updateFlags, restore): + cache, dag.updateFlags, restore): info "Invalid block" return (ValidationResult.Reject, Invalid) diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index 48dc19beb..1e27342f0 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -854,7 +854,7 @@ func translate_participation( state.previous_epoch_participation[index] = add_flag(state.previous_epoch_participation[index], flag_index) -proc upgrade_to_altair*(pre: phase0.BeaconState): altair.BeaconState = +proc upgrade_to_altair*(pre: phase0.BeaconState): ref altair.BeaconState = let epoch = get_current_epoch(pre) # https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.6/specs/altair/fork.md#configuration @@ -869,7 +869,7 @@ proc upgrade_to_altair*(pre: phase0.BeaconState): altair.BeaconState = for _ in 0 ..< len(pre.validators): doAssert inactivity_scores.add 0'u64 - var post = altair.BeaconState( + var post = (ref altair.BeaconState)( genesis_time: pre.genesis_time, genesis_validators_root: pre.genesis_validators_root, slot: pre.slot, @@ -916,12 +916,12 @@ proc upgrade_to_altair*(pre: phase0.BeaconState): altair.BeaconState = # Fill in previous epoch participation from the pre state's pending # attestations - translate_participation(post, pre.previous_epoch_attestations.asSeq) + translate_participation(post[], pre.previous_epoch_attestations.asSeq) # Fill in sync committees # Note: A duplicate committee is assigned for the current and next committee # at the fork boundary - post.current_sync_committee = get_next_sync_committee(post) - post.next_sync_committee = get_next_sync_committee(post) + post[].current_sync_committee = get_next_sync_committee(post[]) + post[].next_sync_committee = get_next_sync_committee(post[]) post diff --git a/beacon_chain/spec/state_transition.nim b/beacon_chain/spec/state_transition.nim index c52224e85..22a77dbed 100644 --- a/beacon_chain/spec/state_transition.nim +++ b/beacon_chain/spec/state_transition.nim @@ -45,7 +45,7 @@ import chronicles, stew/results, ../extras, ../ssz/merkleization, metrics, - ./datatypes/[phase0, altair], ./crypto, ./digest, ./helpers, ./signatures, ./validator, + ./datatypes/[phase0, altair], ./crypto, ./digest, ./helpers, ./signatures, ./validator, ./beaconstate, ./state_transition_block, ./state_transition_epoch, ../../nbench/bench_lab @@ -89,6 +89,20 @@ func verifyStateRoot(state: phase0.BeaconState, blck: phase0.TrustedBeaconBlock) # This is inlined in state_transition(...) in spec. true +func verifyStateRoot(state: altair.BeaconState, blck: altair.TrustedBeaconBlock): bool = + # This is inlined in state_transition(...) in spec. + true + +# one of these can happen on the fork block itself (it's a phase 0 block which +# creates an Altair state) +func verifyStateRoot(state: altair.BeaconState, blck: phase0.TrustedBeaconBlock): bool = + # This is inlined in state_transition(...) in spec. + true + +func verifyStateRoot(state: phase0.BeaconState, blck: altair.TrustedBeaconBlock): bool = + # This is inlined in state_transition(...) in spec. + true + type RollbackProc* = proc(v: var phase0.BeaconState) {.gcsafe, raises: [Defect].} @@ -174,31 +188,106 @@ proc process_slots*(state: var SomeHashedBeaconState, slot: Slot, # will be incorrect state.root = hash_tree_root(state.data) + # https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.6/specs/altair/fork.md#upgrading-the-state + # says to put upgrading here too, TODO. It may not work in state reply + # otherwise, since updateStateData() uses this function. + true func noRollback*(state: var phase0.HashedBeaconState) = trace "Skipping rollback of broken state" +type + BeaconStateFork* = enum + forkPhase0, + forkAltair + + ForkedHashedBeaconState* = object + case beaconStateFork*: BeaconStateFork + of forkPhase0: hbsPhase0*: phase0.HashedBeaconState + of forkAltair: hbsAltair*: altair.HashedBeaconState + +# Dispatch functions +template getStateField*(x: ForkedHashedBeaconState, y: untyped): untyped = + case x.beaconStateFork: + of forkPhase0: x.hbsPhase0.data.y + of forkAltair: x.hbsAltair.data.y + +template getStateField*(x: var ForkedHashedBeaconState, y: untyped): untyped = + case x.beaconStateFork: + of forkPhase0: x.hbsPhase0.data.y + of forkAltair: x.hbsAltair.data.y + +template getStateRoot*(x: ForkedHashedBeaconState): Eth2Digest = + case x.beaconStateFork: + of forkPhase0: x.hbsPhase0.root + of forkAltair: x.hbsAltair.root + +template hash_tree_root*(x: ForkedHashedBeaconState): Eth2Digest = + case x.beaconStateFork: + of forkPhase0: hash_tree_root(x.hbsPhase0.data) + of forkAltair: hash_tree_root(x.hbsAltair.data) + +template callWithBS*(op: untyped, y: ForkedHashedBeaconState): untyped = + let bs {.inject.} = + case y.beaconStateFork: + of forkPhase0: y.hbsPhase0.data + of forkAltair: y.hbsAltair.data + op + +proc maybeUpgradeStateToAltair( + state: var ForkedHashedBeaconState, altairForkSlot: Slot) = + # https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.6/specs/altair/fork.md#upgrading-the-state + + # Both state_transition_slots() and state_transition_block() call this, so + # only run it once by checking for existing fork. + if getStateField(state, slot) == altairForkSlot and + state.beaconStateFork == forkPhase0: + var newState = upgrade_to_altair(state.hbsPhase0.data) + state = ForkedHashedBeaconState( + beaconStateFork: forkAltair, + hbsAltair: altair.HashedBeaconState( + root: hash_tree_root(newState[]), data: newState[])) + proc state_transition_slots( preset: RuntimePreset, - #state: var phase0.HashedBeaconState, signedBlock: phase0.SomeSignedBeaconBlock, - state: var (phase0.HashedBeaconState | altair.HashedBeaconState), signedBlock: phase0.SignedBeaconBlock | phase0.SigVerifiedSignedBeaconBlock | phase0.TrustedSignedBeaconBlock | altair.SignedBeaconBlock, - cache: var StateCache, rewards: var RewardInfo, flags: UpdateFlags): bool {.nbench.} = - ## Apply a block to the state, advancing the slot counter as necessary. The - ## given state must be of a lower slot, or, in case the `slotProcessed` flag - ## is set, can be the slot state of the same slot as the block (where the - ## slot state is the state without any block applied). To create a slot state, - ## advance the state corresponding to the the parent block using - ## `process_slots`. - ## - ## To run the state transition function in preparation for block production, - ## use `makeBeaconBlock` instead. - ## - ## `rollback` is called if the transition fails and the given state has been - ## partially changed. If a temporary state was given to `state_transition`, - ## it is safe to use `noRollback` and leave it broken, else the state - ## object should be rolled back to a consistent state. If the transition fails - ## before the state has been updated, `rollback` will not be called. + state: var ForkedHashedBeaconState, + signedBlock: phase0.SignedBeaconBlock | phase0.SigVerifiedSignedBeaconBlock | phase0.TrustedSignedBeaconBlock | altair.SignedBeaconBlock, + cache: var StateCache, rewards: var RewardInfo, flags: UpdateFlags, + altairForkSlot: Slot): bool {.nbench.} = + let slot = signedBlock.message.slot + + # Update the state so its slot matches that of the block + while getStateField(state, slot) < slot: + case state.beaconStateFork: + of forkPhase0: + advance_slot( + state.hbsPhase0.data, state.hbsPhase0.root, flags, cache, rewards) + + if state.hbsPhase0.data.slot < slot: + # Don't update state root for the slot of the block + state.hbsPhase0.root = hash_tree_root(state.hbsPhase0.data) + of forkAltair: + advance_slot( + state.hbsAltair.data, state.hbsAltair.root, flags, cache, rewards) + + if getStateField(state, slot) < slot: + # Don't update state root for the slot of the block + state.hbsAltair.root = hash_tree_root(state.hbsAltair.data) + + maybeUpgradeStateToAltair(state, altairForkSlot) + + true + +proc state_transition_slots( + preset: RuntimePreset, + state: var SomeHashedBeaconState, + signedBlock: phase0.SignedBeaconBlock | phase0.SigVerifiedSignedBeaconBlock | + phase0.TrustedSignedBeaconBlock | altair.SignedBeaconBlock, + cache: var StateCache, rewards: var RewardInfo, flags: UpdateFlags): + bool {.nbench.} = + # TODO remove when the HashedBeaconState state_transition is removed; it's + # to avoid requiring a wrapped/memory-copied version let slot = signedBlock.message.slot if not (state.data.slot < slot): if slotProcessed notin flags or state.data.slot != slot: @@ -220,10 +309,17 @@ proc state_transition_slots( proc state_transition_block*( preset: RuntimePreset, - #state: var phase0.HashedBeaconState, signedBlock: phase0.SomeSignedBeaconBlock, - state: var (phase0.HashedBeaconState | altair.HashedBeaconState), signedBlock: phase0.SignedBeaconBlock | phase0.SigVerifiedSignedBeaconBlock | phase0.TrustedSignedBeaconBlock | altair.SignedBeaconBlock, - cache: var StateCache, rewards: var RewardInfo, flags: UpdateFlags, - rollback: RollbackHashedProc): bool {.nbench.} = + state: var SomeHashedBeaconState, + signedBlock: phase0.SignedBeaconBlock | phase0.SigVerifiedSignedBeaconBlock | + phase0.TrustedSignedBeaconBlock | altair.SignedBeaconBlock, + cache: var StateCache, flags: UpdateFlags, rollback: RollbackHashedProc): + bool {.nbench.} = + ## `rollback` is called if the transition fails and the given state has been + ## partially changed. If a temporary state was given to `state_transition`, + ## it is safe to use `noRollback` and leave it broken, else the state + ## object should be rolled back to a consistent state. If the transition fails + ## before the state has been updated, `rollback` will not be called. + # Block updates - these happen when there's a new block being suggested # by the block proposer. Every actor in the network will update its state # according to the contents of this block - but first they will validate @@ -270,15 +366,92 @@ proc state_transition_block*( true +proc state_transition_block*( + preset: RuntimePreset, + state: var ForkedHashedBeaconState, + signedBlock: phase0.SignedBeaconBlock | phase0.SigVerifiedSignedBeaconBlock | + phase0.TrustedSignedBeaconBlock | + altair.SignedBeaconBlock | altair.SigVerifiedSignedBeaconBlock, + cache: var StateCache, flags: UpdateFlags, + rollback: RollbackHashedProc, altairForkSlot: Slot): bool {.nbench.} = + ## `rollback` is called if the transition fails and the given state has been + ## partially changed. If a temporary state was given to `state_transition`, + ## it is safe to use `noRollback` and leave it broken, else the state + ## object should be rolled back to a consistent state. If the transition fails + ## before the state has been updated, `rollback` will not be called. + + # Ensure state_transition_block()-only callers trigger this + maybeUpgradeStateToAltair(state, altairForkSlot) + + case state.beaconStateFork: + of forkPhase0: state_transition_block( + preset, state.hbsPhase0, signedBlock, cache, flags, rollback) + of forkAltair: state_transition_block( + preset, state.hbsAltair, signedBlock, cache, flags, rollback) + proc state_transition*( preset: RuntimePreset, - #state: var phase0.HashedBeaconState, signedBlock: phase0.SomeSignedBeaconBlock, - state: var (phase0.HashedBeaconState | altair.HashedBeaconState), signedBlock: phase0.SignedBeaconBlock | phase0.SigVerifiedSignedBeaconBlock | phase0.TrustedSignedBeaconBlock | altair.SignedBeaconBlock, + state: var ForkedHashedBeaconState, + signedBlock: phase0.SignedBeaconBlock | phase0.SigVerifiedSignedBeaconBlock | + phase0.TrustedSignedBeaconBlock | altair.SignedBeaconBlock, cache: var StateCache, rewards: var RewardInfo, flags: UpdateFlags, - rollback: RollbackHashedProc): bool {.nbench.} = - if not state_transition_slots(preset, state, signedBlock, cache, rewards, flags): + rollback: RollbackHashedProc, + altairForkEpoch: Epoch = FAR_FUTURE_EPOCH): bool {.nbench.} = + ## Apply a block to the state, advancing the slot counter as necessary. The + ## given state must be of a lower slot, or, in case the `slotProcessed` flag + ## is set, can be the slot state of the same slot as the block (where the + ## slot state is the state without any block applied). To create a slot state, + ## advance the state corresponding to the the parent block using + ## `process_slots`. + ## + ## To run the state transition function in preparation for block production, + ## use `makeBeaconBlock` instead. + ## + ## `rollback` is called if the transition fails and the given state has been + ## partially changed. If a temporary state was given to `state_transition`, + ## it is safe to use `noRollback` and leave it broken, else the state + ## object should be rolled back to a consistent state. If the transition fails + ## before the state has been updated, `rollback` will not be called. + let slot = signedBlock.message.slot + if not (getStateField(state, slot) < slot): + if slotProcessed notin flags or getStateField(state, slot) != slot: + notice "State must precede block", + state_root = shortLog(getStateRoot(state)), + current_slot = getStateField(state, slot), + blck = shortLog(signedBlock) + return false + + if not state_transition_slots( + preset, state, signedBlock, cache, rewards, flags, + altairForkEpoch.compute_start_slot_at_epoch): return false - state_transition_block(preset, state, signedBlock, cache, rewards, flags, rollback) + state_transition_block( + preset, state, signedBlock, cache, flags, rollback, + altairForkEpoch.compute_start_slot_at_epoch) + +proc state_transition*( + preset: RuntimePreset, + state: var SomeHashedBeaconState, + signedBlock: phase0.SignedBeaconBlock | phase0.SigVerifiedSignedBeaconBlock | + phase0.TrustedSignedBeaconBlock | altair.SignedBeaconBlock, + cache: var StateCache, rewards: var RewardInfo, flags: UpdateFlags, + rollback: RollbackHashedProc): bool = + # Does not follow hard forks; suitable only where that's irrelevant. + # TODO remove when callers gone + let slot = signedBlock.message.slot + if not (state.data.slot < slot): + if slotProcessed notin flags or state.data.slot != slot: + notice "State must precede block", + state_root = shortLog(state.root), + current_slot = state.data.slot, + blck = shortLog(signedBlock) + return false + + if not state_transition_slots( + preset, state, signedBlock, cache, rewards, flags): + return false + state_transition_block( + preset, state, signedBlock, cache, flags, rollback) # https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/validator.md#preparing-for-a-beaconblock proc makeBeaconBlock*( diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index 745b6b90e..7f0d1c0e0 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -412,6 +412,13 @@ proc process_block*( ok() +proc process_block*( + preset: RuntimePreset, + state: var altair.BeaconState, blck: SomePhase0Block, flags: UpdateFlags, + cache: var StateCache): Result[void, cstring] {.nbench.} = + # The transition-triggering block creates, not acts on, an Altair state + err("process_block: Altair state with Phase 0 block") + # https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.6/specs/altair/beacon-chain.md#block-processing # TODO workaround for https://github.com/nim-lang/Nim/issues/18095 # copy of datatypes/altair.nim @@ -432,3 +439,9 @@ proc process_block*( ? process_sync_committee(state, blck.body.sync_aggregate, cache) # [New in Altair] ok() + +proc process_block*( + preset: RuntimePreset, + state: var phase0.BeaconState, blck: SomeAltairBlock, flags: UpdateFlags, + cache: var StateCache): Result[void, cstring] {.nbench.}= + err("process_block: Phase 0 state with Altair block") diff --git a/ncli/ncli_db.nim b/ncli/ncli_db.nim index 21c87be70..5f9c73ccc 100644 --- a/ncli/ncli_db.nim +++ b/ncli/ncli_db.nim @@ -208,7 +208,7 @@ proc cmdBench(conf: DbConf, runtimePreset: RuntimePreset) = if conf.resetCache: cache = StateCache() if not state_transition_block( - runtimePreset, state[].data, b, cache, rewards, {}, noRollback): + runtimePreset, state[].data, b, cache, {}, noRollback): dump("./", b) echo "State transition failed (!)" quit 1 @@ -530,7 +530,7 @@ proc cmdValidatorPerf(conf: DbConf, runtimePreset: RuntimePreset) = processEpoch() if not state_transition_block( - runtimePreset, state[].data, blck, cache, rewards, {}, noRollback): + runtimePreset, state[].data, blck, cache, {}, noRollback): echo "State transition failed (!)" quit 1 @@ -757,7 +757,7 @@ proc cmdValidatorDb(conf: DbConf, runtimePreset: RuntimePreset) = processEpoch() if not state_transition_block( - runtimePreset, state[].data, blck, cache, rewards, {}, noRollback): + runtimePreset, state[].data, blck, cache, {}, noRollback): echo "State transition failed (!)" quit 1 diff --git a/tests/all_tests.nim b/tests/all_tests.nim index da0706990..5e4bc5d66 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -26,7 +26,6 @@ import # Unit test ./test_interop, ./test_peer_pool, ./test_ssz, - ./test_state_transition, ./test_statediff, ./test_sync_manager, ./test_zero_signature, diff --git a/tests/official/altair/all_altair_fixtures_require_ssz.nim b/tests/official/altair/all_altair_fixtures_require_ssz.nim index 3c6fe97c8..1e94b1c21 100644 --- a/tests/official/altair/all_altair_fixtures_require_ssz.nim +++ b/tests/official/altair/all_altair_fixtures_require_ssz.nim @@ -23,4 +23,5 @@ import ./test_fixture_operations_proposer_slashings, ./test_fixture_operations_sync_committee, ./test_fixture_operations_voluntary_exit, - ./test_fixture_fork + ./test_fixture_fork, + ./test_fixture_transition diff --git a/tests/official/altair/test_fixture_fork.nim b/tests/official/altair/test_fixture_fork.nim index 740533918..8f5f2dfa8 100644 --- a/tests/official/altair/test_fixture_fork.nim +++ b/tests/official/altair/test_fixture_fork.nim @@ -36,7 +36,7 @@ proc runTest(identifier: string) = parseTest(testDir/"pre.ssz_snappy", SSZ, phase0.BeaconState)) postState = newClone( parseTest(testDir/"post.ssz_snappy", SSZ, altair.BeaconState)) - upgradedState = newClone(upgrade_to_altair(preState[])) + upgradedState = upgrade_to_altair(preState[]) check: upgradedState[].hash_tree_root() == postState[].hash_tree_root() reportDiff(upgradedState, postState) diff --git a/tests/official/altair/test_fixture_transition.nim b/tests/official/altair/test_fixture_transition.nim new file mode 100644 index 000000000..4c1ce1c38 --- /dev/null +++ b/tests/official/altair/test_fixture_transition.nim @@ -0,0 +1,89 @@ +# beacon_chain +# Copyright (c) 2021-Present 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. + +{.used.} + +import + chronicles, yaml, + # Standard library + os, sequtils, + # Status internal + faststreams, streams, + # Beacon chain internals + ../../../beacon_chain/spec/[crypto, state_transition, presets], + ../../../beacon_chain/spec/datatypes/[phase0, altair], + ../../../beacon_chain/[extras, ssz], + # Test utilities + ../../testutil, + ../fixtures_utils + +const + TransitionDir = SszTestsDir/const_preset/"altair"/"transition"/"core"/"pyspec_tests" + +type + TransitionEpoch = object + post_fork: string + fork_epoch: int + blocks_count: int + fork_block {.defaultVal: 0.}: int + +proc runTest(testName, testDir, unitTestName: string) = + # We wrap the tests in a proc to avoid running out of globals + # in the future: Nim supports up to 3500 globals + # but unittest with the macro/templates put everything as globals + # https://github.com/nim-lang/Nim/issues/12084#issue-486866402 + + let testPath = testDir / unitTestName + + var transitionEpoch: TransitionEpoch + var s = openFileStream(testPath/"meta.yaml") + defer: close(s) + yaml.load(s, transitionEpoch) + + proc `testImpl _ blck _ testName`() = + test testName & " - " & unitTestName & preset(): + var + preState = newClone(parseTest(testPath/"pre.ssz_snappy", SSZ, phase0.BeaconState)) + sdPreState = (ref ForkedHashedBeaconState)(hbsPhase0: phase0.HashedBeaconState( + data: preState[], root: hash_tree_root(preState[])), beaconStateFork: forkPhase0) + cache = StateCache() + rewards = RewardInfo() + + # In test cases with more than 10 blocks the first 10 aren't 0-prefixed, + # so purely lexicographic sorting wouldn't sort properly. + let numBlocks = toSeq(walkPattern(testPath/"blocks_*.ssz_snappy")).len + for i in 0 ..< numBlocks: + let inBeforeTimes = i <= transitionEpoch.fork_block and transitionEpoch.fork_block > 0 + if inBeforeTimes: + let blck = parseTest(testPath/"blocks_" & $i & ".ssz_snappy", SSZ, phase0.SignedBeaconBlock) + + let success = state_transition( + defaultRuntimePreset, sdPreState[], blck, + cache, rewards, + flags = {skipStateRootValidation}, noRollback, + transitionEpoch.fork_epoch.Epoch) + doAssert success, "Failure when applying block " & $i + else: + let blck = parseTest(testPath/"blocks_" & $i & ".ssz_snappy", SSZ, altair.SignedBeaconBlock) + + let success = state_transition( + defaultRuntimePreset, sdPreState[], blck, + cache, rewards, + flags = {skipStateRootValidation}, noRollback, + transitionEpoch.fork_epoch.Epoch) + doAssert success, "Failure when applying block " & $i + + let postState = newClone(parseTest(testPath/"post.ssz_snappy", SSZ, altair.BeaconState)) + when false: + reportDiff(sdPreState.data, postState) + doAssert getStateRoot(sdPreState[]) == postState[].hash_tree_root() + + `testImpl _ blck _ testName`() + +suite "Official - Altair - Transition " & preset(): + for kind, path in walkDir(TransitionDir, true): + runTest("Official - Altair - Transition", TransitionDir, path) diff --git a/tests/test_state_transition.nim b/tests/test_state_transition.nim deleted file mode 100644 index c39d03320..000000000 --- a/tests/test_state_transition.nim +++ /dev/null @@ -1,131 +0,0 @@ -# beacon_chain -# Copyright (c) 2018-2020 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. - -{.used.} - -import - chronicles, - unittest2, - ./testutil, ./testblockutil, - ../beacon_chain/spec/[beaconstate, datatypes, digest, crypto, - validator, state_transition, presets], - ../beacon_chain/ssz - -proc makeAttestation( - state: BeaconState, beacon_block_root: Eth2Digest, - validator_index: ValidatorIndex, cache: var StateCache): Attestation = - # The called functions don't use the extra-BeaconState parts of StateData. - let - stateData = (ref StateData)( - data: HashedBeaconState(data: state), - blck: BlockRef(root: beacon_block_root, slot: state.slot)) - (committee, slot, index) = - find_beacon_committee(stateData[], validator_index, cache) - makeAttestation(stateData[], beacon_block_root, committee, slot, index, - validator_index, cache) - -suite "Block processing" & preset(): - ## For now just test that we can compile and execute block processing with - ## mock data. - - let - # Genesis state with minimal number of deposits - # TODO bls verification is a bit of a bottleneck here - genesisState = newClone(initialize_hashed_beacon_state_from_eth1( - defaultRuntimePreset, Eth2Digest(), 0, makeInitialDeposits(), {})) - genesisBlock = get_initial_beacon_block(genesisState.data) - genesisRoot = hash_tree_root(genesisBlock.message) - - setup: - var - state = newClone(genesisState[]) - cache = StateCache() - rewards = RewardInfo() - - test "Passes from genesis state, no block" & preset(): - check: - process_slots(state[], state.data.slot + 1, cache, rewards) - state.data.slot == genesisState.data.slot + 1 - - test "Passes from genesis state, empty block" & preset(): - var - previous_block_root = genesisBlock.root - new_block = makeTestBlock(state[], previous_block_root, cache) - - let block_ok = state_transition( - defaultRuntimePreset, state[], new_block, cache, rewards, {}, noRollback) - - check: - block_ok - - state.data.slot == genesisState.data.slot + 1 - - test "Passes through epoch update, no block" & preset(): - check: - process_slots(state[], Slot(SLOTS_PER_EPOCH), cache, rewards) - state.data.slot == genesisState.data.slot + SLOTS_PER_EPOCH - - test "Passes through epoch update, empty block" & preset(): - var - previous_block_root = genesisRoot - - for i in 1..SLOTS_PER_EPOCH: - let new_block = makeTestBlock(state[], previous_block_root, cache) - - let block_ok = state_transition( - defaultRuntimePreset, state[], new_block, cache, rewards, {}, noRollback) - - check: - block_ok - - previous_block_root = new_block.root - - check: - state.data.slot == genesisState.data.slot + SLOTS_PER_EPOCH - - test "Attestation gets processed at epoch" & preset(): - var - previous_block_root = genesisRoot - - # Slot 0 is a finalized slot - won't be making attestations for it.. - check: - process_slots(state[], state.data.slot + 1, cache, rewards) - - let - # Create an attestation for slot 1 signed by the only attester we have! - beacon_committee = - get_beacon_committee(state.data, state.data.slot, 0.CommitteeIndex, cache) - attestation = makeAttestation( - state.data, previous_block_root, beacon_committee[0], cache) - - # Some time needs to pass before attestations are included - this is - # to let the attestation propagate properly to interested participants - check: - process_slots( - state[], GENESIS_SLOT + MIN_ATTESTATION_INCLUSION_DELAY + 1, cache, - rewards) - - let - new_block = makeTestBlock(state[], previous_block_root, cache, - attestations = @[attestation] - ) - check state_transition( - defaultRuntimePreset, state[], new_block, cache, rewards, {}, noRollback) - - check: - # TODO epoch attestations can get multiplied now; clean up paths to - # enable exact 1-check again and keep finalization. - state.data.current_epoch_attestations.len >= 1 - - check: - process_slots(state[], Slot(191), cache, rewards) - - # Would need to process more epochs for the attestation to be removed from - # the state! (per above bug) - # - # check: - # state.latest_attestations.len == 0