[Ready 1/2] Fork choice rewrite (#865)
* initial fork-choice refactor * Add fork_choice test for "no votes" * Initial test with voting: fix handling of unknown validators and parent blocks * Fix tiebreak of votes * Cleanup debugging traces * Complexify the vote test * fakeHash use the bigEndian repr of number + fix tiebreak for good * Stash changes: found critical bug in nimcrypto `==` and var openarray * Passing fork choice tests with varying votes * Add FFG fork choice scenario + fork choice to the test suite * Not sure why lmdb / rocksdb reappeared in rebase * Add sanity checks to .nimble file + integrate fork choice tests to the test DB and test timing * Cleanup debugging echos * nimcrypto fix https://github.com/status-im/nim-beacon-chain/pull/864 as been merged, remove TODO comment * Turn fork choice exception-free * Cleanup "result" to ensure early return is properly used * Add a comment on private/public error code vs Result * result -> results following https://github.com/status-im/nim-beacon-chain/pull/866 * Address comments: - raises: [Defect] doesn't work -> TODO - process_attestation cannot fail - try/except as expression pending Nim v1.2.0 - cleanup TODOs * re-enable all sanity checks * tag no raise for process_attestation * use raises defect everywhere in fork choice and fix process_attestation test
This commit is contained in:
parent
0f47c76b50
commit
cbc998ed93
|
@ -46,6 +46,14 @@ OK: 2/2 Fail: 0/2 Skip: 0/2
|
|||
+ Multiaddress to ENode OK
|
||||
```
|
||||
OK: 2/2 Fail: 0/2 Skip: 0/2
|
||||
## Fork Choice + Finality [Preset: mainnet]
|
||||
```diff
|
||||
+ fork_choice - testing finality #01 OK
|
||||
+ fork_choice - testing finality #02 OK
|
||||
+ fork_choice - testing no votes OK
|
||||
+ fork_choice - testing with votes OK
|
||||
```
|
||||
OK: 4/4 Fail: 0/4 Skip: 0/4
|
||||
## Honest validator
|
||||
```diff
|
||||
+ Attestation topics OK
|
||||
|
@ -234,4 +242,4 @@ OK: 4/4 Fail: 0/4 Skip: 0/4
|
|||
OK: 8/8 Fail: 0/8 Skip: 0/8
|
||||
|
||||
---TOTAL---
|
||||
OK: 145/148 Fail: 3/148 Skip: 0/148
|
||||
OK: 149/152 Fail: 3/152 Skip: 0/152
|
||||
|
|
|
@ -73,6 +73,14 @@ OK: 2/2 Fail: 0/2 Skip: 0/2
|
|||
+ Multiaddress to ENode OK
|
||||
```
|
||||
OK: 2/2 Fail: 0/2 Skip: 0/2
|
||||
## Fork Choice + Finality [Preset: minimal]
|
||||
```diff
|
||||
+ fork_choice - testing finality #01 OK
|
||||
+ fork_choice - testing finality #02 OK
|
||||
+ fork_choice - testing no votes OK
|
||||
+ fork_choice - testing with votes OK
|
||||
```
|
||||
OK: 4/4 Fail: 0/4 Skip: 0/4
|
||||
## Honest validator
|
||||
```diff
|
||||
+ Attestation topics OK
|
||||
|
@ -261,4 +269,4 @@ OK: 4/4 Fail: 0/4 Skip: 0/4
|
|||
OK: 8/8 Fail: 0/8 Skip: 0/8
|
||||
|
||||
---TOTAL---
|
||||
OK: 160/163 Fail: 3/163 Skip: 0/163
|
||||
OK: 164/167 Fail: 3/167 Skip: 0/167
|
||||
|
|
|
@ -52,8 +52,12 @@ task test, "Run all tests":
|
|||
# price we pay for that.
|
||||
|
||||
# Minimal config
|
||||
buildBinary "proto_array", "beacon_chain/fork_choice/", "-d:const_preset=minimal"
|
||||
buildBinary "fork_choice", "beacon_chain/fork_choice/", "-d:const_preset=minimal"
|
||||
buildBinary "all_tests", "tests/", "-d:chronicles_log_level=TRACE -d:const_preset=minimal"
|
||||
# Mainnet config
|
||||
buildBinary "proto_array", "beacon_chain/fork_choice/", "-d:const_preset=mainnet"
|
||||
buildBinary "fork_choice", "beacon_chain/fork_choice/", "-d:const_preset=mainnet"
|
||||
buildBinary "all_tests", "tests/", "-d:const_preset=mainnet"
|
||||
|
||||
# Generic SSZ test, doesn't use consensus objects minimal/mainnet presets
|
||||
|
@ -69,4 +73,3 @@ task test, "Run all tests":
|
|||
# State sim; getting into 4th epoch useful to trigger consensus checks
|
||||
buildBinary "state_sim", "research/", "", "--validators=1024 --slots=32"
|
||||
buildBinary "state_sim", "research/", "-d:const_preset=mainnet", "--validators=1024 --slots=128"
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# Fork choice implementations
|
||||
|
||||
References:
|
||||
- https://github.com/ethereum/eth2.0-specs/blob/v0.10.1/specs/phase0/fork-choice.md
|
||||
- https://github.com/protolambda/lmd-ghost
|
|
@ -0,0 +1,582 @@
|
|||
# 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.
|
||||
|
||||
import
|
||||
# Standard library
|
||||
std/tables, std/options, std/typetraits,
|
||||
# Status libraries
|
||||
stew/results,
|
||||
# Internal
|
||||
../spec/[datatypes, digest],
|
||||
# Fork choice
|
||||
./fork_choice_types, ./proto_array
|
||||
|
||||
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/fork-choice.md
|
||||
# This is a port of https://github.com/sigp/lighthouse/pull/804
|
||||
# which is a port of "Proto-Array": https://github.com/protolambda/lmd-ghost
|
||||
# See also:
|
||||
# - Protolambda port of Lighthouse: https://github.com/protolambda/eth2-py-hacks/blob/ae286567/proto_array.py
|
||||
# - Prysmatic writeup: https://hackmd.io/bABJiht3Q9SyV3Ga4FT9lQ#High-level-concept
|
||||
# - Gasper Whitepaper: https://arxiv.org/abs/2003.03052
|
||||
|
||||
const DefaultPruneThreshold = 256
|
||||
|
||||
# Forward declarations
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
func compute_deltas(
|
||||
deltas: var openarray[Delta],
|
||||
indices: Table[Eth2Digest, Index],
|
||||
votes: var openArray[VoteTracker],
|
||||
old_balances: openarray[Gwei],
|
||||
new_balances: openarray[Gwei]
|
||||
): ForkChoiceError {.raises: [Defect].}
|
||||
# TODO: raises [Defect] - once https://github.com/nim-lang/Nim/issues/12862 is fixed
|
||||
# https://github.com/status-im/nim-beacon-chain/pull/865#pullrequestreview-389117232
|
||||
|
||||
# Fork choice routines
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
# API:
|
||||
# - The private procs uses the ForkChoiceError error code
|
||||
# - The public procs use Result
|
||||
|
||||
func initForkChoice*(
|
||||
finalized_block_slot: Slot,
|
||||
finalized_block_state_root: Eth2Digest,
|
||||
justified_epoch: Epoch,
|
||||
finalized_epoch: Epoch,
|
||||
finalized_root: Eth2Digest
|
||||
): Result[ForkChoice, string] {.raises: [Defect].} =
|
||||
## Initialize a fork choice context
|
||||
var proto_array = ProtoArray(
|
||||
prune_threshold: DefaultPruneThreshold,
|
||||
justified_epoch: justified_epoch,
|
||||
finalized_epoch: finalized_epoch
|
||||
)
|
||||
|
||||
let err = proto_array.on_block(
|
||||
finalized_block_slot,
|
||||
finalized_root,
|
||||
none(Eth2Digest),
|
||||
finalized_block_state_root,
|
||||
justified_epoch,
|
||||
finalized_epoch
|
||||
)
|
||||
|
||||
if err.kind != fcSuccess:
|
||||
return err("Failed to add finalized block to proto_array: " & $err)
|
||||
return ok(ForkChoice(proto_array: proto_array))
|
||||
|
||||
func extend[T](s: var seq[T], minLen: int) {.raises: [Defect].} =
|
||||
## Extend a sequence so that it can contains at least `minLen` elements.
|
||||
## If it's already bigger, the sequence is unmodified.
|
||||
## The extension is zero-initialized
|
||||
let curLen = s.len
|
||||
let diff = minLen - curLen
|
||||
if diff > 0:
|
||||
# Note: seq has a length and a capacity.
|
||||
# If the new length is less than the original capacity
|
||||
# => setLen will not zeroMem
|
||||
# If the capacity was too small
|
||||
# => reallocation occurs
|
||||
# => the fresh buffer is zeroMem-ed
|
||||
# In the second case our own zeroMem is redundant
|
||||
# but this should happen rarely as we reuse the buffer
|
||||
# most of the time
|
||||
s.setLen(minLen)
|
||||
zeroMem(s[curLen].addr, diff * sizeof(T))
|
||||
|
||||
|
||||
func process_attestation*(
|
||||
self: var ForkChoice,
|
||||
validator_index: ValidatorIndex,
|
||||
block_root: Eth2Digest,
|
||||
target_epoch: Epoch
|
||||
) {.raises: [Defect].} =
|
||||
## Add an attestation to the fork choice context
|
||||
self.votes.extend(validator_index.int + 1)
|
||||
|
||||
template vote: untyped {.dirty.} = self.votes[validator_index.int]
|
||||
# alias
|
||||
|
||||
if target_epoch > vote.next_epoch or vote == default(VoteTracker):
|
||||
# TODO: the "default" condition is probably unneeded
|
||||
vote.next_root = block_root
|
||||
vote.next_epoch = target_epoch
|
||||
|
||||
|
||||
func process_block*(
|
||||
self: var ForkChoice,
|
||||
slot: Slot,
|
||||
block_root: Eth2Digest,
|
||||
parent_root: Eth2Digest,
|
||||
state_root: Eth2Digest,
|
||||
justified_epoch: Epoch,
|
||||
finalized_epoch: Epoch
|
||||
): Result[void, string] {.raises: [Defect].} =
|
||||
## Add a block to the fork choice context
|
||||
let err = self.proto_array.on_block(
|
||||
slot, block_root, some(parent_root), state_root, justified_epoch, finalized_epoch
|
||||
)
|
||||
if err.kind != fcSuccess:
|
||||
return err("process_block_error: " & $err)
|
||||
return ok()
|
||||
|
||||
|
||||
func find_head*(
|
||||
self: var ForkChoice,
|
||||
justified_epoch: Epoch,
|
||||
justified_root: Eth2Digest,
|
||||
finalized_epoch: Epoch,
|
||||
justified_state_balances: seq[Gwei]
|
||||
): Result[Eth2Digest, string] {.raises: [Defect].} =
|
||||
## Returns the new blockchain head
|
||||
|
||||
# Compute deltas with previous call
|
||||
# we might want to reuse the `deltas` buffer across calls
|
||||
var deltas = newSeq[Delta](self.proto_array.indices.len)
|
||||
let delta_err = deltas.compute_deltas(
|
||||
indices = self.proto_array.indices,
|
||||
votes = self.votes,
|
||||
old_balances = self.balances,
|
||||
new_balances = justified_state_balances
|
||||
)
|
||||
if delta_err.kind != fcSuccess:
|
||||
return err("find_head compute_deltas failed: " & $delta_err)
|
||||
|
||||
# Apply score changes
|
||||
let score_err = self.proto_array.apply_score_changes(
|
||||
deltas, justified_epoch, finalized_epoch
|
||||
)
|
||||
if score_err.kind != fcSuccess:
|
||||
return err("find_head apply_score_changes failed: " & $score_err)
|
||||
|
||||
self.balances = justified_state_balances
|
||||
|
||||
# Find the best block
|
||||
var new_head{.noInit.}: Eth2Digest
|
||||
let ghost_err = self.proto_array.find_head(new_head, justified_root)
|
||||
if ghost_err.kind != fcSuccess:
|
||||
return err("find_head failed: " & $ghost_err)
|
||||
|
||||
return ok(new_head)
|
||||
|
||||
|
||||
func maybe_prune*(
|
||||
self: var ForkChoice, finalized_root: Eth2Digest
|
||||
): Result[void, string] {.raises: [Defect].} =
|
||||
## Prune blocks preceding the finalized root as they are now unneeded.
|
||||
let err = self.proto_array.maybe_prune(finalized_root)
|
||||
if err.kind != fcSuccess:
|
||||
return err("find_head maybe_pruned failed: " & $err)
|
||||
return ok()
|
||||
|
||||
func compute_deltas(
|
||||
deltas: var openarray[Delta],
|
||||
indices: Table[Eth2Digest, Index],
|
||||
votes: var openArray[VoteTracker],
|
||||
old_balances: openarray[Gwei],
|
||||
new_balances: openarray[Gwei]
|
||||
): ForkChoiceError {.raises: [Defect].} =
|
||||
## Update `deltas`
|
||||
## between old and new balances
|
||||
## between votes
|
||||
##
|
||||
## `deltas.len` must match `indices.len` (lenght match)
|
||||
##
|
||||
## Error:
|
||||
## - If a value in indices is greater than `indices.len`
|
||||
## - If a `Eth2Digest` in `votes` does not exist in `indices`
|
||||
## except for the `default(Eth2Digest)` (i.e. zero hash)
|
||||
|
||||
for val_index, vote in votes.mpairs():
|
||||
# No need to create a score change if the validator has never voted
|
||||
# or if votes are for the zero hash (alias to the genesis block)
|
||||
if vote.current_root == default(Eth2Digest) and vote.next_root == default(Eth2Digest):
|
||||
continue
|
||||
|
||||
# If the validator was not included in `old_balances` (i.e. did not exist)
|
||||
# its balance is zero
|
||||
let old_balance = if val_index < old_balances.len: old_balances[val_index]
|
||||
else: 0
|
||||
|
||||
# If the validator is not known in the `new_balances` then use balance of zero
|
||||
#
|
||||
# It is possible that there is a vote for an unknown validator if we change our
|
||||
# justified state to a new state with a higher epoch on a different fork
|
||||
# because that fork may have on-boarded less validators than the previous fork.
|
||||
#
|
||||
# Note that attesters are not different as they are activated only under finality
|
||||
let new_balance = if val_index < new_balances.len: new_balances[val_index]
|
||||
else: 0
|
||||
|
||||
if vote.current_root != vote.next_root or old_balance != new_balance:
|
||||
# Ignore the current or next vote if it is not known in `indices`.
|
||||
# We assume that it is outside of our tree (i.e., pre-finalization) and therefore not interesting.
|
||||
if vote.current_root in indices:
|
||||
let index = indices.unsafeGet(vote.current_root)
|
||||
if index >= deltas.len:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrInvalidNodeDelta,
|
||||
index: index
|
||||
)
|
||||
deltas[index] -= Delta old_balance
|
||||
# Note that delta can be negative
|
||||
# TODO: is int64 big enough?
|
||||
|
||||
if vote.next_root in indices:
|
||||
let index = indices.unsafeGet(vote.next_root)
|
||||
if index >= deltas.len:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrInvalidNodeDelta,
|
||||
index: index
|
||||
)
|
||||
deltas[index] += Delta new_balance
|
||||
# Note that delta can be negative
|
||||
# TODO: is int64 big enough?
|
||||
|
||||
vote.current_root = vote.next_root
|
||||
return ForkChoiceSuccess
|
||||
|
||||
# Sanity checks
|
||||
# ----------------------------------------------------------------------
|
||||
# Sanity checks on internal private procedures
|
||||
|
||||
when isMainModule:
|
||||
import stew/endians2
|
||||
|
||||
func fakeHash*(index: SomeInteger): Eth2Digest =
|
||||
## Create fake hashes
|
||||
## Those are just the value serialized in big-endian
|
||||
## We add 16x16 to avoid having a zero hash are those are special cased
|
||||
## We store them in the first 8 bytes
|
||||
## as those are the one used in hash tables Table[Eth2Digest, T]
|
||||
result.data[0 ..< 8] = (16*16+index).uint64.toBytesBE()
|
||||
|
||||
proc tZeroHash() =
|
||||
echo " fork_choice compute_deltas - test zero votes"
|
||||
|
||||
const validator_count = 16
|
||||
var deltas = newSeqUninitialized[Delta](validator_count)
|
||||
|
||||
var indices: Table[Eth2Digest, Index]
|
||||
var votes: seq[VoteTracker]
|
||||
var old_balances: seq[Gwei]
|
||||
var new_balances: seq[Gwei]
|
||||
|
||||
for i in 0 ..< validator_count:
|
||||
indices.add fakeHash(i), i
|
||||
votes.add default(VoteTracker)
|
||||
old_balances.add 0
|
||||
new_balances.add 0
|
||||
|
||||
let err = deltas.compute_deltas(
|
||||
indices, votes, old_balances, new_balances
|
||||
)
|
||||
|
||||
doAssert err.kind == fcSuccess, "compute_deltas finished with error: " & $err
|
||||
|
||||
doAssert deltas == newSeq[Delta](validator_count), "deltas should be zeros"
|
||||
|
||||
for vote in votes:
|
||||
doAssert vote.current_root == vote.next_root, "The vote should have been updated"
|
||||
|
||||
|
||||
proc tAll_voted_the_same() =
|
||||
echo " fork_choice compute_deltas - test all same votes"
|
||||
|
||||
const
|
||||
Balance = Gwei(42)
|
||||
validator_count = 16
|
||||
var deltas = newSeqUninitialized[Delta](validator_count)
|
||||
|
||||
var indices: Table[Eth2Digest, Index]
|
||||
var votes: seq[VoteTracker]
|
||||
var old_balances: seq[Gwei]
|
||||
var new_balances: seq[Gwei]
|
||||
|
||||
for i in 0 ..< validator_count:
|
||||
indices.add fakeHash(i), i
|
||||
votes.add VoteTracker(
|
||||
current_root: default(Eth2Digest),
|
||||
next_root: fakeHash(0), # Get a non-zero hash
|
||||
next_epoch: Epoch(0)
|
||||
)
|
||||
old_balances.add Balance
|
||||
new_balances.add Balance
|
||||
|
||||
let err = deltas.compute_deltas(
|
||||
indices, votes, old_balances, new_balances
|
||||
)
|
||||
|
||||
doAssert err.kind == fcSuccess, "compute_deltas finished with error: " & $err
|
||||
|
||||
for i, delta in deltas.pairs:
|
||||
if i == 0:
|
||||
doAssert delta == Delta(Balance * validator_count), "The 0th root should have a delta"
|
||||
else:
|
||||
doAssert delta == 0, "The non-0 indexes should have a zero delta"
|
||||
|
||||
for vote in votes:
|
||||
doAssert vote.current_root == vote.next_root, "The vote should have been updated"
|
||||
|
||||
|
||||
proc tDifferent_votes() =
|
||||
echo " fork_choice compute_deltas - test all different votes"
|
||||
|
||||
const
|
||||
Balance = Gwei(42)
|
||||
validator_count = 16
|
||||
var deltas = newSeqUninitialized[Delta](validator_count)
|
||||
|
||||
var indices: Table[Eth2Digest, Index]
|
||||
var votes: seq[VoteTracker]
|
||||
var old_balances: seq[Gwei]
|
||||
var new_balances: seq[Gwei]
|
||||
|
||||
for i in 0 ..< validator_count:
|
||||
indices.add fakeHash(i), i
|
||||
votes.add VoteTracker(
|
||||
current_root: default(Eth2Digest),
|
||||
next_root: fakeHash(i), # Each vote for a different root
|
||||
next_epoch: Epoch(0)
|
||||
)
|
||||
old_balances.add Balance
|
||||
new_balances.add Balance
|
||||
|
||||
let err = deltas.compute_deltas(
|
||||
indices, votes, old_balances, new_balances
|
||||
)
|
||||
|
||||
doAssert err.kind == fcSuccess, "compute_deltas finished with error: " & $err
|
||||
|
||||
for i, delta in deltas.pairs:
|
||||
doAssert delta == Delta(Balance), "Each root should have a delta"
|
||||
|
||||
for vote in votes:
|
||||
doAssert vote.current_root == vote.next_root, "The vote should have been updated"
|
||||
|
||||
|
||||
proc tMoving_votes() =
|
||||
echo " fork_choice compute_deltas - test moving votes"
|
||||
|
||||
const
|
||||
Balance = Gwei(42)
|
||||
validator_count = 16
|
||||
TotalDeltas = Delta(Balance * validator_count)
|
||||
var deltas = newSeqUninitialized[Delta](validator_count)
|
||||
|
||||
var indices: Table[Eth2Digest, Index]
|
||||
var votes: seq[VoteTracker]
|
||||
var old_balances: seq[Gwei]
|
||||
var new_balances: seq[Gwei]
|
||||
|
||||
for i in 0 ..< validator_count:
|
||||
indices.add fakeHash(i), i
|
||||
votes.add VoteTracker(
|
||||
# Move vote from root 0 to root 1
|
||||
current_root: fakeHash(0),
|
||||
next_root: fakeHash(1),
|
||||
next_epoch: Epoch(0)
|
||||
)
|
||||
old_balances.add Balance
|
||||
new_balances.add Balance
|
||||
|
||||
let err = deltas.compute_deltas(
|
||||
indices, votes, old_balances, new_balances
|
||||
)
|
||||
|
||||
doAssert err.kind == fcSuccess, "compute_deltas finished with error: " & $err
|
||||
|
||||
for i, delta in deltas.pairs:
|
||||
if i == 0:
|
||||
doAssert delta == -TotalDeltas, "0th root should have a negative delta"
|
||||
elif i == 1:
|
||||
doAssert delta == TotalDeltas, "1st root should have a positive delta"
|
||||
else:
|
||||
doAssert delta == 0, "The non-0 and non-1 indexes should have a zero delta"
|
||||
|
||||
for vote in votes:
|
||||
doAssert vote.current_root == vote.next_root, "The vote should have been updated"
|
||||
|
||||
|
||||
proc tMove_out_of_tree() =
|
||||
echo " fork_choice compute_deltas - test votes for unknown subtree"
|
||||
|
||||
const Balance = Gwei(42)
|
||||
|
||||
var indices: Table[Eth2Digest, Index]
|
||||
var votes: seq[VoteTracker]
|
||||
|
||||
# Add a block
|
||||
indices.add fakeHash(1), 0
|
||||
|
||||
# 2 validators
|
||||
var deltas = newSeqUninitialized[Delta](2)
|
||||
let old_balances = @[Balance, Balance]
|
||||
let new_balances = @[Balance, Balance]
|
||||
|
||||
# One validator moves their vote from the block to the zero hash
|
||||
votes.add VoteTracker(
|
||||
current_root: fakeHash(1),
|
||||
next_root: default(Eth2Digest),
|
||||
next_epoch: Epoch(0)
|
||||
)
|
||||
|
||||
# One validator moves their vote from the block to something outside of the tree
|
||||
votes.add VoteTracker(
|
||||
current_root: fakeHash(1),
|
||||
next_root: fakeHash(1337),
|
||||
next_epoch: Epoch(0)
|
||||
)
|
||||
|
||||
let err = deltas.compute_deltas(
|
||||
indices, votes, old_balances, new_balances
|
||||
)
|
||||
|
||||
doAssert err.kind == fcSuccess, "compute_deltas finished with error: " & $err
|
||||
|
||||
doAssert deltas[0] == -Delta(Balance)*2, "The 0th block should have lost both balances."
|
||||
|
||||
for vote in votes:
|
||||
doAssert vote.current_root == vote.next_root, "The vote should have been updated"
|
||||
|
||||
|
||||
proc tChanging_balances() =
|
||||
echo " fork_choice compute_deltas - test changing balances"
|
||||
|
||||
const
|
||||
OldBalance = Gwei(42)
|
||||
NewBalance = OldBalance * 2
|
||||
validator_count = 16
|
||||
TotalOldDeltas = Delta(OldBalance * validator_count)
|
||||
TotalNewDeltas = Delta(NewBalance * validator_count)
|
||||
var deltas = newSeqUninitialized[Delta](validator_count)
|
||||
|
||||
var indices: Table[Eth2Digest, Index]
|
||||
var votes: seq[VoteTracker]
|
||||
var old_balances: seq[Gwei]
|
||||
var new_balances: seq[Gwei]
|
||||
|
||||
for i in 0 ..< validator_count:
|
||||
indices.add fakeHash(i), i
|
||||
votes.add VoteTracker(
|
||||
# Move vote from root 0 to root 1
|
||||
current_root: fakeHash(0),
|
||||
next_root: fakeHash(1),
|
||||
next_epoch: Epoch(0)
|
||||
)
|
||||
old_balances.add OldBalance
|
||||
new_balances.add NewBalance
|
||||
|
||||
let err = deltas.compute_deltas(
|
||||
indices, votes, old_balances, new_balances
|
||||
)
|
||||
|
||||
doAssert err.kind == fcSuccess, "compute_deltas finished with error: " & $err
|
||||
|
||||
for i, delta in deltas.pairs:
|
||||
if i == 0:
|
||||
doAssert delta == -TotalOldDeltas, "0th root should have a negative delta"
|
||||
elif i == 1:
|
||||
doAssert delta == TotalNewDeltas, "1st root should have a positive delta"
|
||||
else:
|
||||
doAssert delta == 0, "The non-0 and non-1 indexes should have a zero delta"
|
||||
|
||||
for vote in votes:
|
||||
doAssert vote.current_root == vote.next_root, "The vote should have been updated"
|
||||
|
||||
|
||||
proc tValidator_appears() =
|
||||
echo " fork_choice compute_deltas - test validator appears"
|
||||
|
||||
const Balance = Gwei(42)
|
||||
|
||||
var indices: Table[Eth2Digest, Index]
|
||||
var votes: seq[VoteTracker]
|
||||
|
||||
# Add 2 blocks
|
||||
indices.add fakeHash(1), 0
|
||||
indices.add fakeHash(2), 1
|
||||
|
||||
# 1 validator at the start, 2 at the end
|
||||
var deltas = newSeqUninitialized[Delta](2)
|
||||
let old_balances = @[Balance]
|
||||
let new_balances = @[Balance, Balance]
|
||||
|
||||
# Both moves vote from Block 1 to 2
|
||||
for _ in 0 ..< 2:
|
||||
votes.add VoteTracker(
|
||||
current_root: fakeHash(1),
|
||||
next_root: fakeHash(2),
|
||||
next_epoch: Epoch(0)
|
||||
)
|
||||
|
||||
|
||||
let err = deltas.compute_deltas(
|
||||
indices, votes, old_balances, new_balances
|
||||
)
|
||||
|
||||
doAssert err.kind == fcSuccess, "compute_deltas finished with error: " & $err
|
||||
|
||||
doAssert deltas[0] == -Delta(Balance), "Block 1 should have lost only 1 balance"
|
||||
doAssert deltas[1] == Delta(Balance)*2, "Block 2 should have gained 2 balances"
|
||||
|
||||
for vote in votes:
|
||||
doAssert vote.current_root == vote.next_root, "The vote should have been updated"
|
||||
|
||||
|
||||
proc tValidator_disappears() =
|
||||
echo " fork_choice compute_deltas - test validator disappears"
|
||||
|
||||
const Balance = Gwei(42)
|
||||
|
||||
var indices: Table[Eth2Digest, Index]
|
||||
var votes: seq[VoteTracker]
|
||||
|
||||
# Add 2 blocks
|
||||
indices.add fakeHash(1), 0
|
||||
indices.add fakeHash(2), 1
|
||||
|
||||
# 1 validator at the start, 2 at the end
|
||||
var deltas = newSeqUninitialized[Delta](2)
|
||||
let old_balances = @[Balance, Balance]
|
||||
let new_balances = @[Balance]
|
||||
|
||||
# Both moves vote from Block 1 to 2
|
||||
for _ in 0 ..< 2:
|
||||
votes.add VoteTracker(
|
||||
current_root: fakeHash(1),
|
||||
next_root: fakeHash(2),
|
||||
next_epoch: Epoch(0)
|
||||
)
|
||||
|
||||
|
||||
let err = deltas.compute_deltas(
|
||||
indices, votes, old_balances, new_balances
|
||||
)
|
||||
|
||||
doAssert err.kind == fcSuccess, "compute_deltas finished with error: " & $err
|
||||
|
||||
doAssert deltas[0] == -Delta(Balance)*2, "Block 1 should have lost 2 balances"
|
||||
doAssert deltas[1] == Delta(Balance), "Block 2 should have gained 1 balance"
|
||||
|
||||
for vote in votes:
|
||||
doAssert vote.current_root == vote.next_root, "The vote should have been updated"
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
echo "fork_choice internal tests for compute_deltas"
|
||||
tZeroHash()
|
||||
tAll_voted_the_same()
|
||||
tDifferent_votes()
|
||||
tMoving_votes()
|
||||
tChanging_balances()
|
||||
tValidator_appears()
|
||||
tValidator_disappears()
|
|
@ -0,0 +1,127 @@
|
|||
|
||||
# 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.
|
||||
|
||||
import
|
||||
# Standard library
|
||||
std/tables, std/options,
|
||||
# Internal
|
||||
../spec/[datatypes, digest]
|
||||
|
||||
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/fork-choice.md
|
||||
# This is a port of https://github.com/sigp/lighthouse/pull/804
|
||||
# which is a port of "Proto-Array": https://github.com/protolambda/lmd-ghost
|
||||
# See also:
|
||||
# - Protolambda port of Lighthouse: https://github.com/protolambda/eth2-py-hacks/blob/ae286567/proto_array.py
|
||||
# - Prysmatic writeup: https://hackmd.io/bABJiht3Q9SyV3Ga4FT9lQ#High-level-concept
|
||||
# - Gasper Whitepaper: https://arxiv.org/abs/2003.03052
|
||||
|
||||
# ProtoArray low-level types
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
type
|
||||
FcErrKind* = enum
|
||||
## Fork Choice Error Kinds
|
||||
fcSuccess
|
||||
fcErrFinalizedNodeUnknown
|
||||
fcErrJustifiedNodeUnknown
|
||||
fcErrInvalidFinalizedRootCHange
|
||||
fcErrInvalidNodeIndex
|
||||
fcErrInvalidParentIndex
|
||||
fcErrInvalidBestChildIndex
|
||||
fcErrInvalidJustifiedIndex
|
||||
fcErrInvalidBestDescendant
|
||||
fcErrInvalidParentDelta
|
||||
fcErrInvalidNodeDelta
|
||||
fcErrDeltaUnderflow
|
||||
fcErrIndexUnderflow
|
||||
fcErrInvalidDeltaLen
|
||||
fcErrRevertedFinalizedEpoch
|
||||
fcErrInvalidBestNode
|
||||
|
||||
FcUnderflowKind* = enum
|
||||
## Fork Choice Overflow Kinds
|
||||
fcUnderflowIndices = "Indices Overflow"
|
||||
fcUnderflowBestChild = "Best Child Overflow"
|
||||
fcUnderflowBestDescendant = "Best Descendant Overflow"
|
||||
|
||||
Index* = int
|
||||
Delta* = int
|
||||
## Delta indices
|
||||
|
||||
ForkChoiceError* = object
|
||||
case kind*: FcErrKind
|
||||
of fcSuccess:
|
||||
discard
|
||||
of fcErrFinalizedNodeUnknown,
|
||||
fcErrJustifiedNodeUnknown:
|
||||
block_root*: Eth2Digest
|
||||
of fcErrInvalidFinalizedRootChange:
|
||||
discard
|
||||
of fcErrInvalidNodeIndex,
|
||||
fcErrInvalidParentIndex,
|
||||
fcErrInvalidBestChildIndex,
|
||||
fcErrInvalidJustifiedIndex,
|
||||
fcErrInvalidBestDescendant,
|
||||
fcErrInvalidParentDelta,
|
||||
fcErrInvalidNodeDelta,
|
||||
fcErrDeltaUnderflow:
|
||||
index*: Index
|
||||
of fcErrIndexUnderflow:
|
||||
underflowKind*: FcUnderflowKind
|
||||
of fcErrInvalidDeltaLen:
|
||||
deltasLen*: int
|
||||
indicesLen*: int
|
||||
of fcErrRevertedFinalizedEpoch:
|
||||
current_finalized_epoch*: Epoch
|
||||
new_finalized_epoch*: Epoch
|
||||
of fcErrInvalidBestNode:
|
||||
start_root*: Eth2Digest
|
||||
justified_epoch*: Epoch
|
||||
finalized_epoch*: Epoch
|
||||
head_root*: Eth2Digest
|
||||
head_justified_epoch*: Epoch
|
||||
head_finalized_epoch*: Epoch
|
||||
|
||||
ProtoArray* = object
|
||||
prune_threshold*: int
|
||||
justified_epoch*: Epoch
|
||||
finalized_epoch*: Epoch
|
||||
nodes*: seq[ProtoNode]
|
||||
indices*: Table[Eth2Digest, Index]
|
||||
|
||||
ProtoNode* = object
|
||||
# TODO: generic "Metadata" field for slot/state_root
|
||||
slot*: Slot # This is unnecessary for fork choice but helps external components
|
||||
state_root*: Eth2Digest # This is unnecessary for fork choice but helps external components
|
||||
# Fields used in fork choice
|
||||
root*: Eth2Digest
|
||||
parent*: Option[Index]
|
||||
justified_epoch*: Epoch
|
||||
finalized_epoch*: Epoch
|
||||
weight*: int64
|
||||
best_child*: Option[Index]
|
||||
best_descendant*: Option[Index]
|
||||
|
||||
const ForkChoiceSuccess* = ForkChoiceError(kind: fcSuccess)
|
||||
|
||||
# Fork choice high-level types
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
type
|
||||
VoteTracker* = object
|
||||
current_root*: Eth2Digest
|
||||
next_root*: Eth2Digest
|
||||
next_epoch*: Epoch
|
||||
|
||||
ForkChoice* = object
|
||||
# Note: Lighthouse is protecting all fields with Reader-Writer locks.
|
||||
# However, given the nature of the fields, I suspect sharing those fields
|
||||
# will lead to thread contention. For now, stay single-threaded. - Mamy
|
||||
proto_array*: ProtoArray
|
||||
votes*: seq[VoteTracker]
|
||||
balances*: seq[Gwei]
|
|
@ -0,0 +1,507 @@
|
|||
# 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.
|
||||
|
||||
import
|
||||
# Standard library
|
||||
std/tables, std/options, std/typetraits,
|
||||
# Internal
|
||||
../spec/[datatypes, digest],
|
||||
# Fork choice
|
||||
./fork_choice_types
|
||||
|
||||
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/fork-choice.md
|
||||
# This is a port of https://github.com/sigp/lighthouse/pull/804
|
||||
# which is a port of "Proto-Array": https://github.com/protolambda/lmd-ghost
|
||||
# See also:
|
||||
# - Protolambda port of Lighthouse: https://github.com/protolambda/eth2-py-hacks/blob/ae286567/proto_array.py
|
||||
# - Prysmatic writeup: https://hackmd.io/bABJiht3Q9SyV3Ga4FT9lQ#High-level-concept
|
||||
# - Gasper Whitepaper: https://arxiv.org/abs/2003.03052
|
||||
|
||||
# Helper
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
func tiebreak(a, b: Eth2Digest): bool =
|
||||
## Fork-Choice tie-break between 2 digests
|
||||
## Currently implemented as `>=` (greater or equal)
|
||||
## on the binary representation
|
||||
for i in 0 ..< a.data.len:
|
||||
if a.data[i] < b.data[i]:
|
||||
return false
|
||||
elif a.data[i] > b.data[i]:
|
||||
return true
|
||||
# else we have equality so far
|
||||
return true
|
||||
|
||||
template getOrFailcase*[K, V](table: Table[K, V], key: K, failcase: untyped): V =
|
||||
## Get a value from a Nim Table, turning KeyError into
|
||||
## the "failcase"
|
||||
block:
|
||||
# TODO: try/except expression with Nim v1.2.0:
|
||||
# https://github.com/status-im/nim-beacon-chain/pull/865#discussion_r404856551
|
||||
var value: V
|
||||
try:
|
||||
value = table[key]
|
||||
except KeyError:
|
||||
failcase
|
||||
value
|
||||
|
||||
template unsafeGet*[K, V](table: Table[K, V], key: K): V =
|
||||
## Get a value from a Nim Table, turning KeyError into
|
||||
## an AssertionError defect
|
||||
getOrFailcase(table, key):
|
||||
doAssert false, "The " & astToStr(table) & " table shouldn't miss a key"
|
||||
|
||||
# Forward declarations
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
func maybe_update_best_child_and_descendant(self: var ProtoArray, parent_index: Index, child_index: Index): ForkChoiceError {.raises: [Defect].}
|
||||
func node_is_viable_for_head(self: ProtoArray, node: ProtoNode): bool {.raises: [Defect].}
|
||||
func node_leads_to_viable_head(self: ProtoArray, node: ProtoNode): tuple[viable: bool, err: ForkChoiceError] {.raises: [Defect].}
|
||||
|
||||
# ProtoArray routines
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
func apply_score_changes*(
|
||||
self: var ProtoArray,
|
||||
deltas: var openarray[Delta],
|
||||
justified_epoch: Epoch,
|
||||
finalized_epoch: Epoch
|
||||
): ForkChoiceError {.raises: [Defect].}=
|
||||
## Iterate backwards through the array, touching all nodes and their parents
|
||||
## and potentially the best-child of each parent.
|
||||
##
|
||||
## The structure of `self.nodes` array ensures that the child of each node
|
||||
## is always touched before it's aprent.
|
||||
##
|
||||
## For each node the following is done:
|
||||
##
|
||||
## 1. Update the node's weight with the corresponding delta.
|
||||
## 2. Backpropagate each node's delta to its parent's delta.
|
||||
## 3. Compare the current node with the parent's best-child,
|
||||
## updating if the current node should become the best-child
|
||||
## 4. If required, update the parent's best-descendant with the current node or its best-descendant
|
||||
if deltas.len != self.indices.len:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrInvalidDeltaLen,
|
||||
deltasLen: deltas.len,
|
||||
indicesLen: self.indices.len
|
||||
)
|
||||
|
||||
self.justified_epoch = justified_epoch
|
||||
self.finalized_epoch = finalized_epoch
|
||||
|
||||
# Iterate backwards through all the indices in `self.nodes`
|
||||
for node_index in countdown(self.nodes.len - 1, 0):
|
||||
template node: untyped {.dirty.}= self.nodes[node_index]
|
||||
## Alias
|
||||
# This cannot raise the IndexError exception, how to tell compiler?
|
||||
|
||||
if node.root == default(Eth2Digest):
|
||||
continue
|
||||
|
||||
if node_index notin {0..deltas.len-1}:
|
||||
# TODO: Here `deltas.len == self.indices.len` from the previous check
|
||||
# and we can probably assume that
|
||||
# `self.indices.len == self.nodes.len` by construction
|
||||
# and avoid this check in a loop or altogether
|
||||
return ForkChoiceError(
|
||||
kind: fcErrInvalidNodeDelta,
|
||||
index: node_index
|
||||
)
|
||||
let node_delta = deltas[node_index]
|
||||
|
||||
# Apply the delta to the node
|
||||
# We fail fast if underflow, which shouldn't happen.
|
||||
# Note that delta can be negative but weight cannot
|
||||
let weight = node.weight + node_delta
|
||||
if weight < 0:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrDeltaUnderflow,
|
||||
index: node_index
|
||||
)
|
||||
node.weight = weight
|
||||
|
||||
# If the node has a parent, try to update its best-child and best-descendant
|
||||
if node.parent.isSome():
|
||||
# TODO: Nim `options` module could use some {.inline.}
|
||||
# and a mutable overload for unsafeGet
|
||||
# and a "no exceptions" (only panics) implementation.
|
||||
let parent_index = node.parent.unsafeGet()
|
||||
if parent_index notin {0..deltas.len-1}:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrInvalidParentDelta,
|
||||
index: parent_index
|
||||
)
|
||||
|
||||
# Back-propagate the nodes delta to its parent.
|
||||
deltas[parent_index] += node_delta
|
||||
|
||||
let err = self.maybe_update_best_child_and_descendant(parent_index, node_index)
|
||||
if err.kind != fcSuccess:
|
||||
return err
|
||||
|
||||
return ForkChoiceSuccess
|
||||
|
||||
|
||||
func on_block*(
|
||||
self: var ProtoArray,
|
||||
slot: Slot,
|
||||
root: Eth2Digest,
|
||||
parent: Option[Eth2Digest],
|
||||
state_root: Eth2Digest,
|
||||
justified_epoch: Epoch,
|
||||
finalized_epoch: Epoch
|
||||
): ForkChoiceError {.raises: [Defect].} =
|
||||
## Register a block with the fork choice
|
||||
## A `none` parent is only valid for Genesis
|
||||
|
||||
# If the block is already known, ignore it
|
||||
if root in self.indices:
|
||||
return ForkChoiceSuccess
|
||||
|
||||
let node_index = self.nodes.len
|
||||
|
||||
let parent_index = block:
|
||||
if parent.isNone:
|
||||
none(int)
|
||||
elif parent.unsafeGet() notin self.indices:
|
||||
# Is this possible?
|
||||
none(int)
|
||||
else:
|
||||
some(self.indices.unsafeGet(parent.unsafeGet()))
|
||||
|
||||
let node = ProtoNode(
|
||||
slot: slot,
|
||||
state_root: state_root,
|
||||
root: root,
|
||||
parent: parent_index,
|
||||
justified_epoch: justified_epoch,
|
||||
finalized_epoch: finalized_epoch,
|
||||
weight: 0,
|
||||
best_child: none(int),
|
||||
best_descendant: none(int)
|
||||
)
|
||||
|
||||
self.indices[node.root] = node_index
|
||||
self.nodes.add node # TODO: if this is costly, we can setLen + construct the node in-place
|
||||
|
||||
if parent_index.isSome():
|
||||
let err = self.maybe_update_best_child_and_descendant(parent_index.unsafeGet(), node_index)
|
||||
if err.kind != fcSuccess:
|
||||
return err
|
||||
|
||||
return ForkChoiceSuccess
|
||||
|
||||
func find_head*(
|
||||
self: var ProtoArray,
|
||||
head: var Eth2Digest,
|
||||
justified_root: Eth2Digest
|
||||
): ForkChoiceError {.raises: [Defect].} =
|
||||
## Follows the best-descendant links to find the best-block (i.e. head-block)
|
||||
##
|
||||
## ⚠️ Warning
|
||||
## The result may not be accurate if `on_new_block`
|
||||
## is not followed by `apply_score_changes` as `on_new_block` does not
|
||||
## update the whole tree.
|
||||
|
||||
let justified_index = self.indices.getOrFailcase(justified_root):
|
||||
return ForkChoiceError(
|
||||
kind: fcErrJustifiedNodeUnknown,
|
||||
block_root: justified_root
|
||||
)
|
||||
|
||||
if justified_index notin {0..self.nodes.len-1}:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrInvalidJustifiedIndex,
|
||||
index: justified_index
|
||||
)
|
||||
|
||||
template justified_node: untyped {.dirty.} = self.nodes[justified_index]
|
||||
# Alias, IndexError are defects
|
||||
|
||||
let best_descendant_index = block:
|
||||
if justified_node.best_descendant.isSome():
|
||||
justified_node.best_descendant.unsafeGet()
|
||||
else:
|
||||
justified_index
|
||||
|
||||
if best_descendant_index notin {0..self.nodes.len-1}:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrInvalidBestDescendant,
|
||||
index: best_descendant_index
|
||||
)
|
||||
template best_node: untyped {.dirty.} = self.nodes[best_descendant_index]
|
||||
# Alias, IndexError are defects
|
||||
|
||||
# Perform a sanity check to ensure the node can be head
|
||||
if not self.node_is_viable_for_head(best_node):
|
||||
return ForkChoiceError(
|
||||
kind: fcErrInvalidBestNode,
|
||||
start_root: justified_root,
|
||||
justified_epoch: self.justified_epoch,
|
||||
finalized_epoch: self.finalized_epoch,
|
||||
head_root: justified_node.root,
|
||||
head_justified_epoch: justified_node.justified_epoch,
|
||||
head_finalized_epoch: justified_node.finalized_epoch
|
||||
)
|
||||
|
||||
head = best_node.root
|
||||
return ForkChoiceSuccess
|
||||
|
||||
# TODO: pruning can be made cheaper by keeping the new offset as a field
|
||||
# in proto_array instead of scanning the table to substract the offset.
|
||||
# In that case pruning can always be done and does not need a threshold for efficiency.
|
||||
# https://github.com/protolambda/eth2-py-hacks/blob/ae286567/proto_array.py
|
||||
func maybe_prune*(
|
||||
self: var ProtoArray,
|
||||
finalized_root: Eth2Digest
|
||||
): ForkChoiceError {.raises: [Defect].} =
|
||||
## Update the tree with new finalization information.
|
||||
## The tree is pruned if and only if:
|
||||
## - The `finalized_root` and finalized epoch are different from current
|
||||
## - The number of nodes in `self` is at least `self.prune_threshold`
|
||||
##
|
||||
## Returns error if:
|
||||
## - The finalized epoch is less than the current one
|
||||
## - The finalized epoch matches the current one but the finalized root is different
|
||||
## - Internal error due to invalid indices in `self`
|
||||
let finalized_index = self.indices.getOrFailcase(finalized_root):
|
||||
return ForkChoiceError(
|
||||
kind: fcErrFinalizedNodeUnknown,
|
||||
block_root: finalized_root
|
||||
)
|
||||
|
||||
if finalized_index < self.prune_threshold:
|
||||
# Pruning small numbers of nodes incurs more overhead than leaving them as is
|
||||
return ForkChoiceSuccess
|
||||
|
||||
# Remove the `self.indices` key/values for the nodes slated for deletion
|
||||
if finalized_index notin {0..self.nodes.len-1}:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrInvalidNodeIndex,
|
||||
index: finalized_index
|
||||
)
|
||||
for node_index in 0 ..< finalized_index:
|
||||
self.indices.del(self.nodes[node_index].root)
|
||||
|
||||
# Drop all nodes prior to finalization.
|
||||
# This is done in-place with `moveMem` to avoid costly reallocations.
|
||||
static: doAssert ProtoNode.supportsCopyMem(), "ProtoNode must be a trivial type"
|
||||
let tail = self.nodes.len - finalized_index
|
||||
# TODO: can we have an unallocated `self.nodes`? i.e. self.nodes[0] is nil
|
||||
moveMem(self.nodes[0].addr, self.nodes[finalized_index].addr, tail * sizeof(ProtoNode))
|
||||
self.nodes.setLen(tail)
|
||||
|
||||
# Adjust the indices map
|
||||
for index in self.indices.mvalues():
|
||||
index -= finalized_index
|
||||
if index < 0:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrIndexUnderflow,
|
||||
underflowKind: fcUnderflowIndices
|
||||
)
|
||||
|
||||
# Iterate through all the existing nodes and adjust their indices to match
|
||||
# the new layout of `self.nodes`
|
||||
for node in self.nodes.mitems():
|
||||
# If `node.parent` is less than `finalized_index`, set it to None
|
||||
if node.parent.isSome():
|
||||
let new_parent = node.parent.unsafeGet() - finalized_index
|
||||
if new_parent < 0:
|
||||
node.parent = none(Index)
|
||||
else:
|
||||
node.parent = some(new_parent)
|
||||
|
||||
if node.best_child.isSome():
|
||||
let new_best_child = node.best_child.unsafeGet() - finalized_index
|
||||
if new_best_child < 0:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrIndexUnderflow,
|
||||
underflowKind: fcUnderflowBestChild
|
||||
)
|
||||
node.best_child = some(new_best_child)
|
||||
|
||||
if node.best_descendant.isSome():
|
||||
let new_best_descendant = node.best_descendant.unsafeGet() - finalized_index
|
||||
if new_best_descendant < 0:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrIndexUnderflow,
|
||||
underflowKind: fcUnderflowBestDescendant
|
||||
)
|
||||
node.best_descendant = some(new_best_descendant)
|
||||
|
||||
return ForkChoiceSuccess
|
||||
|
||||
|
||||
func maybe_update_best_child_and_descendant(
|
||||
self: var ProtoArray,
|
||||
parent_index: Index,
|
||||
child_index: Index): ForkChoiceError {.raises: [Defect].} =
|
||||
## Observe the parent at `parent_index` with respect to the child at `child_index` and
|
||||
## potentiatlly modify the `parent.best_child` and `parent.best_descendant` values
|
||||
##
|
||||
## There are four scenarios:
|
||||
##
|
||||
## 1. The child is already the best child
|
||||
## but it's now invalid due to a FFG change and should be removed.
|
||||
## 2. The child is already the best child
|
||||
## and the parent is updated with the new best descendant
|
||||
## 3. The child is not the best child but becomes the best child
|
||||
## 4. The child is not the best child and does not become the best child
|
||||
|
||||
if child_index notin {0..self.nodes.len-1}:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrInvalidNodeIndex,
|
||||
index: child_index
|
||||
)
|
||||
if parent_index notin {0..self.nodes.len-1}:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrInvalidNodeIndex,
|
||||
index: parent_index
|
||||
)
|
||||
|
||||
# Aliases
|
||||
template child: untyped {.dirty.} = self.nodes[child_index]
|
||||
template parent: untyped {.dirty.} = self.nodes[parent_index]
|
||||
|
||||
let (child_leads_to_viable_head, err) = self.node_leads_to_viable_head(child)
|
||||
if err.kind != fcSuccess:
|
||||
return err
|
||||
|
||||
let # Aliases to the 3 possible (best_child, best_descendant) tuples
|
||||
change_to_none = (none(Index), none(Index))
|
||||
change_to_child = (
|
||||
some(child_index),
|
||||
# Nim `options` module doesn't implement option `or`
|
||||
if child.best_descendant.isSome(): child.best_descendant
|
||||
else: some(child_index)
|
||||
)
|
||||
no_change = (parent.best_child, parent.best_descendant)
|
||||
|
||||
# TODO: state-machine? The control-flow is messy
|
||||
let (new_best_child, new_best_descendant) = block:
|
||||
if parent.best_child.isSome:
|
||||
let best_child_index = parent.best_child.unsafeGet()
|
||||
if best_child_index == child_index and not child_leads_to_viable_head:
|
||||
# The child is already the best-child of the parent
|
||||
# but it's not viable to be the head block => remove it
|
||||
change_to_none
|
||||
elif best_child_index == child_index:
|
||||
# If the child is the best-child already, set it again to ensure
|
||||
# that the best-descendant of the parent is up-to-date.
|
||||
change_to_child
|
||||
else:
|
||||
if best_child_index notin {0..self.nodes.len-1}:
|
||||
return ForkChoiceError(
|
||||
kind: fcErrInvalidBestDescendant,
|
||||
index: best_child_index
|
||||
)
|
||||
let best_child = self.nodes[best_child_index]
|
||||
|
||||
let (best_child_leads_to_viable_head, err) = self.node_leads_to_viable_head(best_child)
|
||||
if err.kind != fcSuccess:
|
||||
return err
|
||||
|
||||
if child_leads_to_viable_head and not best_child_leads_to_viable_head:
|
||||
# The child leads to a viable head, but the current best-child doesn't
|
||||
change_to_child
|
||||
elif not child_leads_to_viable_head and best_child_leads_to_viable_head:
|
||||
# The best child leads to a viable head, but the child doesn't
|
||||
no_change
|
||||
elif child.weight == best_child.weight:
|
||||
# Tie-breaker of equal weights by root
|
||||
if child.root.tiebreak(best_child.root):
|
||||
change_to_child
|
||||
else:
|
||||
no_change
|
||||
else: # Choose winner by weight
|
||||
if child.weight >= best_child.weight:
|
||||
change_to_child
|
||||
else:
|
||||
no_change
|
||||
else:
|
||||
if child_leads_to_viable_head:
|
||||
# There is no current best-child and the child is viable
|
||||
change_to_child
|
||||
else:
|
||||
# There is no current best-child but the child is not viable
|
||||
no_change
|
||||
|
||||
self.nodes[parent_index].best_child = new_best_child
|
||||
self.nodes[parent_index].best_descendant = new_best_descendant
|
||||
|
||||
return ForkChoiceSuccess
|
||||
|
||||
func node_leads_to_viable_head(
|
||||
self: ProtoArray, node: ProtoNode
|
||||
): tuple[viable: bool, err: ForkChoiceError] {.raises: [Defect].} =
|
||||
## Indicates if the node itself or its best-descendant are viable
|
||||
## for blockchain head
|
||||
let best_descendant_is_viable_for_head = block:
|
||||
if node.best_descendant.isSome():
|
||||
let best_descendant_index = node.best_descendant.unsafeGet()
|
||||
if best_descendant_index notin {0..self.nodes.len-1}:
|
||||
return (
|
||||
false,
|
||||
ForkChoiceError(
|
||||
kind: fcErrInvalidBestDescendant,
|
||||
index: best_descendant_index
|
||||
)
|
||||
)
|
||||
let best_descendant = self.nodes[best_descendant_index]
|
||||
self.node_is_viable_for_head(best_descendant)
|
||||
else:
|
||||
false
|
||||
|
||||
return (
|
||||
best_descendant_is_viable_for_head or
|
||||
self.node_is_viable_for_head(node),
|
||||
ForkChoiceSuccess
|
||||
)
|
||||
|
||||
func node_is_viable_for_head(self: ProtoArray, node: ProtoNode): bool {.raises: [Defect].} =
|
||||
## This is the equivalent of `filter_block_tree` function in eth2 spec
|
||||
## https://github.com/ethereum/eth2.0-specs/blob/v0.10.0/specs/phase0/fork-choice.md#filter_block_tree
|
||||
##
|
||||
## Any node that has a different finalized or justified epoch
|
||||
## should not be viable for the head.
|
||||
(
|
||||
(node.justified_epoch == self.justified_epoch) or
|
||||
(self.justified_epoch == Epoch(0))
|
||||
) and (
|
||||
(node.finalized_epoch == self.finalized_epoch) or
|
||||
(self.finalized_epoch == Epoch(0))
|
||||
)
|
||||
|
||||
# Sanity checks
|
||||
# ----------------------------------------------------------------------
|
||||
# Sanity checks on internal private procedures
|
||||
|
||||
when isMainModule:
|
||||
import nimcrypto/[hash, utils]
|
||||
|
||||
echo "Sanity checks on fork choice tiebreaks"
|
||||
|
||||
block:
|
||||
let a = Eth2Digest.fromHex("0x0000000000000001000000000000000000000000000000000000000000000000")
|
||||
let b = Eth2Digest.fromHex("0x0000000000000000000000000000000000000000000000000000000000000000") # sha256(1)
|
||||
|
||||
doAssert tiebreak(a, b)
|
||||
|
||||
|
||||
block:
|
||||
let a = Eth2Digest.fromHex("0x0000000000000002000000000000000000000000000000000000000000000000")
|
||||
let b = Eth2Digest.fromHex("0x0000000000000001000000000000000000000000000000000000000000000000") # sha256(1)
|
||||
|
||||
doAssert tiebreak(a, b)
|
||||
|
||||
|
||||
block:
|
||||
let a = Eth2Digest.fromHex("0xD86E8112F3C4C4442126F8E9F44F16867DA487F29052BF91B810457DB34209A4") # sha256(2)
|
||||
let b = Eth2Digest.fromHex("0x7C9FA136D4413FA6173637E883B6998D32E1D675F88CDDFF9DCBCF331820F4B8") # sha256(1)
|
||||
|
||||
doAssert tiebreak(a, b)
|
|
@ -29,7 +29,8 @@ import # Unit test
|
|||
./test_peer_pool,
|
||||
./test_sync_manager,
|
||||
./test_honest_validator,
|
||||
./test_interop
|
||||
./test_interop,
|
||||
./fork_choice/tests_fork_choice
|
||||
|
||||
import # Refactor state transition unit tests
|
||||
# TODO re-enable when useful
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
# beacon_chain
|
||||
# Copyright (c) 2018 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, std/tables, std/options,
|
||||
# Status libraries
|
||||
stew/[result, endians2],
|
||||
# Internals
|
||||
../../beacon_chain/spec/[datatypes, digest],
|
||||
../../beacon_chain/fork_choice/[fork_choice, fork_choice_types]
|
||||
|
||||
export result, datatypes, digest, fork_choice, fork_choice_types, tables, options
|
||||
|
||||
func fakeHash*(index: SomeInteger): Eth2Digest =
|
||||
## Create fake hashes
|
||||
## Those are just the value serialized in big-endian
|
||||
## We add 16x16 to avoid having a zero hash are those are special cased
|
||||
## We store them in the first 8 bytes
|
||||
## as those are the one used in hash tables Table[Eth2Digest, T]
|
||||
result.data[0 ..< 8] = (16*16+index).uint64.toBytesBE()
|
||||
|
||||
# The fork choice tests are quite complex.
|
||||
# For flexibility in block arrival, timers, operations sequencing, ...
|
||||
# we create a small interpreter that will trigger events in proper order
|
||||
# before fork choice.
|
||||
|
||||
type
|
||||
OpKind* = enum
|
||||
FindHead
|
||||
InvalidFindHead
|
||||
ProcessBlock
|
||||
ProcessAttestation
|
||||
Prune
|
||||
|
||||
Operation* = object
|
||||
# variant specific fields
|
||||
case kind*: OpKind
|
||||
of FindHead, InvalidFindHead:
|
||||
justified_epoch*: Epoch
|
||||
justified_root*: Eth2Digest
|
||||
finalized_epoch*: Epoch
|
||||
justified_state_balances*: seq[Gwei]
|
||||
expected_head*: Eth2Digest
|
||||
of ProcessBlock:
|
||||
root*: Eth2Digest
|
||||
parent_root*: Eth2Digest
|
||||
blk_justified_epoch*: Epoch
|
||||
blk_finalized_epoch*: Epoch
|
||||
of ProcessAttestation:
|
||||
validator_index*: ValidatorIndex
|
||||
block_root*: Eth2Digest
|
||||
target_epoch*: Epoch
|
||||
of Prune: # ProtoArray specific
|
||||
finalized_root*: Eth2Digest
|
||||
prune_threshold*: int
|
||||
expected_len*: int
|
||||
|
||||
func apply(ctx: var ForkChoice, id: int, op: Operation) =
|
||||
## Apply the specified operation to a ForkChoice context
|
||||
## ``id`` is additional debugging info. It is the
|
||||
## operation index.
|
||||
# debugEcho " ========================================================================================="
|
||||
case op.kind
|
||||
of FindHead, InvalidFindHead:
|
||||
let r = ctx.find_head(
|
||||
op.justified_epoch,
|
||||
op.justified_root,
|
||||
op.finalized_epoch,
|
||||
op.justified_state_balances
|
||||
)
|
||||
if op.kind == FindHead:
|
||||
doAssert r.isOk(), &"find_head (op #{id}) returned an error: {r.error}"
|
||||
doAssert r.get() == op.expected_head, &"find_head (op #{id}) returned an incorrect result: {r.get()} (expected: {op.expected_head})"
|
||||
debugEcho " Found expected head: 0x", op.expected_head, " from justified checkpoint(epoch: ", op.justified_epoch, ", root: 0x", op.justified_root, ")"
|
||||
else:
|
||||
doAssert r.isErr(), "find_head was unexpectedly successful"
|
||||
debugEcho " Detected an expected invalid head"
|
||||
of ProcessBlock:
|
||||
let r = ctx.process_block(
|
||||
slot = default(Slot), # unused in fork choice, only helpful for external components
|
||||
block_root = op.root,
|
||||
parent_root = op.parent_root,
|
||||
state_root = default(Eth2Digest), # unused in fork choice, only helpful for external components
|
||||
justified_epoch = op.blk_justified_epoch,
|
||||
finalized_epoch = op.blk_finalized_epoch
|
||||
)
|
||||
doAssert r.isOk(), &"process_block (op #{id}) returned an error: {r.error}"
|
||||
debugEcho " Processed block 0x", op.root, " with parent 0x", op.parent_root, " and justified epoch ", op.blk_justified_epoch
|
||||
of ProcessAttestation:
|
||||
ctx.process_attestation(
|
||||
validator_index = op.validator_index,
|
||||
block_root = op.block_root,
|
||||
target_epoch = op.target_epoch
|
||||
)
|
||||
debugEcho " Processed att target 0x", op.block_root, " from validator ", op.validator_index, " for epoch ", op.target_epoch
|
||||
of Prune:
|
||||
ctx.proto_array.prune_threshold = op.prune_threshold
|
||||
let r = ctx.maybe_prune(op.finalized_root)
|
||||
doAssert r.isOk(), &"prune (op #{id}) returned an error: {r.error}"
|
||||
doAssert ctx.proto_array.nodes.len == op.expected_len,
|
||||
&"prune (op #{id}): the resulting length ({ctx.proto_array.nodes.len}) was not expected ({op.expected_len})"
|
||||
debugEcho " Maybe_pruned block preceding finalized block 0x", op.finalized_root
|
||||
|
||||
func run*(ctx: var ForkChoice, ops: seq[Operation]) =
|
||||
## Apply a sequence of fork-choice operations on a store
|
||||
for i, op in ops:
|
||||
ctx.apply(i, op)
|
|
@ -0,0 +1,129 @@
|
|||
# beacon_chain
|
||||
# Copyright (c) 2018 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 ../interpreter # included to be able to use "suiteReport"
|
||||
|
||||
proc setup_finality_01(): tuple[fork_choice: ForkChoice, ops: seq[Operation]] =
|
||||
var balances = @[Gwei(1), Gwei(1)]
|
||||
let GenesisRoot = fakeHash(0)
|
||||
|
||||
# Initialize the fork choice context
|
||||
result.fork_choice = initForkChoice(
|
||||
finalized_block_slot = Slot(0), # Metadata unused in fork choice
|
||||
finalized_block_state_root = default(Eth2Digest), # Metadata unused in fork choice
|
||||
justified_epoch = Epoch(1),
|
||||
finalized_epoch = Epoch(1),
|
||||
finalized_root = GenesisRoot
|
||||
).get()
|
||||
|
||||
# ----------------------------------
|
||||
|
||||
# Head should be genesis
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: GenesisRoot
|
||||
)
|
||||
|
||||
# Build the following chain
|
||||
#
|
||||
# 0 <- just: 0, fin: 0
|
||||
# |
|
||||
# 1 <- just: 0, fin: 0
|
||||
# |
|
||||
# 2 <- just: 1, fin: 0
|
||||
# |
|
||||
# 3 <- just: 2, fin: 1
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(1),
|
||||
parent_root: GenesisRoot,
|
||||
blk_justified_epoch: Epoch(0),
|
||||
blk_finalized_epoch: Epoch(0)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(2),
|
||||
parent_root: fakeHash(1),
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(0)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(3),
|
||||
parent_root: fakeHash(2),
|
||||
blk_justified_epoch: Epoch(2),
|
||||
blk_finalized_epoch: Epoch(1)
|
||||
)
|
||||
|
||||
# Ensure that with justified epoch 0 we find 3
|
||||
#
|
||||
# 0 <- start
|
||||
# |
|
||||
# 1
|
||||
# |
|
||||
# 2
|
||||
# |
|
||||
# 3 <- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(0),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(3)
|
||||
)
|
||||
|
||||
# Ensure that with justified epoch 1 we find 2
|
||||
#
|
||||
# 0
|
||||
# |
|
||||
# 1
|
||||
# |
|
||||
# 2 <- start
|
||||
# |
|
||||
# 3 <- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: fakeHash(2),
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(2)
|
||||
)
|
||||
|
||||
# Ensure that with justified epoch 2 we find 3
|
||||
#
|
||||
# 0
|
||||
# |
|
||||
# 1
|
||||
# |
|
||||
# 2
|
||||
# |
|
||||
# 3 <- start + head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(3),
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(3)
|
||||
)
|
||||
|
||||
proc test_ffg01() =
|
||||
timedTest "fork_choice - testing finality #01":
|
||||
# for i in 0 ..< 4:
|
||||
# echo " block (", i, ") hash: ", fakeHash(i)
|
||||
# echo " ------------------------------------------------------"
|
||||
|
||||
var (ctx, ops) = setup_finality_01()
|
||||
ctx.run(ops)
|
||||
|
||||
test_ffg01()
|
|
@ -0,0 +1,391 @@
|
|||
# beacon_chain
|
||||
# Copyright (c) 2018 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 ../interpreter # included to be able to use "suiteReport"
|
||||
|
||||
proc setup_finality_02(): tuple[fork_choice: ForkChoice, ops: seq[Operation]] =
|
||||
var balances = @[Gwei(1), Gwei(1)]
|
||||
let GenesisRoot = fakeHash(0)
|
||||
|
||||
# Initialize the fork choice context
|
||||
result.fork_choice = initForkChoice(
|
||||
finalized_block_slot = Slot(0), # Metadata unused in fork choice
|
||||
finalized_block_state_root = default(Eth2Digest), # Metadata unused in fork choice
|
||||
justified_epoch = Epoch(1),
|
||||
finalized_epoch = Epoch(1),
|
||||
finalized_root = GenesisRoot
|
||||
).get()
|
||||
|
||||
# ----------------------------------
|
||||
|
||||
# Head should be genesis
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: GenesisRoot
|
||||
)
|
||||
|
||||
# Build the following tree.
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# just: 0, fin: 0 -> 1 2 <- just: 0, fin: 0
|
||||
# | |
|
||||
# just: 1, fin: 0 -> 3 4 <- just: 0, fin: 0
|
||||
# | |
|
||||
# just: 1, fin: 0 -> 5 6 <- just: 0, fin: 0
|
||||
# | |
|
||||
# just: 1, fin: 0 -> 7 8 <- just: 1, fin: 0
|
||||
# | |
|
||||
# just: 2, fin: 0 -> 9 10 <- just: 2, fin: 0
|
||||
|
||||
# Left branch
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(1),
|
||||
parent_root: GenesisRoot,
|
||||
blk_justified_epoch: Epoch(0),
|
||||
blk_finalized_epoch: Epoch(0)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(3),
|
||||
parent_root: fakeHash(1),
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(0)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(5),
|
||||
parent_root: fakeHash(3),
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(0)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(7),
|
||||
parent_root: fakeHash(5),
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(0)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(9),
|
||||
parent_root: fakeHash(7),
|
||||
blk_justified_epoch: Epoch(2),
|
||||
blk_finalized_epoch: Epoch(0)
|
||||
)
|
||||
|
||||
# Build the following tree.
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# just: 0, fin: 0 -> 1 2 <- just: 0, fin: 0
|
||||
# | |
|
||||
# just: 1, fin: 0 -> 3 4 <- just: 0, fin: 0
|
||||
# | |
|
||||
# just: 1, fin: 0 -> 5 6 <- just: 0, fin: 0
|
||||
# | |
|
||||
# just: 1, fin: 0 -> 7 8 <- just: 1, fin: 0
|
||||
# | |
|
||||
# just: 2, fin: 0 -> 9 10 <- just: 2, fin: 0
|
||||
|
||||
# Right branch
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(2),
|
||||
parent_root: GenesisRoot,
|
||||
blk_justified_epoch: Epoch(0),
|
||||
blk_finalized_epoch: Epoch(0)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(4),
|
||||
parent_root: fakeHash(2),
|
||||
blk_justified_epoch: Epoch(0),
|
||||
blk_finalized_epoch: Epoch(0)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(6),
|
||||
parent_root: fakeHash(4),
|
||||
blk_justified_epoch: Epoch(0),
|
||||
blk_finalized_epoch: Epoch(0)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(8),
|
||||
parent_root: fakeHash(6),
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(0)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(10),
|
||||
parent_root: fakeHash(8),
|
||||
blk_justified_epoch: Epoch(2),
|
||||
blk_finalized_epoch: Epoch(0)
|
||||
)
|
||||
|
||||
# Ensure that if we start at 0 we find 10 (just: 0, fin: 0).
|
||||
#
|
||||
# 0 <-- start
|
||||
# / \
|
||||
# 1 2
|
||||
# | |
|
||||
# 3 4
|
||||
# | |
|
||||
# 5 6
|
||||
# | |
|
||||
# 7 8
|
||||
# | |
|
||||
# 9 10 <-- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(0),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(10)
|
||||
)
|
||||
|
||||
# Same with justified_epoch 2
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(10)
|
||||
)
|
||||
|
||||
# Justified epoch 3 is invalid
|
||||
result.ops.add Operation(
|
||||
kind: InvalidFindHead,
|
||||
justified_epoch: Epoch(3), # <--- Wrong epoch
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances
|
||||
)
|
||||
|
||||
# Add a vote to 1.
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# +1 vote -> 1 2
|
||||
# | |
|
||||
# 3 4
|
||||
# | |
|
||||
# 5 6
|
||||
# | |
|
||||
# 7 8
|
||||
# | |
|
||||
# 9 10
|
||||
result.ops.add Operation(
|
||||
kind: ProcessAttestation,
|
||||
validator_index: ValidatorIndex(0),
|
||||
block_root: fake_hash(1),
|
||||
target_epoch: Epoch(0)
|
||||
)
|
||||
|
||||
# Ensure that if we start at 0 we find 9 (just: 0, fin: 0).
|
||||
#
|
||||
# 0 <-- start
|
||||
# / \
|
||||
# 1 2
|
||||
# | |
|
||||
# 3 4
|
||||
# | |
|
||||
# 5 6
|
||||
# | |
|
||||
# 7 8
|
||||
# | |
|
||||
# head -> 9 10
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(0),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(9)
|
||||
)
|
||||
|
||||
# Same with justified_epoch 2
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(9)
|
||||
)
|
||||
|
||||
# Justified epoch 3 is invalid
|
||||
result.ops.add Operation(
|
||||
kind: InvalidFindHead,
|
||||
justified_epoch: Epoch(3), # <--- Wrong epoch
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances
|
||||
)
|
||||
|
||||
# Add a vote to 2.
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 1 2 <- +1 vote
|
||||
# | |
|
||||
# 3 4
|
||||
# | |
|
||||
# 5 6
|
||||
# | |
|
||||
# 7 8
|
||||
# | |
|
||||
# 9 10
|
||||
result.ops.add Operation(
|
||||
kind: ProcessAttestation,
|
||||
validator_index: ValidatorIndex(1),
|
||||
block_root: fake_hash(2),
|
||||
target_epoch: Epoch(0)
|
||||
)
|
||||
|
||||
# Ensure that if we start at 0 we find 10 again (just: 0, fin: 0).
|
||||
#
|
||||
# 0 <-- start
|
||||
# / \
|
||||
# 1 2
|
||||
# | |
|
||||
# 3 4
|
||||
# | |
|
||||
# 5 6
|
||||
# | |
|
||||
# 7 8
|
||||
# | |
|
||||
# 9 10 <-- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(0),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(10)
|
||||
)
|
||||
|
||||
# Same with justified_epoch 2
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(10)
|
||||
)
|
||||
|
||||
# Justified epoch 3 is invalid
|
||||
result.ops.add Operation(
|
||||
kind: InvalidFindHead,
|
||||
justified_epoch: Epoch(3), # <--- Wrong epoch
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances
|
||||
)
|
||||
|
||||
# Ensure that if we start at 1 (instead of 0) we find 9 (just: 0, fin: 0).
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# start-> 1 2
|
||||
# | |
|
||||
# 3 4
|
||||
# | |
|
||||
# 5 6
|
||||
# | |
|
||||
# 7 8
|
||||
# | |
|
||||
# head -> 9 10
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(0),
|
||||
justified_root: fakeHash(1),
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(9)
|
||||
)
|
||||
|
||||
# Same with justified_epoch 2
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(1),
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(9)
|
||||
)
|
||||
|
||||
# Justified epoch 3 is invalid
|
||||
result.ops.add Operation(
|
||||
kind: InvalidFindHead,
|
||||
justified_epoch: Epoch(3), # <--- Wrong epoch
|
||||
justified_root: fakeHash(1),
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances
|
||||
)
|
||||
|
||||
# Ensure that if we start at 2 (instead of 0) we find 10 (just: 0, fin: 0).
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 1 2 <- start
|
||||
# | |
|
||||
# 3 4
|
||||
# | |
|
||||
# 5 6
|
||||
# | |
|
||||
# 7 8
|
||||
# | |
|
||||
# 9 10 <- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(0),
|
||||
justified_root: fakeHash(2),
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(10)
|
||||
)
|
||||
|
||||
# Same with justified_epoch 2
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(2),
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(10)
|
||||
)
|
||||
|
||||
# Justified epoch 3 is invalid
|
||||
result.ops.add Operation(
|
||||
kind: InvalidFindHead,
|
||||
justified_epoch: Epoch(3), # <--- Wrong epoch
|
||||
justified_root: fakeHash(2),
|
||||
finalized_epoch: Epoch(0),
|
||||
justified_state_balances: balances
|
||||
)
|
||||
|
||||
proc test_ffg02() =
|
||||
timedTest "fork_choice - testing finality #02":
|
||||
# for i in 0 ..< 12:
|
||||
# echo " block (", i, ") hash: ", fakeHash(i)
|
||||
# echo " ------------------------------------------------------"
|
||||
|
||||
var (ctx, ops) = setup_finality_02()
|
||||
ctx.run(ops)
|
||||
|
||||
test_ffg02()
|
|
@ -0,0 +1,266 @@
|
|||
# beacon_chain
|
||||
# Copyright (c) 2018 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 ../interpreter # included to be able to use "suiteReport"
|
||||
|
||||
proc setup_no_votes(): tuple[fork_choice: ForkChoice, ops: seq[Operation]] =
|
||||
let balances = newSeq[Gwei](16)
|
||||
let GenesisRoot = fakeHash(0)
|
||||
|
||||
# Initialize the fork choice context
|
||||
result.fork_choice = initForkChoice(
|
||||
finalized_block_slot = Slot(0), # Metadata unused in fork choice
|
||||
finalized_block_state_root = default(Eth2Digest), # Metadata unused in fork choice
|
||||
justified_epoch = Epoch(1),
|
||||
finalized_epoch = Epoch(1),
|
||||
finalized_root = GenesisRoot
|
||||
).get()
|
||||
|
||||
# ----------------------------------
|
||||
|
||||
# Head should be genesis
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: GenesisRoot
|
||||
)
|
||||
|
||||
# Add block 2
|
||||
#
|
||||
# 0
|
||||
# /
|
||||
# 2
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(2),
|
||||
parent_root: GenesisRoot,
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(1)
|
||||
)
|
||||
|
||||
# Head should be 2
|
||||
#
|
||||
# 0
|
||||
# /
|
||||
# 2 <- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(2)
|
||||
)
|
||||
|
||||
# Add block 1 as a fork
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(1),
|
||||
parent_root: GenesisRoot,
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(1)
|
||||
)
|
||||
|
||||
# Head is still 2 due to tiebreaker as fakeHash(2) (0xD8...) > fakeHash(1) (0x7C...)
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# head-> 2 1
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(2)
|
||||
)
|
||||
|
||||
# Add block 3
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(3),
|
||||
parent_root: fakeHash(1),
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(1)
|
||||
)
|
||||
|
||||
# Head is still 2
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# head-> 2 1
|
||||
# |
|
||||
# 3
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(2)
|
||||
)
|
||||
|
||||
# Add block 4
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# | |
|
||||
# 4 3
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(4),
|
||||
parent_root: fakeHash(2),
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(1)
|
||||
)
|
||||
|
||||
# Check that head is 4
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# | |
|
||||
# head-> 4 3
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(4)
|
||||
)
|
||||
|
||||
# Add block 5 with justified epoch of 2
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# | |
|
||||
# 4 3
|
||||
# |
|
||||
# 5 <- justified epoch = 2
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(5),
|
||||
parent_root: fakeHash(4),
|
||||
blk_justified_epoch: Epoch(2),
|
||||
blk_finalized_epoch: Epoch(1)
|
||||
)
|
||||
|
||||
# Ensure the head is still 4 whilst the justified epoch is 0.
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# | |
|
||||
# head-> 4 3
|
||||
# |
|
||||
# 5
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(4)
|
||||
)
|
||||
|
||||
# Ensure that there is an error when starting from a block with the wrong justified epoch
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# | |
|
||||
# 4 3
|
||||
# |
|
||||
# 5 <- starting from 5 with justified epoch 1 should error.
|
||||
result.ops.add Operation(
|
||||
kind: InvalidFindHead,
|
||||
justified_epoch: Epoch(1), # <--- Wrong epoch
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances
|
||||
)
|
||||
|
||||
# Set the justified epoch to 2 and the start block to 5 and ensure 5 is the head.
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# | |
|
||||
# 4 3
|
||||
# |
|
||||
# 5 <- head + justified
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(5)
|
||||
)
|
||||
|
||||
# Add block 6
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# | |
|
||||
# 4 3
|
||||
# |
|
||||
# 5 <- justified root
|
||||
# |
|
||||
# 6
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(6),
|
||||
parent_root: fakeHash(5),
|
||||
blk_justified_epoch: Epoch(2),
|
||||
blk_finalized_epoch: Epoch(1)
|
||||
)
|
||||
|
||||
# Ensure 6 is the head
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# | |
|
||||
# 4 3
|
||||
# |
|
||||
# 5 <- justified root
|
||||
# |
|
||||
# 6 <- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(6)
|
||||
)
|
||||
|
||||
proc test_no_votes() =
|
||||
timedTest "fork_choice - testing no votes":
|
||||
# for i in 0 ..< 6:
|
||||
# echo " block (", i, ") hash: ", fakeHash(i)
|
||||
# echo " ------------------------------------------------------"
|
||||
|
||||
var (ctx, ops) = setup_no_votes()
|
||||
ctx.run(ops)
|
||||
|
||||
test_no_votes()
|
|
@ -0,0 +1,718 @@
|
|||
# beacon_chain
|
||||
# Copyright (c) 2018 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 ../interpreter # included to be able to use "suiteReport"
|
||||
|
||||
proc setup_votes(): tuple[fork_choice: ForkChoice, ops: seq[Operation]] =
|
||||
var balances = @[Gwei(1), Gwei(1)]
|
||||
let GenesisRoot = fakeHash(0)
|
||||
|
||||
# Initialize the fork choice context
|
||||
result.fork_choice = initForkChoice(
|
||||
finalized_block_slot = Slot(0), # Metadata unused in fork choice
|
||||
finalized_block_state_root = default(Eth2Digest), # Metadata unused in fork choice
|
||||
justified_epoch = Epoch(1),
|
||||
finalized_epoch = Epoch(1),
|
||||
finalized_root = GenesisRoot
|
||||
).get()
|
||||
|
||||
# ----------------------------------
|
||||
|
||||
# Head should be genesis
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: GenesisRoot
|
||||
)
|
||||
|
||||
# Add block 2
|
||||
#
|
||||
# 0
|
||||
# /
|
||||
# 2
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(2),
|
||||
parent_root: GenesisRoot,
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(1)
|
||||
)
|
||||
|
||||
# Head should be 2
|
||||
#
|
||||
# 0
|
||||
# /
|
||||
# 2 <- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(2)
|
||||
)
|
||||
|
||||
# Add block 1 as a fork
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(1),
|
||||
parent_root: GenesisRoot,
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(1)
|
||||
)
|
||||
|
||||
# Head is still 2 due to tiebreaker as fakeHash(2) (0xD8...) > fakeHash(1) (0x7C...)
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# head-> 2 1
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(2)
|
||||
)
|
||||
|
||||
# Add a vote to block 1
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1 <- +vote
|
||||
result.ops.add Operation(
|
||||
kind: ProcessAttestation,
|
||||
validator_index: ValidatorIndex(0),
|
||||
block_root: fakeHash(1),
|
||||
target_epoch: Epoch(2)
|
||||
)
|
||||
|
||||
# Head is now 1 as 1 has an extra vote
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1 <- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(1)
|
||||
)
|
||||
|
||||
# Add a vote to block 2
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# +vote-> 2 1
|
||||
result.ops.add Operation(
|
||||
kind: ProcessAttestation,
|
||||
validator_index: ValidatorIndex(1),
|
||||
block_root: fakeHash(2),
|
||||
target_epoch: Epoch(2)
|
||||
)
|
||||
|
||||
# Head is back to 2 due to tiebreaker as fakeHash(2) (0xD8...) > fakeHash(1) (0x7C...)
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# head-> 2 1
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(2)
|
||||
)
|
||||
|
||||
# Add block 3 as on chain 1
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(3),
|
||||
parent_root: fakeHash(1),
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(1)
|
||||
)
|
||||
|
||||
# Head is still 2
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# head-> 2 1
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(2)
|
||||
)
|
||||
|
||||
# Move validator #0 vote from 1 to 3
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1 <- -vote
|
||||
# |
|
||||
# 3 <- +vote
|
||||
result.ops.add Operation(
|
||||
kind: ProcessAttestation,
|
||||
validator_index: ValidatorIndex(0),
|
||||
block_root: fakeHash(3),
|
||||
target_epoch: Epoch(3)
|
||||
)
|
||||
|
||||
# Head is still 2
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# head-> 2 1
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(2)
|
||||
)
|
||||
|
||||
# Move validator #1 vote from 2 to 1 (this is an equivocation, but fork choice doesn't
|
||||
# care)
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# -vote-> 2 1 <- +vote
|
||||
# |
|
||||
# 3
|
||||
result.ops.add Operation(
|
||||
kind: ProcessAttestation,
|
||||
validator_index: ValidatorIndex(1),
|
||||
block_root: fakeHash(1),
|
||||
target_epoch: Epoch(3)
|
||||
)
|
||||
|
||||
# Head is now 3
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3 <- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(3)
|
||||
)
|
||||
|
||||
# Add block 4 on chain 1-3
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(4),
|
||||
parent_root: fakeHash(3),
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(1)
|
||||
)
|
||||
|
||||
# Head is now 4
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4 <- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(4)
|
||||
)
|
||||
|
||||
# Add block 5, which has a justified epoch of 2.
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4
|
||||
# /
|
||||
# 5 <- justified epoch = 2
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(5),
|
||||
parent_root: fakeHash(4),
|
||||
blk_justified_epoch: Epoch(2),
|
||||
blk_finalized_epoch: Epoch(2)
|
||||
)
|
||||
|
||||
# Ensure that 5 is filtered out and the head stays at 4.
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4 <- head
|
||||
# /
|
||||
# 5
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(4)
|
||||
)
|
||||
|
||||
# Add block 6, which has a justified epoch of 0.
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4
|
||||
# / \
|
||||
# 5 6 <- justified epoch = 0
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(6),
|
||||
parent_root: fakeHash(4),
|
||||
blk_justified_epoch: Epoch(1),
|
||||
blk_finalized_epoch: Epoch(1)
|
||||
)
|
||||
|
||||
# Move both votes to 5.
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4
|
||||
# / \
|
||||
# +2 vote-> 5 6
|
||||
result.ops.add Operation(
|
||||
kind: ProcessAttestation,
|
||||
validator_index: ValidatorIndex(0),
|
||||
block_root: fakeHash(5),
|
||||
target_epoch: Epoch(4)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessAttestation,
|
||||
validator_index: ValidatorIndex(1),
|
||||
block_root: fakeHash(5),
|
||||
target_epoch: Epoch(4)
|
||||
)
|
||||
|
||||
# Add blocks 7, 8 and 9. Adding these blocks helps test the `best_descendant`
|
||||
# functionality.
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4
|
||||
# / \
|
||||
# 5 6
|
||||
# |
|
||||
# 7
|
||||
# |
|
||||
# 8
|
||||
# /
|
||||
# 9
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(7),
|
||||
parent_root: fakeHash(5),
|
||||
blk_justified_epoch: Epoch(2),
|
||||
blk_finalized_epoch: Epoch(2)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(8),
|
||||
parent_root: fakeHash(7),
|
||||
blk_justified_epoch: Epoch(2),
|
||||
blk_finalized_epoch: Epoch(2)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(9),
|
||||
parent_root: fakeHash(8),
|
||||
blk_justified_epoch: Epoch(2),
|
||||
blk_finalized_epoch: Epoch(2)
|
||||
)
|
||||
|
||||
# Ensure that 6 is the head, even though 5 has all the votes. This is testing to ensure
|
||||
# that 5 is filtered out due to a differing justified epoch.
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4
|
||||
# / \
|
||||
# 5 6 <- head
|
||||
# |
|
||||
# 7
|
||||
# |
|
||||
# 8
|
||||
# /
|
||||
# 9
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(1),
|
||||
justified_root: GenesisRoot,
|
||||
finalized_epoch: Epoch(1),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(6)
|
||||
)
|
||||
|
||||
# Change fork-choice justified epoch to 1, and the start block to 5 and ensure that 9 is
|
||||
# the head.
|
||||
#
|
||||
# << Change justified epoch to 1 >>
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4
|
||||
# / \
|
||||
# 5 6
|
||||
# |
|
||||
# 7
|
||||
# |
|
||||
# 8
|
||||
# /
|
||||
# head-> 9
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(2),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(9)
|
||||
)
|
||||
|
||||
# Update votes to block 9
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4
|
||||
# / \
|
||||
# 5 6
|
||||
# |
|
||||
# 7
|
||||
# |
|
||||
# 8
|
||||
# /
|
||||
# 9 <- +2 votes
|
||||
result.ops.add Operation(
|
||||
kind: ProcessAttestation,
|
||||
validator_index: ValidatorIndex(0),
|
||||
block_root: fakeHash(9),
|
||||
target_epoch: Epoch(5)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessAttestation,
|
||||
validator_index: ValidatorIndex(1),
|
||||
block_root: fakeHash(9),
|
||||
target_epoch: Epoch(5)
|
||||
)
|
||||
|
||||
# Head should still be 9
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(2),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(9)
|
||||
)
|
||||
|
||||
# Add block 10
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4
|
||||
# / \
|
||||
# 5 6
|
||||
# |
|
||||
# 7
|
||||
# |
|
||||
# 8
|
||||
# / \
|
||||
# 9 10
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(10),
|
||||
parent_root: fakeHash(8),
|
||||
blk_justified_epoch: Epoch(2),
|
||||
blk_finalized_epoch: Epoch(2)
|
||||
)
|
||||
|
||||
# Head should still be 9
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(2),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(9)
|
||||
)
|
||||
|
||||
# Introduce 2 new validators
|
||||
balances = @[Gwei(1), Gwei(1), Gwei(1), Gwei(1)]
|
||||
|
||||
# Have them vote for block 10
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4
|
||||
# / \
|
||||
# 5 6
|
||||
# |
|
||||
# 7
|
||||
# |
|
||||
# 8
|
||||
# / \
|
||||
# 9 10 <- +2 votes
|
||||
result.ops.add Operation(
|
||||
kind: ProcessAttestation,
|
||||
validator_index: ValidatorIndex(2),
|
||||
block_root: fakeHash(10),
|
||||
target_epoch: Epoch(5)
|
||||
)
|
||||
result.ops.add Operation(
|
||||
kind: ProcessAttestation,
|
||||
validator_index: ValidatorIndex(3),
|
||||
block_root: fakeHash(10),
|
||||
target_epoch: Epoch(5)
|
||||
)
|
||||
|
||||
# Check that the head is now 10.
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4
|
||||
# / \
|
||||
# 5 6
|
||||
# |
|
||||
# 7
|
||||
# |
|
||||
# 8
|
||||
# / \
|
||||
# 9 10 <- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(2),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(10)
|
||||
)
|
||||
|
||||
# Set the last 2 validators balances to 0
|
||||
balances = @[Gwei(1), Gwei(1), Gwei(0), Gwei(0)]
|
||||
|
||||
# head should be 9 again
|
||||
# .
|
||||
# |
|
||||
# 8
|
||||
# / \
|
||||
# head -> 9 10
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(2),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(9)
|
||||
)
|
||||
|
||||
# Set the last 2 validators balances back to 1
|
||||
balances = @[Gwei(1), Gwei(1), Gwei(1), Gwei(1)]
|
||||
|
||||
# head should be 10 again
|
||||
# .
|
||||
# |
|
||||
# 8
|
||||
# / \
|
||||
# 9 10 <- head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(2),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(10)
|
||||
)
|
||||
|
||||
# Remove the validators
|
||||
balances = @[Gwei(1), Gwei(1)]
|
||||
|
||||
# head should be 9 again
|
||||
# .
|
||||
# |
|
||||
# 8
|
||||
# / \
|
||||
# head -> 9 10
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(2),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(9)
|
||||
)
|
||||
|
||||
# Pruning below the prune threshold doesn't prune
|
||||
result.ops.add Operation(
|
||||
kind: Prune,
|
||||
finalized_root: fakeHash(5),
|
||||
prune_threshold: high(int),
|
||||
expected_len: 11
|
||||
)
|
||||
|
||||
# Prune shouldn't have changed the head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(2),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(9)
|
||||
)
|
||||
|
||||
# Ensure that pruning above the prune threshold does prune.
|
||||
#
|
||||
#
|
||||
# 0
|
||||
# / \
|
||||
# 2 1
|
||||
# |
|
||||
# 3
|
||||
# |
|
||||
# 4
|
||||
# -------pruned here ------
|
||||
# 5 6
|
||||
# |
|
||||
# 7
|
||||
# |
|
||||
# 8
|
||||
# / \
|
||||
# 9 10
|
||||
result.ops.add Operation(
|
||||
kind: Prune,
|
||||
finalized_root: fakeHash(5),
|
||||
prune_threshold: 1,
|
||||
expected_len: 6
|
||||
)
|
||||
|
||||
# Prune shouldn't have changed the head
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(2),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(9)
|
||||
)
|
||||
|
||||
# Add block 11
|
||||
#
|
||||
# 5 6
|
||||
# |
|
||||
# 7
|
||||
# |
|
||||
# 8
|
||||
# / \
|
||||
# 9 10
|
||||
# |
|
||||
# 11
|
||||
result.ops.add Operation(
|
||||
kind: ProcessBlock,
|
||||
root: fakeHash(11),
|
||||
parent_root: fakeHash(9),
|
||||
blk_justified_epoch: Epoch(2),
|
||||
blk_finalized_epoch: Epoch(2)
|
||||
)
|
||||
|
||||
# Head is now 11
|
||||
result.ops.add Operation(
|
||||
kind: FindHead,
|
||||
justified_epoch: Epoch(2),
|
||||
justified_root: fakeHash(5),
|
||||
finalized_epoch: Epoch(2),
|
||||
justified_state_balances: balances,
|
||||
expected_head: fakeHash(11)
|
||||
)
|
||||
|
||||
proc test_votes() =
|
||||
timedTest "fork_choice - testing with votes":
|
||||
# for i in 0 ..< 12:
|
||||
# echo " block (", i, ") hash: ", fakeHash(i)
|
||||
# echo " ------------------------------------------------------"
|
||||
|
||||
var (ctx, ops) = setup_votes()
|
||||
ctx.run(ops)
|
||||
|
||||
test_votes()
|
|
@ -0,0 +1,10 @@
|
|||
# Don't forgot to run the following files as main modules:
|
||||
# - beacon_chain/fork_choice/proto_array.nim (sanity checks for tiebreak)
|
||||
# - beacon_chain/fork_choice/fork_choice.nim (sanity checks for compute_deltas)
|
||||
|
||||
import ../testutil, std/unittest
|
||||
|
||||
# include to be able to use "suiteReport"
|
||||
import ./interpreter
|
||||
suiteReport "Fork Choice + Finality " & preset():
|
||||
include scenarios/[no_votes, votes, ffg_01, ffg_02]
|
Loading…
Reference in New Issue