2020-04-09 16:15:00 +00:00
|
|
|
# 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.
|
|
|
|
|
2020-04-15 09:21:22 +00:00
|
|
|
{.push raises: [Defect].}
|
|
|
|
|
2020-04-09 16:15:00 +00:00
|
|
|
import
|
|
|
|
# Standard library
|
2020-07-28 13:54:32 +00:00
|
|
|
std/[sets, tables, typetraits],
|
2020-04-09 16:15:00 +00:00
|
|
|
# Status libraries
|
2020-06-10 06:58:12 +00:00
|
|
|
stew/results, chronicles,
|
2020-04-09 16:15:00 +00:00
|
|
|
# Internal
|
2020-07-25 19:41:12 +00:00
|
|
|
../spec/[beaconstate, datatypes, digest, helpers],
|
2020-04-09 16:15:00 +00:00
|
|
|
# Fork choice
|
2020-07-25 19:41:12 +00:00
|
|
|
./fork_choice_types, ./proto_array,
|
2020-07-31 14:49:06 +00:00
|
|
|
../block_pools/[spec_cache, chain_dag]
|
2020-07-28 13:54:32 +00:00
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
export sets, results, fork_choice_types
|
2020-04-09 16:15:00 +00:00
|
|
|
|
2020-07-25 19:41:12 +00:00
|
|
|
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md
|
2020-04-09 16:15:00 +00:00
|
|
|
# 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]
|
2020-07-30 15:48:25 +00:00
|
|
|
): FcResult[void]
|
2020-04-09 16:15:00 +00:00
|
|
|
# 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
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
2020-06-10 06:58:12 +00:00
|
|
|
logScope:
|
|
|
|
topics = "fork_choice"
|
|
|
|
|
2020-04-09 16:15:00 +00:00
|
|
|
# API:
|
|
|
|
# - The private procs uses the ForkChoiceError error code
|
|
|
|
# - The public procs use Result
|
|
|
|
|
2020-07-25 19:41:12 +00:00
|
|
|
func get_effective_balances(state: BeaconState): seq[Gwei] =
|
|
|
|
## Get the balances from a state
|
|
|
|
result.newSeq(state.validators.len) # zero-init
|
|
|
|
|
|
|
|
let epoch = state.get_current_epoch()
|
|
|
|
|
|
|
|
for i in 0 ..< result.len:
|
|
|
|
# All non-active validators have a 0 balance
|
|
|
|
template validator: Validator = state.validators[i]
|
|
|
|
if validator.is_active_validator(epoch):
|
|
|
|
result[i] = validator.effective_balance
|
|
|
|
|
|
|
|
proc initForkChoiceBackend*(justified_epoch: Epoch,
|
|
|
|
finalized_epoch: Epoch,
|
|
|
|
finalized_root: Eth2Digest,
|
2020-07-30 15:48:25 +00:00
|
|
|
): FcResult[ForkChoiceBackend] =
|
2020-04-09 16:15:00 +00:00
|
|
|
var proto_array = ProtoArray(
|
|
|
|
prune_threshold: DefaultPruneThreshold,
|
2020-07-25 19:41:12 +00:00
|
|
|
justified_epoch: finalized_epoch,
|
2020-04-09 16:15:00 +00:00
|
|
|
finalized_epoch: finalized_epoch
|
|
|
|
)
|
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
? proto_array.on_block(
|
2020-04-09 16:15:00 +00:00
|
|
|
finalized_root,
|
2020-06-10 06:58:12 +00:00
|
|
|
hasParentInForkChoice = false,
|
2020-07-25 19:41:12 +00:00
|
|
|
Eth2Digest(),
|
|
|
|
finalized_epoch,
|
2020-04-09 16:15:00 +00:00
|
|
|
finalized_epoch
|
|
|
|
)
|
|
|
|
|
2020-07-25 19:41:12 +00:00
|
|
|
ok(ForkChoiceBackend(
|
|
|
|
proto_array: proto_array,
|
|
|
|
))
|
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
proc initForkChoice*(finalizedState: StateData, ): FcResult[ForkChoice] =
|
2020-07-25 19:41:12 +00:00
|
|
|
## Initialize a fork choice context
|
|
|
|
debug "Initializing fork choice",
|
|
|
|
state_epoch = finalizedState.data.data.get_current_epoch(),
|
|
|
|
blck = shortLog(finalizedState.blck)
|
|
|
|
|
|
|
|
let finalized_epoch = finalizedState.data.data.get_current_epoch()
|
|
|
|
|
|
|
|
let ffgCheckpoint = FFGCheckpoints(
|
|
|
|
justified: BalanceCheckpoint(
|
|
|
|
blck: finalizedState.blck,
|
|
|
|
epoch: finalized_epoch,
|
|
|
|
balances: get_effective_balances(finalizedState.data.data)),
|
|
|
|
finalized: Checkpoint(root: finalizedState.blck.root, epoch: finalized_epoch))
|
|
|
|
|
|
|
|
let backend = ? initForkChoiceBackend(
|
|
|
|
finalized_epoch, finalized_epoch, finalizedState.blck.root)
|
|
|
|
|
|
|
|
ok(ForkChoice(
|
|
|
|
backend: backend,
|
|
|
|
checkpoints: Checkpoints(
|
|
|
|
current: ffgCheckpoint,
|
|
|
|
best: ffgCheckpoint),
|
|
|
|
finalizedBlock: finalizedState.blck,
|
|
|
|
))
|
2020-04-09 16:15:00 +00:00
|
|
|
|
2020-04-15 09:21:22 +00:00
|
|
|
func extend[T](s: var seq[T], minLen: int) =
|
2020-04-09 16:15:00 +00:00
|
|
|
## 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
|
2020-07-30 15:48:25 +00:00
|
|
|
if s.len < minLen:
|
2020-04-09 16:15:00 +00:00
|
|
|
s.setLen(minLen)
|
|
|
|
|
|
|
|
func process_attestation*(
|
2020-07-25 19:41:12 +00:00
|
|
|
self: var ForkChoiceBackend,
|
2020-04-09 16:15:00 +00:00
|
|
|
validator_index: ValidatorIndex,
|
|
|
|
block_root: Eth2Digest,
|
|
|
|
target_epoch: Epoch
|
2020-04-15 09:21:22 +00:00
|
|
|
) =
|
2020-07-25 19:41:12 +00:00
|
|
|
if block_root == Eth2Digest():
|
|
|
|
return
|
|
|
|
|
2020-04-09 16:15:00 +00:00
|
|
|
## Add an attestation to the fork choice context
|
|
|
|
self.votes.extend(validator_index.int + 1)
|
|
|
|
|
2020-07-25 19:41:12 +00:00
|
|
|
template vote: untyped = self.votes[validator_index.int]
|
2020-04-09 16:15:00 +00:00
|
|
|
# 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
|
|
|
|
|
2020-06-10 06:58:12 +00:00
|
|
|
{.noSideEffect.}:
|
2020-07-09 09:29:32 +00:00
|
|
|
trace "Integrating vote in fork choice",
|
2020-07-22 09:42:55 +00:00
|
|
|
validator_index = validator_index,
|
2020-07-25 19:41:12 +00:00
|
|
|
new_vote = shortLog(vote)
|
2020-06-10 06:58:12 +00:00
|
|
|
|
2020-07-25 19:41:12 +00:00
|
|
|
func process_attestation*(
|
|
|
|
self: var ForkChoice,
|
|
|
|
validator_index: ValidatorIndex,
|
|
|
|
block_root: Eth2Digest,
|
|
|
|
target_epoch: Epoch
|
|
|
|
) =
|
|
|
|
self.backend.process_attestation(validator_index, block_root, target_epoch)
|
|
|
|
|
|
|
|
func contains*(self: ForkChoiceBackend, block_root: Eth2Digest): bool =
|
2020-06-10 06:58:12 +00:00
|
|
|
## Returns `true` if a block is known to the fork choice
|
|
|
|
## and `false` otherwise.
|
|
|
|
##
|
|
|
|
## In particular, before adding a block, its parent must be known to the fork choice
|
|
|
|
self.proto_array.indices.contains(block_root)
|
2020-04-09 16:15:00 +00:00
|
|
|
|
2020-07-31 14:49:06 +00:00
|
|
|
proc get_balances_for_block(self: var Checkpoints, blck: BlockSlot, dag: ChainDAGRef): seq[Gwei] =
|
2020-07-30 19:18:17 +00:00
|
|
|
dag.withState(dag.balanceState, blck):
|
2020-07-25 19:41:12 +00:00
|
|
|
get_effective_balances(state)
|
|
|
|
|
|
|
|
proc process_state(self: var Checkpoints,
|
2020-07-31 14:49:06 +00:00
|
|
|
dag: ChainDAGRef,
|
2020-08-03 18:39:43 +00:00
|
|
|
epochRef: EpochRef,
|
2020-07-30 15:48:25 +00:00
|
|
|
blck: BlockRef) =
|
2020-08-03 18:39:43 +00:00
|
|
|
trace "Processing epoch",
|
|
|
|
epoch = epochRef.epoch,
|
|
|
|
state_justified = epochRef.current_justified_checkpoint.epoch,
|
2020-07-25 19:41:12 +00:00
|
|
|
current_justified = self.current.justified.epoch,
|
2020-08-03 18:39:43 +00:00
|
|
|
state_finalized = epochRef.finalized_checkpoint.epoch,
|
2020-07-25 19:41:12 +00:00
|
|
|
current_finalized = self.current.finalized
|
|
|
|
|
2020-08-03 18:39:43 +00:00
|
|
|
if (epochRef.current_justified_checkpoint.epoch > self.current.justified.epoch) and
|
|
|
|
(epochRef.finalized_checkpoint.epoch >= self.current.finalized.epoch):
|
2020-07-30 15:48:25 +00:00
|
|
|
let justifiedBlck = blck.atEpochStart(
|
2020-08-03 18:39:43 +00:00
|
|
|
epochRef.current_justified_checkpoint.epoch)
|
2020-07-25 19:41:12 +00:00
|
|
|
|
2020-08-03 18:39:43 +00:00
|
|
|
doAssert justifiedBlck.blck.root == epochRef.current_justified_checkpoint.root
|
2020-07-25 19:41:12 +00:00
|
|
|
|
|
|
|
let candidate = FFGCheckpoints(
|
|
|
|
justified: BalanceCheckpoint(
|
|
|
|
blck: justifiedBlck.blck,
|
2020-08-03 18:39:43 +00:00
|
|
|
epoch: epochRef.current_justified_checkpoint.epoch,
|
2020-07-30 19:18:17 +00:00
|
|
|
balances: self.get_balances_for_block(justifiedBlck, dag),
|
2020-07-25 19:41:12 +00:00
|
|
|
),
|
2020-08-03 18:39:43 +00:00
|
|
|
finalized: epochRef.finalized_checkpoint,
|
2020-07-25 19:41:12 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
trace "Applying candidate",
|
|
|
|
justified_block = shortLog(candidate.justified.blck),
|
|
|
|
justified_epoch = shortLog(candidate.justified.epoch),
|
|
|
|
finalized = candidate.finalized,
|
2020-08-03 18:39:43 +00:00
|
|
|
state_finalized = epochRef.finalized_checkpoint.epoch
|
2020-07-25 19:41:12 +00:00
|
|
|
|
|
|
|
if self.current.justified.blck.isAncestorOf(justifiedBlck.blck):
|
|
|
|
trace "Updating current",
|
|
|
|
prev = shortLog(self.current.justified.blck)
|
|
|
|
self.current = candidate
|
|
|
|
else:
|
|
|
|
trace "No current update",
|
|
|
|
prev = shortLog(self.current.justified.blck)
|
|
|
|
|
|
|
|
if candidate.justified.epoch > self.best.justified.epoch:
|
|
|
|
trace "Updating best",
|
|
|
|
prev = shortLog(self.best.justified.blck)
|
|
|
|
self.best = candidate
|
|
|
|
else:
|
|
|
|
trace "No best update",
|
|
|
|
prev = shortLog(self.best.justified.blck)
|
|
|
|
|
|
|
|
# self.balances_cache.process_state(block_root, state)?;
|
|
|
|
|
|
|
|
func compute_slots_since_epoch_start(slot: Slot): uint64 =
|
|
|
|
slot - compute_start_slot_at_epoch(compute_epoch_at_slot(slot))
|
|
|
|
|
2020-07-30 19:18:17 +00:00
|
|
|
proc maybe_update(self: var Checkpoints, current_slot: Slot) =
|
2020-07-25 19:41:12 +00:00
|
|
|
trace "Updating checkpoint",
|
|
|
|
current_slot,
|
|
|
|
best = shortLog(self.best.justified.blck),
|
|
|
|
current = shortLog(self.current.justified.blck),
|
|
|
|
updateAt = self.updateAt
|
|
|
|
|
|
|
|
if self.best.justified.epoch > self.current.justified.epoch:
|
|
|
|
let current_epoch = current_slot.compute_epoch_at_slot()
|
|
|
|
|
|
|
|
if self.update_at.isNone():
|
|
|
|
if self.best.justified.epoch > self.current.justified.epoch:
|
|
|
|
if compute_slots_since_epoch_start(current_slot) < SAFE_SLOTS_TO_UPDATE_JUSTIFIED:
|
|
|
|
self.current = self.best
|
|
|
|
else:
|
|
|
|
self.update_at = some(current_epoch + 1)
|
|
|
|
elif self.updateAt.get() <= current_epoch:
|
|
|
|
self.current = self.best
|
|
|
|
self.update_at = none(Epoch)
|
|
|
|
|
|
|
|
proc process_block*(self: var ForkChoiceBackend,
|
|
|
|
block_root: Eth2Digest,
|
|
|
|
parent_root: Eth2Digest,
|
|
|
|
justified_epoch: Epoch,
|
2020-07-30 15:48:25 +00:00
|
|
|
finalized_epoch: Epoch): FcResult[void] =
|
|
|
|
self.proto_array.on_block(
|
2020-07-25 19:41:12 +00:00
|
|
|
block_root, hasParentInForkChoice = true, parent_root,
|
|
|
|
justified_epoch, finalized_epoch)
|
|
|
|
|
|
|
|
proc process_block*(self: var ForkChoice,
|
2020-07-31 14:49:06 +00:00
|
|
|
dag: ChainDAGRef,
|
2020-08-03 18:39:43 +00:00
|
|
|
epochRef: EpochRef,
|
2020-07-25 19:41:12 +00:00
|
|
|
blckRef: BlockRef,
|
|
|
|
blck: SomeBeaconBlock,
|
2020-07-30 15:48:25 +00:00
|
|
|
wallSlot: Slot): FcResult[void] =
|
2020-08-03 18:39:43 +00:00
|
|
|
process_state(self.checkpoints, dag, epochRef, blckRef)
|
2020-07-30 15:48:25 +00:00
|
|
|
|
2020-07-30 19:18:17 +00:00
|
|
|
maybe_update(self.checkpoints, wallSlot)
|
2020-07-25 19:41:12 +00:00
|
|
|
|
|
|
|
for attestation in blck.body.attestations:
|
2020-07-30 19:18:17 +00:00
|
|
|
let targetBlck = dag.getRef(attestation.data.target.root)
|
2020-07-28 13:54:32 +00:00
|
|
|
if targetBlck.isNil:
|
|
|
|
continue
|
2020-07-25 19:41:12 +00:00
|
|
|
if attestation.data.beacon_block_root in self.backend:
|
2020-07-30 15:48:25 +00:00
|
|
|
let
|
|
|
|
epochRef =
|
2020-07-30 19:18:17 +00:00
|
|
|
dag.getEpochRef(targetBlck, attestation.data.target.epoch)
|
2020-07-30 15:48:25 +00:00
|
|
|
participants = get_attesting_indices(
|
|
|
|
epochRef, attestation.data, attestation.aggregation_bits)
|
2020-07-25 19:41:12 +00:00
|
|
|
|
|
|
|
for validator in participants:
|
|
|
|
self.process_attestation(
|
|
|
|
validator,
|
|
|
|
attestation.data.beacon_block_root,
|
|
|
|
attestation.data.target.epoch)
|
|
|
|
|
|
|
|
? process_block(
|
|
|
|
self.backend, blckRef.root, blck.parent_root,
|
2020-08-03 18:39:43 +00:00
|
|
|
epochRef.current_justified_checkpoint.epoch, epochRef.finalized_checkpoint.epoch
|
2020-07-25 19:41:12 +00:00
|
|
|
)
|
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
trace "Integrating block in fork choice",
|
|
|
|
block_root = shortLog(blckRef)
|
2020-06-10 06:58:12 +00:00
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
ok()
|
2020-04-09 16:15:00 +00:00
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
proc find_head*(
|
2020-07-25 19:41:12 +00:00
|
|
|
self: var ForkChoiceBackend,
|
2020-04-09 16:15:00 +00:00
|
|
|
justified_epoch: Epoch,
|
|
|
|
justified_root: Eth2Digest,
|
|
|
|
finalized_epoch: Epoch,
|
|
|
|
justified_state_balances: seq[Gwei]
|
2020-07-30 15:48:25 +00:00
|
|
|
): FcResult[Eth2Digest] =
|
2020-04-09 16:15:00 +00:00
|
|
|
## 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)
|
2020-07-30 15:48:25 +00:00
|
|
|
? deltas.compute_deltas(
|
2020-04-09 16:15:00 +00:00
|
|
|
indices = self.proto_array.indices,
|
|
|
|
votes = self.votes,
|
|
|
|
old_balances = self.balances,
|
|
|
|
new_balances = justified_state_balances
|
|
|
|
)
|
|
|
|
|
|
|
|
# Apply score changes
|
2020-07-30 15:48:25 +00:00
|
|
|
? self.proto_array.apply_score_changes(
|
2020-04-09 16:15:00 +00:00
|
|
|
deltas, justified_epoch, finalized_epoch
|
|
|
|
)
|
|
|
|
|
|
|
|
self.balances = justified_state_balances
|
|
|
|
|
|
|
|
# Find the best block
|
|
|
|
var new_head{.noInit.}: Eth2Digest
|
2020-07-30 15:48:25 +00:00
|
|
|
? self.proto_array.find_head(new_head, justified_root)
|
2020-04-09 16:15:00 +00:00
|
|
|
|
2020-06-10 06:58:12 +00:00
|
|
|
{.noSideEffect.}:
|
2020-07-09 09:29:32 +00:00
|
|
|
debug "Fork choice requested",
|
2020-07-22 09:42:55 +00:00
|
|
|
justified_epoch = justified_epoch,
|
2020-07-25 19:41:12 +00:00
|
|
|
justified_root = shortLog(justified_root),
|
2020-07-22 09:42:55 +00:00
|
|
|
finalized_epoch = finalized_epoch,
|
2020-07-25 19:41:12 +00:00
|
|
|
fork_choice_head = shortLog(new_head)
|
2020-06-10 06:58:12 +00:00
|
|
|
|
2020-04-09 16:15:00 +00:00
|
|
|
return ok(new_head)
|
|
|
|
|
2020-07-25 19:41:12 +00:00
|
|
|
proc find_head*(self: var ForkChoice,
|
2020-07-30 19:18:17 +00:00
|
|
|
wallSlot: Slot): FcResult[Eth2Digest] =
|
2020-07-25 19:41:12 +00:00
|
|
|
template remove_alias(blck_root: Eth2Digest): Eth2Digest =
|
|
|
|
if blck_root == Eth2Digest():
|
|
|
|
self.finalizedBlock.root
|
|
|
|
else:
|
|
|
|
blck_root
|
|
|
|
|
2020-07-30 19:18:17 +00:00
|
|
|
self.checkpoints.maybe_update(wallSlot)
|
2020-07-25 19:41:12 +00:00
|
|
|
|
|
|
|
self.backend.find_head(
|
|
|
|
self.checkpoints.current.justified.epoch,
|
|
|
|
remove_alias(self.checkpoints.current.justified.blck.root),
|
|
|
|
self.checkpoints.current.finalized.epoch,
|
|
|
|
self.checkpoints.current.justified.balances,
|
|
|
|
)
|
2020-04-09 16:15:00 +00:00
|
|
|
|
|
|
|
func maybe_prune*(
|
2020-07-25 19:41:12 +00:00
|
|
|
self: var ForkChoiceBackend, finalized_root: Eth2Digest
|
2020-07-30 15:48:25 +00:00
|
|
|
): FcResult[void] =
|
2020-04-09 16:15:00 +00:00
|
|
|
## Prune blocks preceding the finalized root as they are now unneeded.
|
2020-07-30 15:48:25 +00:00
|
|
|
self.proto_array.maybe_prune(finalized_root)
|
2020-04-09 16:15:00 +00:00
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
func prune*(self: var ForkChoice): FcResult[void] =
|
2020-07-25 19:41:12 +00:00
|
|
|
let finalized_root = self.checkpoints.current.finalized.root
|
|
|
|
self.backend.maybe_prune(finalized_root)
|
|
|
|
|
2020-04-09 16:15:00 +00:00
|
|
|
func compute_deltas(
|
|
|
|
deltas: var openarray[Delta],
|
|
|
|
indices: Table[Eth2Digest, Index],
|
|
|
|
votes: var openArray[VoteTracker],
|
|
|
|
old_balances: openarray[Gwei],
|
|
|
|
new_balances: openarray[Gwei]
|
2020-07-30 15:48:25 +00:00
|
|
|
): FcResult[void] =
|
2020-04-09 16:15:00 +00:00
|
|
|
## 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:
|
2020-07-30 15:48:25 +00:00
|
|
|
return err ForkChoiceError(
|
|
|
|
kind: fcInvalidNodeDelta,
|
2020-04-09 16:15:00 +00:00
|
|
|
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:
|
2020-07-30 15:48:25 +00:00
|
|
|
return err ForkChoiceError(
|
|
|
|
kind: fcInvalidNodeDelta,
|
2020-04-09 16:15:00 +00:00
|
|
|
index: index
|
|
|
|
)
|
|
|
|
deltas[index] += Delta new_balance
|
|
|
|
# Note that delta can be negative
|
|
|
|
# TODO: is int64 big enough?
|
|
|
|
|
|
|
|
vote.current_root = vote.next_root
|
2020-07-30 15:48:25 +00:00
|
|
|
return ok()
|
2020-04-09 16:15:00 +00:00
|
|
|
|
|
|
|
# Sanity checks
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
# Sanity checks on internal private procedures
|
|
|
|
|
|
|
|
when isMainModule:
|
|
|
|
import stew/endians2
|
|
|
|
|
2020-06-10 06:58:12 +00:00
|
|
|
func fakeHash(index: SomeInteger): Eth2Digest =
|
2020-04-09 16:15:00 +00:00
|
|
|
## 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
|
|
|
|
)
|
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
doAssert err.isOk, "compute_deltas finished with error: " & $err
|
2020-04-09 16:15:00 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
doAssert err.isOk, "compute_deltas finished with error: " & $err
|
2020-04-09 16:15:00 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
doAssert err.isOk, "compute_deltas finished with error: " & $err
|
2020-04-09 16:15:00 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
doAssert err.isOk, "compute_deltas finished with error: " & $err
|
2020-04-09 16:15:00 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
doAssert err.isOk, "compute_deltas finished with error: " & $err
|
2020-04-09 16:15:00 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
doAssert err.isOk, "compute_deltas finished with error: " & $err
|
2020-04-09 16:15:00 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
doAssert err.isOk, "compute_deltas finished with error: " & $err
|
2020-04-09 16:15:00 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
2020-07-30 15:48:25 +00:00
|
|
|
doAssert err.isOk, "compute_deltas finished with error: " & $err
|
2020-04-09 16:15:00 +00:00
|
|
|
|
|
|
|
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()
|