spec cleanups (#1379)

* add helper for beacon committee length (used for quickly validating
attestations)
* refactor some attestation checks to do cheap checks early
* validate attestation epoch before computing active validator set
* clean up documentation / comments
* fill state cache on demand
This commit is contained in:
Jacek Sieka 2020-07-27 18:04:44 +02:00 committed by GitHub
parent 99dcb81e77
commit 48cebc7157
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 309 additions and 428 deletions

View File

@ -22,13 +22,14 @@ logScope:
func is_aggregator(state: BeaconState, slot: Slot, index: CommitteeIndex,
slot_signature: ValidatorSig, cache: var StateCache): bool =
let
committee = get_beacon_committee(state, slot, index, cache)
modulo = max(1'u64, len(committee).uint64 div TARGET_AGGREGATORS_PER_COMMITTEE)
committee_len = get_beacon_committee_len(state, slot, index, cache)
modulo = max(1'u64, committee_len div TARGET_AGGREGATORS_PER_COMMITTEE)
bytes_to_uint64(eth2digest(slot_signature.toRaw()).data[0..7]) mod modulo == 0
proc aggregate_attestations*(
pool: AttestationPool, state: BeaconState, index: CommitteeIndex,
privkey: ValidatorPrivKey, trailing_distance: uint64): Option[AggregateAndProof] =
privkey: ValidatorPrivKey, trailing_distance: uint64,
cache: var StateCache): Option[AggregateAndProof] =
doAssert state.slot >= trailing_distance
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/p2p-interface.md#configuration
@ -42,8 +43,6 @@ proc aggregate_attestations*(
doAssert slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= state.slot
doAssert state.slot >= slot
var cache = StateCache()
# TODO performance issue for future, via get_active_validator_indices(...)
doAssert index.uint64 < get_committee_count_per_slot(state, slot.epoch, cache)
# TODO for testing purposes, refactor this into the condition check
@ -99,7 +98,12 @@ proc isValidAttestationSlot(
true
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#attestation-subnets
func checkPropagationSlotRange(data: AttestationData, current_slot: Slot): bool =
# TODO clock disparity
(data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot) and
(current_slot >= data.slot)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/p2p-interface.md#attestation-subnets
proc isValidAttestation*(
pool: var AttestationPool, attestation: Attestation, current_slot: Slot,
topicCommitteeIndex: uint64): bool =
@ -107,8 +111,12 @@ proc isValidAttestation*(
topics = "att_aggr valid_att"
received_attestation = shortLog(attestation)
if not (attestation.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >=
current_slot and current_slot >= attestation.data.slot):
# TODO https://github.com/ethereum/eth2.0-specs/issues/1998
if (let v = check_attestation_slot_target(attestation.data); v.isErr):
debug "Invalid attestation", err = v.error
return false
if not checkPropagationSlotRange(attestation.data, current_slot):
debug "attestation.data.slot not within ATTESTATION_PROPAGATION_SLOT_RANGE"
return false
@ -202,6 +210,11 @@ proc isValidAggregatedAttestation*(
logScope:
aggregate = shortLog(aggregate)
# TODO https://github.com/ethereum/eth2.0-specs/issues/1998
if (let v = check_attestation_slot_target(aggregate.data); v.isErr):
debug "Invalid aggregate", err = v.error
return false
# There's some overlap between this and isValidAttestation(), but unclear if
# saving a few lines of code would balance well with losing straightforward,
# spec-based synchronization.
@ -210,8 +223,7 @@ proc isValidAggregatedAttestation*(
# ATTESTATION_PROPAGATION_SLOT_RANGE slots (with a
# MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- i.e. aggregate.data.slot +
# ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= aggregate.data.slot
if not (aggregate.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >=
current_slot and current_slot >= aggregate.data.slot):
if not checkPropagationSlotRange(aggregate.data, current_slot):
debug "aggregation.data.slot not within ATTESTATION_PROPAGATION_SLOT_RANGE"
return false

View File

@ -14,9 +14,11 @@ import
chronicles, stew/[byteutils], json_serialization/std/sets,
# Internal
./spec/[beaconstate, datatypes, crypto, digest, helpers],
./extras, ./block_pool, ./block_pools/candidate_chains, ./beacon_node_types,
./block_pool, ./block_pools/candidate_chains, ./beacon_node_types,
./fork_choice/fork_choice
export beacon_node_types
logScope: topics = "attpool"
proc init*(T: type AttestationPool, blockPool: BlockPool): T =
@ -32,7 +34,10 @@ proc init*(T: type AttestationPool, blockPool: BlockPool): T =
blockPool.tmpState,
).get()
# Feed fork choice with unfinalized history
# Feed fork choice with unfinalized history - during startup, block pool only
# keeps track of a single history so we just need to follow it
doAssert blockPool.heads.len == 1, "Init only supports a single history"
var blocks: seq[BlockRef]
var cur = blockPool.head.blck
while cur != blockPool.finalizedHead.blck:
@ -66,25 +71,6 @@ proc init*(T: type AttestationPool, blockPool: BlockPool): T =
forkChoice: forkChoice
)
proc combine*(tgt: var Attestation, src: Attestation, flags: UpdateFlags) =
## Combine the signature and participation bitfield, with the assumption that
## the same data is being signed - if the signatures overlap, they are not
## combined.
# TODO: Exported only for testing, all usage are internals
doAssert tgt.data == src.data
# In a BLS aggregate signature, one needs to count how many times a
# particular public key has been added - since we use a single bit per key, we
# can only it once, thus we can never combine signatures that overlap already!
if not tgt.aggregation_bits.overlaps(src.aggregation_bits):
tgt.aggregation_bits.combine(src.aggregation_bits)
if skipBlsValidation notin flags:
tgt.signature.aggregate(src.signature)
else:
trace "Ignoring overlapping attestations"
proc slotIndex(
pool: var AttestationPool, state: BeaconState, attestationSlot: Slot): int =
## Grow and garbage collect pool, returning the deque index of the slot
@ -139,10 +125,10 @@ proc slotIndex(
int(attestationSlot - pool.startingSlot)
func processAttestation(
pool: var AttestationPool, state: BeaconState,
participants: seq[ValidatorIndex], block_root: Eth2Digest, target_epoch: Epoch) =
pool: var AttestationPool, participants: HashSet[ValidatorIndex],
block_root: Eth2Digest, target_epoch: Epoch) =
# Add attestation votes to fork choice
for validator in participants:
# ForkChoice v2
pool.forkChoice.process_attestation(validator, block_root, target_epoch)
func addUnresolved(pool: var AttestationPool, attestation: Attestation) =
@ -152,6 +138,9 @@ func addUnresolved(pool: var AttestationPool, attestation: Attestation) =
)
proc addResolved(pool: var AttestationPool, blck: BlockRef, attestation: Attestation) =
logScope:
attestation = shortLog(attestation)
doAssert blck.root == attestation.data.beacon_block_root
# TODO Which state should we use to validate the attestation? It seems
@ -163,7 +152,6 @@ proc addResolved(pool: var AttestationPool, blck: BlockRef, attestation: Attesta
# we should use isValidAttestationSlot instead
if blck.slot > attestation.data.slot:
notice "Invalid attestation (too new!)",
attestation = shortLog(attestation),
blockSlot = shortLog(blck.slot)
return
@ -172,7 +160,6 @@ proc addResolved(pool: var AttestationPool, blck: BlockRef, attestation: Attesta
# though they no longer are relevant for finalization - let's clear
# these out
debug "Old attestation",
attestation = shortLog(attestation),
startingSlot = pool.startingSlot
return
@ -181,14 +168,8 @@ proc addResolved(pool: var AttestationPool, blck: BlockRef, attestation: Attesta
# return
# Check that the attestation is indeed valid
# TODO: we might want to split checks that depend
# on the state and those that don't to cheaply
# discard invalid attestations before rewinding state.
if not isValidAttestationTargetEpoch(
attestation.data.target.epoch, attestation.data):
notice "Invalid attestation",
attestation = shortLog(attestation),
current_epoch = attestation.data.slot.compute_epoch_at_slot
if (let v = check_attestation_slot_target(attestation.data); v.isErr):
debug "Invalid attestation", err = v.error
return
# Get a temporary state at the (block, slot) targeted by the attestation
@ -209,8 +190,8 @@ proc addResolved(pool: var AttestationPool, blck: BlockRef, attestation: Attesta
validation = Validation(
aggregation_bits: attestation.aggregation_bits,
aggregate_signature: attestation.signature)
participants = toSeq(items(get_attesting_indices(
state, attestation.data, validation.aggregation_bits, cache)))
participants = get_attesting_indices(
state, attestation.data, validation.aggregation_bits, cache)
var found = false
for a in attestationsSeen.attestations.mitems():
@ -240,7 +221,7 @@ proc addResolved(pool: var AttestationPool, blck: BlockRef, attestation: Attesta
a.validations.add(validation)
pool.processAttestation(
state, participants, a.blck.root, attestation.data.target.epoch)
participants, a.blck.root, attestation.data.target.epoch)
info "Attestation resolved",
attestation = shortLog(attestation),
@ -259,7 +240,7 @@ proc addResolved(pool: var AttestationPool, blck: BlockRef, attestation: Attesta
validations: @[validation]
))
pool.processAttestation(
state, participants, blck.root, attestation.data.target.epoch)
participants, blck.root, attestation.data.target.epoch)
info "Attestation resolved",
attestation = shortLog(attestation),
@ -383,9 +364,11 @@ proc getAttestationsForBlock*(pool: AttestationPool,
# state should they be validated, if at all?
# TODO we're checking signatures here every time which is very slow and we don't want
# to include a broken attestation
if not check_attestation(state, attestation, {}, cache):
if (let v = check_attestation(state, attestation, {}, cache); v.isErr):
warn "Attestation no longer validates...",
attestation = shortLog(attestation)
attestation = shortLog(attestation),
err = v.error
continue
for v in a.validations[1..^1]:

View File

@ -17,28 +17,18 @@ type
#
# #############################################
Validation* = object
## Validations collect a set of signatures for a distict attestation - in
## eth2, a single bit is used to keep track of which signatures have been
## added to the aggregate meaning that only non-overlapping aggregates may
## be further combined.
aggregation_bits*: CommitteeValidatorsBits
aggregate_signature*: ValidatorSig
# Per Danny as of 2018-12-21:
# Yeah, you can do any linear combination of signatures. but you have to
# remember the linear combination of pubkeys that constructed
# if you have two instances of a signature from pubkey p, then you need 2*p
# in the group pubkey because the attestation bitlist is only 1 bit per
# pubkey right now, attestations do not support this it could be extended to
# support N overlaps up to N times per pubkey if we had N bits per validator
# instead of 1
# We are shying away from this for the time being. If there end up being
# substantial difficulties in network layer aggregation, then adding bits to
# aid in supporting overlaps is one potential solution
AttestationEntry* = object
## Each entry holds the known signatures for a particular, distinct vote
data*: AttestationData
blck*: BlockRef
validations*: seq[Validation] ## \
## Instead of aggregating the signatures eagerly, we simply dump them in
## this seq and aggregate only when needed
## TODO there are obvious caching opportunities here..
validations*: seq[Validation]
AttestationsSeen* = object
attestations*: seq[AttestationEntry] ## \
@ -54,11 +44,11 @@ type
tries*: int
AttestationPool* = object
## The attestation pool keeps all attestations that are known to the
## client - each attestation counts as votes towards the fork choice
## rule that determines which block we consider to be the head. The pool
## contains both votes that have been included in the chain and those that
## have not.
## The attestation pool keeps track of all attestations that potentially
## could be added to a block during block production.
## These attestations also contribute to the fork choice, which combines
## "free" attestations with those found in past blocks - these votes
## are tracked separately in the fork choice.
mapSlotsToAttestations*: Deque[AttestationsSeen] ## \
## We keep one item per slot such that indexing matches slot number

View File

@ -8,7 +8,7 @@
import
extras, beacon_chain_db,
stew/results,
spec/[crypto, datatypes, digest, helpers, presets]
spec/[crypto, datatypes, digest, presets, validator]
import

View File

@ -1,8 +1,8 @@
import
deques, tables, hashes, options, strformat,
std/[deques, tables, hashes, options, strformat],
chronos, web3, web3/ethtypes as web3Types, json, chronicles,
eth/common/eth_types, eth/async_utils,
spec/[datatypes, digest, crypto, beaconstate, helpers],
spec/[datatypes, digest, crypto, beaconstate, helpers, validator],
network_metadata, merkle_minimal
from times import epochTime
@ -506,7 +506,8 @@ proc createBeaconStateAux(preset: RuntimePreset,
eth1Block.voteData.block_hash,
eth1Block.timestamp.uint64,
deposits, {})
let activeValidators = count_active_validators(result[], GENESIS_EPOCH, StateCache())
var cache = StateCache()
let activeValidators = count_active_validators(result[], GENESIS_EPOCH, cache)
eth1Block.knownGoodDepositsCount = some activeValidators
proc createBeaconState(m: MainchainMonitor, eth1Block: Eth1Block): BeaconStateRef =

View File

@ -513,185 +513,116 @@ func get_indexed_attestation*(state: BeaconState, attestation: TrustedAttestatio
# Attestation validation
# ------------------------------------------------------------------------------------------
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#attestations
#
# TODO: Refactor, we need:
# - cheap validations that do not involve the fork state (to avoid unnecessary and costly rewinding which are probably a DOS vector)
# - check that must be done on the targeted state (for example beacon committee consistency)
# - check that requires easily computed data from the targeted sate (slots, epoch, justified checkpoints)
proc isValidAttestationSlot*(attestationSlot, stateSlot: Slot): bool =
## Attestation check that does not depend on the whole block state
## for cheaper prefiltering of attestations that come from the network
## to limit DOS via state rewinding
if not (attestationSlot + MIN_ATTESTATION_INCLUSION_DELAY <= stateSlot):
warn("Attestation too new",
attestation_slot = shortLog(attestationSlot),
state_slot = shortLog(stateSlot))
return false
if not (stateSlot <= attestationSlot + SLOTS_PER_EPOCH):
warn("Attestation too old",
attestation_slot = shortLog(attestationSlot),
state_slot = shortLog(stateSlot))
return false
true
# TODO remove/merge with p2p-interface validation
proc isValidAttestationTargetEpoch*(
state_epoch: Epoch, data: AttestationData): bool =
# TODO what constitutes a valid attestation when it's about to be added to
# the pool? we're interested in attestations that will become viable
# for inclusion in blocks in the future and on any fork, so we need to
# consider that validations might happen using the state of a different
# fork.
# Some things are always invalid (like out-of-bounds issues etc), but
# others are more subtle - how do we validate the signature for example?
# It might be valid on one fork but not another. One thing that helps
# is that committees are stable per epoch and that it should be OK to
# include an attestation in a block even if the corresponding validator
# was slashed in the same epoch - there's no penalty for doing this and
# the vote counting logic will take care of any ill effects (TODO verify)
# Without this check, we can't get a slot number for the attestation as
# certain helpers will assert
# TODO this could probably be avoided by being smart about the specific state
# used to validate the attestation: most likely if we pick the state of
# the beacon block being voted for and a slot in the target epoch
# of the attestation, we'll be safe!
# TODO the above state selection logic should probably live here in the
# attestation pool
if not (data.target.epoch == get_previous_epoch(state_epoch) or
data.target.epoch == state_epoch):
warn("Target epoch not current or previous epoch")
return
if not (data.target.epoch == compute_epoch_at_slot(data.slot)):
warn("Target epoch inconsistent with epoch of data slot",
target_epoch = data.target.epoch,
data_slot_epoch = compute_epoch_at_slot(data.slot))
return
true
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#attestations
func check_attestation_slot_target*(data: AttestationData): Result[void, cstring] =
if not (data.target.epoch == compute_epoch_at_slot(data.slot)):
return err("Target epoch doesn't match attestation slot")
ok()
func check_attestation_target_epoch*(
data: AttestationData, current_epoch: Epoch): Result[void, cstring] =
if not (data.target.epoch == get_previous_epoch(current_epoch) or
data.target.epoch == current_epoch):
return err("Target epoch not current or previous epoch")
ok()
func check_attestation_inclusion*(data: AttestationData,
current_slot: Slot): Result[void, cstring] =
if not (data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= current_slot):
return err("Attestation too new")
if not (current_slot <= data.slot + SLOTS_PER_EPOCH):
return err("Attestation too old")
ok()
func check_attestation_index*(
data: AttestationData, committees_per_slot: uint64): Result[void, cstring] =
if not (data.index < committees_per_slot):
return err("Data index exceeds committee count")
ok()
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/beacon-chain.md#attestations
proc check_attestation*(
state: BeaconState, attestation: SomeAttestation, flags: UpdateFlags,
stateCache: var StateCache): bool =
cache: var StateCache): Result[void, cstring] =
## Check that an attestation follows the rules of being included in the state
## at the current slot. When acting as a proposer, the same rules need to
## be followed!
# TODO: When checking attestation from the network, currently rewinding/advancing
# a temporary state is needed.
# If this is too costly we can be DOS-ed.
# We might want to split this into "state-dependent" checks and "state-independent" checks
# The latter one should be made prior to state-rewinding.
let
stateSlot = state.slot
data = attestation.data
logScope:
attestation = shortLog(attestation)
trace "process_attestation: beginning"
? check_attestation_target_epoch(data, state.get_current_epoch())
? check_attestation_slot_target(data)
? check_attestation_inclusion(data, state.slot)
? check_attestation_index(
data,
get_committee_count_per_slot(state, data.target.epoch, cache))
if not isValidAttestationTargetEpoch(state.get_current_epoch(), data):
# Logging in isValidAttestationTargetEpoch
return
let committee_len = get_beacon_committee_len(
state, data.slot, data.index.CommitteeIndex, cache)
if not isValidAttestationSlot(data.slot, stateSlot):
# Logging in isValidAttestationSlot
return
let committees_per_slot =
get_committee_count_per_slot(
state, data.target.epoch, stateCache)
if not (data.index < committees_per_slot):
warn "Data index exceeds committee count",
committee_count = committees_per_slot
return
if not (data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot):
warn "Attestation too new"
return
if not (state.slot <= data.slot + SLOTS_PER_EPOCH):
warn "Attestation too old"
return
let committee = get_beacon_committee(
state, data.slot, data.index.CommitteeIndex, stateCache)
if attestation.aggregation_bits.len != committee.len:
warn "Inconsistent aggregation and committee length",
aggregation_bits_len = attestation.aggregation_bits.len,
committee_len = committee.len
return
let ffg_check_data = (data.source.epoch, data.source.root, data.target.epoch)
if attestation.aggregation_bits.lenu64 != committee_len:
return err("Inconsistent aggregation and committee length")
if data.target.epoch == get_current_epoch(state):
if not (ffg_check_data == (state.current_justified_checkpoint.epoch,
state.current_justified_checkpoint.root, get_current_epoch(state))):
warn "FFG data not matching current justified epoch"
return
if not (data.source == state.current_justified_checkpoint):
return err("FFG data not matching current justified epoch")
else:
if not (ffg_check_data == (state.previous_justified_checkpoint.epoch,
state.previous_justified_checkpoint.root, get_previous_epoch(state))):
warn "FFG data not matching previous justified epoch"
return
if not (data.source == state.previous_justified_checkpoint):
return err("FFG data not matching previous justified epoch")
if not is_valid_indexed_attestation(
state, get_indexed_attestation(state, attestation, stateCache), flags):
warn "process_attestation: signature or bitfields incorrect"
return
state, get_indexed_attestation(state, attestation, cache), flags):
return err("signature or bitfields incorrect")
true
ok()
proc process_attestation*(
state: var BeaconState, attestation: SomeAttestation, flags: UpdateFlags,
stateCache: var StateCache): Result[void, cstring] {.nbench.} =
cache: var StateCache): Result[void, cstring] {.nbench.} =
# In the spec, attestation validation is mixed with state mutation, so here
# we've split it into two functions so that the validation logic can be
# reused when looking for suitable blocks to include in attestations.
# TODO don't log warnings when looking for attestations (return
# Result[void, cstring] instead of logging in check_attestation?)
let proposer_index = get_beacon_proposer_index(state, stateCache)
let proposer_index = get_beacon_proposer_index(state, cache)
if proposer_index.isNone:
return err("process_attestation: no beacon proposer index and probably no active validators")
if check_attestation(state, attestation, flags, stateCache):
let
attestation_slot = attestation.data.slot
pending_attestation = PendingAttestation(
data: attestation.data,
aggregation_bits: attestation.aggregation_bits,
inclusion_delay: state.slot - attestation_slot,
proposer_index: proposer_index.get.uint64,
)
? check_attestation(state, attestation, flags, cache)
if attestation.data.target.epoch == get_current_epoch(state):
trace "process_attestation: current_epoch_attestations.add",
attestation = shortLog(attestation),
pending_attestation = pending_attestation,
indices = get_attesting_indices(
state, attestation.data, attestation.aggregation_bits, stateCache).len
state.current_epoch_attestations.add(pending_attestation)
else:
trace "process_attestation: previous_epoch_attestations.add",
attestation = shortLog(attestation),
pending_attestation = pending_attestation,
indices = get_attesting_indices(
state, attestation.data, attestation.aggregation_bits, stateCache).len
state.previous_epoch_attestations.add(pending_attestation)
let
attestation_slot = attestation.data.slot
pending_attestation = PendingAttestation(
data: attestation.data,
aggregation_bits: attestation.aggregation_bits,
inclusion_delay: state.slot - attestation_slot,
proposer_index: proposer_index.get.uint64,
)
ok()
if attestation.data.target.epoch == get_current_epoch(state):
trace "process_attestation: current_epoch_attestations.add",
attestation = shortLog(attestation),
pending_attestation = pending_attestation,
indices = get_attesting_indices(
state, attestation.data, attestation.aggregation_bits, cache).len
state.current_epoch_attestations.add(pending_attestation)
else:
err("process_attestation: check_attestation failed")
trace "process_attestation: previous_epoch_attestations.add",
attestation = shortLog(attestation),
pending_attestation = pending_attestation,
indices = get_attesting_indices(
state, attestation.data, attestation.aggregation_bits, cache).len
state.previous_epoch_attestations.add(pending_attestation)
ok()
func makeAttestationData*(
state: BeaconState, slot: Slot, committee_index: uint64,

View File

@ -650,15 +650,19 @@ func shortLog*(v: DepositData): auto =
signature: shortLog(v.signature)
)
func shortLog*(v: Checkpoint): auto =
(
epoch: shortLog(v.epoch),
root: shortLog(v.root),
)
func shortLog*(v: AttestationData): auto =
(
slot: shortLog(v.slot),
index: v.index,
beacon_block_root: shortLog(v.beacon_block_root),
source_epoch: shortLog(v.source.epoch),
source_root: shortLog(v.source.root),
target_epoch: shortLog(v.target.epoch),
target_root: shortLog(v.target.root)
source: shortLog(v.source),
target: shortLog(v.target),
)
func shortLog*(v: SomeAttestation): auto =
@ -673,6 +677,7 @@ chronicles.formatIt Epoch: it.shortLog
chronicles.formatIt BeaconBlock: it.shortLog
chronicles.formatIt AttestationData: it.shortLog
chronicles.formatIt Attestation: it.shortLog
chronicles.formatIt Checkpoint: it.shortLog
import json_serialization
export json_serialization

View File

@ -11,7 +11,7 @@
import
# Standard lib
std/[math, sequtils, tables],
std/[math, tables],
# Third-party
stew/endians2,
# Internal
@ -56,18 +56,6 @@ func is_active_validator*(validator: Validator, epoch: Epoch): bool =
### Check if ``validator`` is active
validator.activation_epoch <= epoch and epoch < validator.exit_epoch
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_active_validator_indices
func count_active_validators*(state: BeaconState,
epoch: Epoch,
cache: StateCache): uint64 =
if epoch in cache.shuffled_active_validator_indices:
try:
cache.shuffled_active_validator_indices[epoch].lenu64
except KeyError:
raiseAssert "just checked"
else:
countIt(state.validators, is_active_validator(it, epoch)).uint64
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_active_validator_indices
func get_active_validator_indices*(state: BeaconState, epoch: Epoch):
seq[ValidatorIndex] =
@ -76,28 +64,6 @@ func get_active_validator_indices*(state: BeaconState, epoch: Epoch):
if is_active_validator(val, epoch):
result.add idx.ValidatorIndex
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_committee_count_per_slot
func get_committee_count_per_slot*(num_active_validators: uint64): uint64 =
clamp(
num_active_validators div SLOTS_PER_EPOCH div TARGET_COMMITTEE_SIZE,
1'u64, MAX_COMMITTEES_PER_SLOT)
func get_committee_count_per_slot*(state: BeaconState,
epoch: Epoch,
cache: StateCache): uint64 =
# Return the number of committees at ``slot``.
# TODO this is mostly used in for loops which have indexes which then need to
# be converted to CommitteeIndex types for get_beacon_committee(...); replace
# with better and more type-safe use pattern, probably beginning with using a
# CommitteeIndex return type here.
let
active_validator_count = count_active_validators(state, epoch, cache)
result = get_committee_count_per_slot(active_validator_count)
# Otherwise, get_beacon_committee(...) cannot access some committees.
doAssert (SLOTS_PER_EPOCH * MAX_COMMITTEES_PER_SLOT) >= uint64(result)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_current_epoch
func get_current_epoch*(state: BeaconState): Epoch =
# Return the current epoch.
@ -123,12 +89,6 @@ func bytes_to_uint64*(data: openarray[byte]): uint64 =
func uint_to_bytes8*(x: uint64): array[8, byte] =
x.toBytesLE()
func int_to_bytes1*(x: int): array[1, byte] =
doAssert x >= 0
doAssert x < 256
result[0] = x.byte
func uint_to_bytes4*(x: uint64): array[4, byte] =
doAssert x < 2'u64^32

View File

@ -8,8 +8,8 @@
{.push raises: [Defect].}
import
strformat,
datatypes, helpers
std/[strformat, sets],
./datatypes, ./helpers, ./validator
const
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/p2p-interface.md#topics-and-messages
@ -92,3 +92,22 @@ func getAttestationTopic*(forkDigest: ForkDigest,
compute_subnet_for_attestation(
get_committee_count_per_slot(num_active_validators),
attestation.data.slot, attestation.data.index.CommitteeIndex))
func get_committee_assignments*(
state: BeaconState, epoch: Epoch,
validator_indices: HashSet[ValidatorIndex]):
seq[tuple[subnetIndex: uint64, slot: Slot]] =
var cache = StateCache()
let
committees_per_slot = get_committee_count_per_slot(state, epoch, cache)
start_slot = compute_start_slot_at_epoch(epoch)
for slot in start_slot ..< start_slot + SLOTS_PER_EPOCH:
for index in 0'u64 ..< committees_per_slot:
let idx = index.CommitteeIndex
if not disjoint(validator_indices,
get_beacon_committee(state, slot, idx, cache).toHashSet):
result.add(
(compute_subnet_for_attestation(committees_per_slot, slot, idx),
slot))

View File

@ -201,25 +201,7 @@ proc state_transition*(
## it is safe to use `noRollback` and leave it broken, else the state
## object should be rolled back to a consistent state. If the transition fails
## before the state has been updated, `rollback` will not be called.
#
# TODO this function can be written with a loop inside to handle all empty
# slots up to the slot of the new_block - but then again, why not eagerly
# update the state as time passes? Something to ponder...
# One reason to keep it this way is that you need to look ahead if you're
# the block proposer, though in reality we only need a partial update for
# that ===> Implemented as process_slots
# TODO There's a discussion about what this function should do, and when:
# https://github.com/ethereum/eth2.0-specs/issues/284
# TODO check to which extent this copy can be avoided (considering forks etc),
# for now, it serves as a reminder that we need to handle invalid blocks
# somewhere..
# many functions will mutate `state` partially without rolling back
# the changes in case of failure (look out for `var BeaconState` and
# bool return values...)
doAssert not rollback.isNil, "use noRollback if it's ok to mess up state"
doAssert stateCache.shuffled_active_validator_indices.hasKey(
state.data.get_current_epoch())
if not process_slots(state, signedBlock.message.slot, flags):
rollback(state)
@ -229,8 +211,6 @@ proc state_transition*(
# by the block proposer. Every actor in the network will update its state
# according to the contents of this block - but first they will validate
# that the block is sane.
# TODO what should happen if block processing fails?
# https://github.com/ethereum/eth2.0-specs/issues/293
if skipBLSValidation in flags or
verify_block_signature(state.data, signedBlock):
@ -265,9 +245,6 @@ proc state_transition*(
# and fuzzing code should always be coming from blockpool which should
# always be providing cache or equivalent
var cache = StateCache()
cache.shuffled_active_validator_indices[state.data.get_current_epoch()] =
get_shuffled_active_validator_indices(
state.data, state.data.get_current_epoch())
state_transition(preset, state, signedBlock, cache, flags, rollback)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#preparing-for-a-beaconblock

View File

@ -61,14 +61,9 @@ func get_total_active_balance*(state: BeaconState, cache: var StateCache): Gwei
let
epoch = state.get_current_epoch()
try:
if epoch notin cache.shuffled_active_validator_indices:
cache.shuffled_active_validator_indices[epoch] =
get_shuffled_active_validator_indices(state, epoch)
get_total_balance(state, cache.shuffled_active_validator_indices[epoch])
except KeyError:
raiseAssert("get_total_active_balance(): cache always filled before usage")
get_total_balance(
state, cache.get_shuffled_active_validator_indices(state, epoch))
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#helper-functions-1
func get_matching_source_attestations(state: BeaconState,

View File

@ -9,17 +9,9 @@
import
# Standard library
sets,
std/sets,
# Internals
./datatypes, ./digest, ./beaconstate
# Logging utilities
# --------------------------------------------------------
# TODO: gather all logging utilities
# from crypto, digest, etc in a single file
func shortLog*(x: Checkpoint): string =
"(epoch: " & $x.epoch & ", root: \"" & shortLog(x.root) & "\")"
./datatypes, ./beaconstate
# Helpers used in epoch transition and trace-level block transition
# --------------------------------------------------------
@ -27,19 +19,19 @@ func shortLog*(x: Checkpoint): string =
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#helper-functions-1
func get_attesting_indices*(
state: BeaconState, attestations: openArray[PendingAttestation],
stateCache: var StateCache): HashSet[ValidatorIndex] =
cache: var StateCache): HashSet[ValidatorIndex] =
# This is part of get_unslashed_attesting_indices(...) in spec.
# Exported bceause of external trace-level chronicles logging.
result = initHashSet[ValidatorIndex]()
for a in attestations:
result.incl get_attesting_indices(
state, a.data, a.aggregation_bits, stateCache)
state, a.data, a.aggregation_bits, cache)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#helper-functions-1
func get_unslashed_attesting_indices*(
state: BeaconState, attestations: openArray[PendingAttestation],
stateCache: var StateCache): HashSet[ValidatorIndex] =
result = get_attesting_indices(state, attestations, stateCache)
cache: var StateCache): HashSet[ValidatorIndex] =
result = get_attesting_indices(state, attestations, cache)
for index in result:
if state.validators[index].slashed:
result.excl index

View File

@ -9,8 +9,8 @@
{.push raises: [Defect].}
import
algorithm, options, sequtils, math, tables, sets,
./datatypes, ./digest, ./helpers, ./network
algorithm, options, sequtils, math, tables,
./datatypes, ./digest, ./helpers
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#compute_shuffled_index
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#compute_committee
@ -46,10 +46,10 @@ func get_shuffled_seq*(seed: Eth2Digest,
pivot_buffer[0..31] = seed.data
source_buffer[0..31] = seed.data
for round in 0 ..< SHUFFLE_ROUND_COUNT.int:
let round_bytes1 = int_to_bytes1(round)[0]
pivot_buffer[32] = round_bytes1
source_buffer[32] = round_bytes1
static: doAssert SHUFFLE_ROUND_COUNT < uint8.high
for round in 0'u8 ..< SHUFFLE_ROUND_COUNT.uint8:
pivot_buffer[32] = round
source_buffer[32] = round
# Only one pivot per round.
let pivot =
@ -93,14 +93,48 @@ func get_shuffled_active_validator_indices*(state: BeaconState, epoch: Epoch):
active_validator_indices[it])
func get_shuffled_active_validator_indices*(
state: BeaconState, epoch: Epoch, cache: var StateCache):
seq[ValidatorIndex] =
try:
cache.shuffled_active_validator_indices[epoch]
except KeyError:
let validator_indices = get_shuffled_active_validator_indices(state, epoch)
cache.shuffled_active_validator_indices[epoch] = validator_indices
validator_indices
cache: var StateCache, state: BeaconState, epoch: Epoch):
var seq[ValidatorIndex] =
# `cache` comes first because of nim's borrowing rules for the `var` return -
# the `var` returns avoids copying the validator set.
cache.shuffled_active_validator_indices.withValue(epoch, validator_indices) do:
return validator_indices[]
do:
let indices = get_shuffled_active_validator_indices(state, epoch)
return cache.shuffled_active_validator_indices.mgetOrPut(epoch, indices)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_active_validator_indices
func count_active_validators*(state: BeaconState,
epoch: Epoch,
cache: var StateCache): uint64 =
cache.get_shuffled_active_validator_indices(state, epoch).lenu64
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_committee_count_per_slot
func get_committee_count_per_slot*(num_active_validators: uint64): uint64 =
clamp(
num_active_validators div SLOTS_PER_EPOCH div TARGET_COMMITTEE_SIZE,
1'u64, MAX_COMMITTEES_PER_SLOT)
func get_committee_count_per_slot*(state: BeaconState,
epoch: Epoch,
cache: var StateCache): uint64 =
# Return the number of committees at ``slot``.
# TODO this is mostly used in for loops which have indexes which then need to
# be converted to CommitteeIndex types for get_beacon_committee(...); replace
# with better and more type-safe use pattern, probably beginning with using a
# CommitteeIndex return type here.
let
active_validator_count = count_active_validators(state, epoch, cache)
result = get_committee_count_per_slot(active_validator_count)
# Otherwise, get_beacon_committee(...) cannot access some committees.
doAssert (SLOTS_PER_EPOCH * MAX_COMMITTEES_PER_SLOT) >= uint64(result)
func get_committee_count_per_slot*(state: BeaconState,
slot: Slot,
cache: var StateCache): uint64 =
get_committee_count_per_slot(state, slot.compute_epoch_at_slot, cache)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_previous_epoch
func get_previous_epoch*(current_epoch: Epoch): Epoch =
@ -115,28 +149,47 @@ func get_previous_epoch*(state: BeaconState): Epoch =
get_previous_epoch(get_current_epoch(state))
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#compute_committee
func compute_committee(indices: seq[ValidatorIndex], seed: Eth2Digest,
func compute_committee(shuffled_indices: seq[ValidatorIndex],
index: uint64, count: uint64): seq[ValidatorIndex] =
## Return the committee corresponding to ``indices``, ``seed``, ``index``,
## and committee ``count``.
## In this version, we pass in the shuffled indices meaning we no longer need
## the seed.
let
active_validators = shuffled_indices.len.uint64
start = (active_validators * index) div count
endIdx = (active_validators * (index + 1)) div count
# These assertions from compute_shuffled_index(...)
doAssert endIdx <= active_validators
doAssert active_validators <= 2'u64^40
# In spec, this calls get_shuffled_index() every time, but that's wasteful
# Here, get_beacon_committee() gets the shuffled version.
try:
shuffled_indices[start.int .. (endIdx.int-1)]
except KeyError:
raiseAssert("Cached entries are added before use")
func compute_committee_len(active_validators: uint64,
index: uint64, count: uint64): uint64 =
## Return the committee corresponding to ``indices``, ``seed``, ``index``,
## and committee ``count``.
# indices only used here for its length, or for the shuffled version,
# so unlike spec, pass the shuffled version in directly.
let
start = (len(indices).uint64 * index) div count
endIdx = (len(indices).uint64 * (index + 1)) div count
start = (active_validators * index) div count
endIdx = (active_validators * (index + 1)) div count
# These assertions from compute_shuffled_index(...)
let index_count = indices.len().uint64
doAssert endIdx <= index_count
doAssert index_count <= 2'u64^40
doAssert endIdx <= active_validators
doAssert active_validators <= 2'u64^40
# In spec, this calls get_shuffled_index() every time, but that's wasteful
# Here, get_beacon_committee() gets the shuffled version.
try:
indices[start.int .. (endIdx.int-1)]
except KeyError:
raiseAssert("Cached entries are added before use")
endIdx - start
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_beacon_committee
func get_beacon_committee*(
@ -145,28 +198,28 @@ func get_beacon_committee*(
# Return the beacon committee at ``slot`` for ``index``.
let
epoch = compute_epoch_at_slot(slot)
committees_per_slot = get_committee_count_per_slot(state, epoch, cache)
compute_committee(
cache.get_shuffled_active_validator_indices(state, epoch),
(slot mod SLOTS_PER_EPOCH) * committees_per_slot +
index.uint64,
committees_per_slot * SLOTS_PER_EPOCH
)
# This is a somewhat more fragile, but high-ROI, caching setup --
# get_active_validator_indices() is slow to run in a loop and only
# changes once per epoch. It is not, in the general case, possible
# to precompute these arbitrarily far out so still need to pick up
# missing cases here.
if epoch notin cache.shuffled_active_validator_indices:
cache.shuffled_active_validator_indices[epoch] =
get_shuffled_active_validator_indices(state, epoch)
func get_beacon_committee_len*(
state: BeaconState, slot: Slot, index: CommitteeIndex,
cache: var StateCache): uint64 =
# Return the number of members in the beacon committee at ``slot`` for ``index``.
let
epoch = compute_epoch_at_slot(slot)
committees_per_slot = get_committee_count_per_slot(state, epoch, cache)
try:
let committees_per_slot = get_committee_count_per_slot(
cache.shuffled_active_validator_indices[epoch].lenu64)
compute_committee(
cache.shuffled_active_validator_indices[epoch],
get_seed(state, epoch, DOMAIN_BEACON_ATTESTER),
(slot mod SLOTS_PER_EPOCH) * committees_per_slot +
index.uint64,
committees_per_slot * SLOTS_PER_EPOCH
)
except KeyError:
raiseAssert "values are added to cache before using them"
compute_committee_len(
count_active_validators(state, epoch, cache),
(slot mod SLOTS_PER_EPOCH) * committees_per_slot +
index.uint64,
committees_per_slot * SLOTS_PER_EPOCH
)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#compute_shuffled_index
func compute_shuffled_index(
@ -184,10 +237,9 @@ func compute_shuffled_index(
# Swap or not (https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf)
# See the 'generalized domain' algorithm on page 3
for current_round in 0 ..< SHUFFLE_ROUND_COUNT.int:
let round_bytes1 = int_to_bytes1(current_round)[0]
pivot_buffer[32] = round_bytes1
source_buffer[32] = round_bytes1
for current_round in 0'u8 ..< SHUFFLE_ROUND_COUNT.uint8:
pivot_buffer[32] = current_round
source_buffer[32] = current_round
let
# If using multiple indices, can amortize this
@ -237,36 +289,26 @@ func compute_proposer_index(state: BeaconState, indices: seq[ValidatorIndex],
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_beacon_proposer_index
func get_beacon_proposer_index*(state: BeaconState, cache: var StateCache, slot: Slot):
Option[ValidatorIndex] =
try:
if slot in cache.beacon_proposer_indices:
return cache.beacon_proposer_indices[slot]
except KeyError:
raiseAssert("Cached entries are added before use")
cache.beacon_proposer_indices.withValue(slot, proposer) do:
return proposer[]
do:
# Return the beacon proposer index at the current slot.
let epoch = get_current_epoch(state)
# Return the beacon proposer index at the current slot.
let epoch = get_current_epoch(state)
var buffer: array[32 + 8, byte]
buffer[0..31] = get_seed(state, epoch, DOMAIN_BEACON_PROPOSER).data
buffer[32..39] = uint_to_bytes8(slot.uint64)
var buffer: array[32 + 8, byte]
buffer[0..31] = get_seed(state, epoch, DOMAIN_BEACON_PROPOSER).data
buffer[32..39] = uint_to_bytes8(slot.uint64)
# TODO fixme; should only be run once per slot and cached
# There's exactly one beacon proposer per slot.
if epoch notin cache.shuffled_active_validator_indices:
cache.shuffled_active_validator_indices[epoch] =
get_shuffled_active_validator_indices(state, epoch)
# There's exactly one beacon proposer per slot.
try:
let
seed = eth2digest(buffer)
indices =
sorted(cache.shuffled_active_validator_indices[epoch], system.cmp)
sorted(cache.get_shuffled_active_validator_indices(state, epoch), system.cmp)
cache.beacon_proposer_indices[slot] =
compute_proposer_index(state, indices, seed)
cache.beacon_proposer_indices[slot]
except KeyError:
raiseAssert("Cached entries are added before use")
return cache.beacon_proposer_indices.mgetOrPut(
slot, compute_proposer_index(state, indices, seed))
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/beacon-chain.md#get_beacon_proposer_index
func get_beacon_proposer_index*(state: BeaconState, cache: var StateCache):
@ -300,33 +342,3 @@ func get_committee_assignment*(
if validator_index in committee:
return some((committee, idx, slot))
none(tuple[a: seq[ValidatorIndex], b: CommitteeIndex, c: Slot])
func get_committee_assignments*(
state: BeaconState, epoch: Epoch,
validator_indices: HashSet[ValidatorIndex]):
seq[tuple[subnetIndex: uint64, slot: Slot]] =
let next_epoch = get_current_epoch(state) + 1
doAssert epoch <= next_epoch
var cache = StateCache()
let start_slot = compute_start_slot_at_epoch(epoch)
let committees_per_slot =
get_committee_count_per_slot(state, epoch, cache)
for slot in start_slot ..< start_slot + SLOTS_PER_EPOCH:
for index in 0'u64 ..< committees_per_slot:
let idx = index.CommitteeIndex
if not disjoint(validator_indices,
get_beacon_committee(state, slot, idx, cache).toHashSet):
result.add(
(compute_subnet_for_attestation(committees_per_slot, slot, idx),
slot))
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#validator-assignments
func is_proposer(
state: BeaconState, validator_index: ValidatorIndex): bool {.used.} =
var cache = StateCache()
let proposer_index = get_beacon_proposer_index(state, cache)
proposer_index.isSome and proposer_index.get == validator_index

View File

@ -414,7 +414,7 @@ proc broadcastAggregatedAttestations(
# TODO https://github.com/status-im/nim-beacon-chain/issues/545
# this assumes in-process private keys
validator.privKey,
trailing_distance)
trailing_distance, cache)
# Don't broadcast when, e.g., this node isn't an aggregator
if aggregateAndProof.isSome:

View File

@ -188,12 +188,6 @@ template processEpochScenarioImpl(
when needCache:
var cache = StateCache()
let epoch = state.data.get_current_epoch()
cache.shuffled_active_validator_indices[epoch] =
get_shuffled_active_validator_indices(state.data, epoch)
# Epoch transitions can't fail (TODO is this true?)
when needCache:
transitionFn(state.data, cache)
else:
transitionFn(state.data)

View File

@ -3,7 +3,7 @@ import
../tests/[testblockutil],
../beacon_chain/[extras],
../beacon_chain/ssz/[merkleization, ssz_serialization],
../beacon_chain/spec/[beaconstate, datatypes, digest, helpers, presets]
../beacon_chain/spec/[beaconstate, crypto, datatypes, digest, helpers, presets]
template withTimer*(stats: var RunningStat, body: untyped) =
# TODO unify timing somehow
@ -99,3 +99,19 @@ proc printTimers*[Timers: enum](
echo "Validators: ", state.validators.len, ", epoch length: ", SLOTS_PER_EPOCH
echo "Validators per attestation (mean): ", attesters.mean
printTimers(validate, timers)
proc combine*(tgt: var Attestation, src: Attestation, flags: UpdateFlags) =
## Combine the signature and participation bitfield, with the assumption that
## the same data is being signed - if the signatures overlap, they are not
## combined.
doAssert tgt.data == src.data
# In a BLS aggregate signature, one needs to count how many times a
# particular public key has been added - since we use a single bit per key, we
# can only it once, thus we can never combine signatures that overlap already!
if not tgt.aggregation_bits.overlaps(src.aggregation_bits):
tgt.aggregation_bits.combine(src.aggregation_bits)
if skipBlsValidation notin flags:
tgt.signature.aggregate(src.signature)

View File

@ -35,8 +35,6 @@ proc addMockAttestations*(
# TODO: Working with an unsigned Gwei balance is a recipe for underflows to happen
var cache = StateCache()
cache.shuffled_active_validator_indices[epoch] =
get_shuffled_active_validator_indices(state, epoch)
var remaining_balance = state.get_total_active_balance(cache).int64 * 2 div 3
let

View File

@ -7,16 +7,12 @@
{.used.}
import
../beacon_chain/spec/datatypes,
../beacon_chain/ssz
import
unittest,
chronicles,
stew/byteutils,
./testutil, ./testblockutil,
../beacon_chain/spec/[digest, validator, state_transition,
./testutil, ./testblockutil, ../research/simutils,
../beacon_chain/spec/[crypto, datatypes, digest, validator, state_transition,
helpers, beaconstate, presets],
../beacon_chain/[beacon_node_types, attestation_pool, block_pool, extras],
../beacon_chain/fork_choice/[fork_choice_types, fork_choice]