initial attestation aggregation (#769)

* initial attestation aggregation

* fix usage of committee index, vs index in committee; uniformly set trailing/following distance; document how the only-broadcast-if mechanism works better and what aggregation already happens, not otherwise sufficiently clear; use correct BlockSlot across epoch boundaries

* address inconsistent notion of which slot in past to target for aggregate broadcast; follow 0.11.x aggregate broadcast p2p interface topic

* Fix get_slot_signature(...) call after get_domain(...) change required genesis_validators_root

* mark all spec references which aren't dealt with in other PRs as v0.11.1

* update two more spec refs to v0.11.1
This commit is contained in:
tersec 2020-04-01 09:59:55 +00:00 committed by GitHub
parent cd388bc9bb
commit 6eb4f1f39d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 98 additions and 34 deletions

View File

@ -5,20 +5,12 @@
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0). # * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms. # at your option. This file may not be copied, modified, or distributed except according to those terms.
# Have an an aggregated aggregation ready for broadcast at
# SECONDS_PER_SLOT * 2 / 3, i.e. 2/3 through relevant slot
# intervals.
#
# The other part is arguably part of attestation pool -- the validation's # The other part is arguably part of attestation pool -- the validation's
# something that should be happing on receipt, not aggregation per se. In # something that should be happing on receipt, not aggregation per se. In
# that part, check that messages conform -- so, check for each type # that part, check that messages conform -- so, check for each type
# https://github.com/ethereum/eth2.0-specs/blob/v0.10.1/specs/phase0/p2p-interface.md#topics-and-messages # https://github.com/ethereum/eth2.0-specs/blob/v0.10.1/specs/phase0/p2p-interface.md#topics-and-messages
# specifies. So by the time this calls attestation pool, all validation's # specifies. So by the time this calls attestation pool, all validation's
# already done. # already done.
#
# Finally, some of the filtering's libp2p stuff. Consistency checks between
# topic/message types and GOSSIP_MAX_SIZE -- mostly doesn't belong here, so
# while TODO, isn't TODO for this module.
import import
options, options,
@ -26,11 +18,7 @@ import
state_transition_block], state_transition_block],
./attestation_pool, ./beacon_node_types, ./ssz ./attestation_pool, ./beacon_node_types, ./ssz
# TODO gossipsub validation lives somewhere, maybe here # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#aggregation-selection
# TODO add tests, especially for validation
# https://github.com/status-im/nim-beacon-chain/issues/122#issuecomment-562479965
# https://github.com/ethereum/eth2.0-specs/blob/v0.10.1/specs/phase0/validator.md#aggregation-selection
func is_aggregator(state: BeaconState, slot: Slot, index: uint64, func is_aggregator(state: BeaconState, slot: Slot, index: uint64,
slot_signature: ValidatorSig): bool = slot_signature: ValidatorSig): bool =
# TODO index is a CommitteeIndex, aka uint64 # TODO index is a CommitteeIndex, aka uint64
@ -43,34 +31,49 @@ func is_aggregator(state: BeaconState, slot: Slot, index: uint64,
proc aggregate_attestations*( proc aggregate_attestations*(
pool: AttestationPool, state: BeaconState, index: uint64, pool: AttestationPool, state: BeaconState, index: uint64,
privkey: ValidatorPrivKey): Option[AggregateAndProof] = privkey: ValidatorPrivKey, trailing_distance: uint64): Option[AggregateAndProof] =
# TODO alias CommitteeIndex to actual type then convert various uint64's here # TODO alias CommitteeIndex to actual type then convert various uint64's here
let doAssert state.slot >= trailing_distance
slot = state.slot - 2
slot_signature = get_slot_signature(state.fork, slot, privkey) # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#configuration
doAssert trailing_distance <= ATTESTATION_PROPAGATION_SLOT_RANGE
let
slot = state.slot - trailing_distance
slot_signature = get_slot_signature(
state.fork, state.genesis_validators_root, slot, privkey)
if slot < 0:
return none(AggregateAndProof)
doAssert slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= state.slot doAssert slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= state.slot
doAssert state.slot >= slot doAssert state.slot >= slot
# https://github.com/ethereum/eth2.0-specs/blob/v0.10.1/specs/phase0/validator.md#aggregation-selection # TODO performance issue for future, via get_active_validator_indices(...)
doAssert index < get_committee_count_at_slot(state, slot)
# TODO for testing purposes, refactor this into the condition check
# and just calculation
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#aggregation-selection
if not is_aggregator(state, slot, index, slot_signature): if not is_aggregator(state, slot, index, slot_signature):
return none(AggregateAndProof) return none(AggregateAndProof)
let attestation_data = # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#attestation-data
makeAttestationData(state, slot, index, get_block_root_at_slot(state, slot)) # describes how to construct an attestation, which applies for makeAttestationData(...)
# TODO this won't actually match anything
let attestation_data = AttestationData(
slot: slot,
index: index,
beacon_block_root: get_block_root_at_slot(state, slot))
# https://github.com/ethereum/eth2.0-specs/blob/v0.10.1/specs/phase0/validator.md#construct-aggregate # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#construct-aggregate
for attestation in getAttestationsForBlock(pool, state, slot): # TODO once EV goes in w/ refactoring of getAttestationsForBlock, pull out the getSlot version and use
# it. This is incorrect.
for attestation in getAttestationsForBlock(pool, state):
# getAttestationsForBlock(...) already aggregates
if attestation.data == attestation_data: if attestation.data == attestation_data:
# https://github.com/ethereum/eth2.0-specs/blob/v0.10.1/specs/phase0/validator.md#aggregateandproof # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#aggregateandproof
return some(AggregateAndProof( return some(AggregateAndProof(
aggregator_index: index, aggregator_index: index,
aggregate: attestation, aggregate: attestation,
selection_proof: slot_signature)) selection_proof: slot_signature))
# TODO in catch-up mode, we could get here, so probably shouldn't assert
doAssert false
none(AggregateAndProof) none(AggregateAndProof)

View File

@ -15,7 +15,8 @@ import
conf, time, state_transition, beacon_chain_db, validator_pool, extras, conf, time, state_transition, beacon_chain_db, validator_pool, extras,
attestation_pool, block_pool, eth2_network, eth2_discovery, attestation_pool, block_pool, eth2_network, eth2_discovery,
beacon_node_types, mainchain_monitor, version, ssz, ssz/dynamic_navigator, beacon_node_types, mainchain_monitor, version, ssz, ssz/dynamic_navigator,
sync_protocol, request_manager, validator_keygen, interop, statusbar sync_protocol, request_manager, validator_keygen, interop, statusbar,
attestation_aggregation
const const
genesisFile = "genesis.ssz" genesisFile = "genesis.ssz"
@ -602,6 +603,45 @@ proc verifyFinalization(node: BeaconNode, slot: Slot) =
node.blockPool.finalizedHead.blck.slot.compute_epoch_at_slot() node.blockPool.finalizedHead.blck.slot.compute_epoch_at_slot()
doAssert finalizedEpoch + 2 == epoch doAssert finalizedEpoch + 2 == epoch
proc broadcastAggregatedAttestations(
node: BeaconNode, state: auto, head: var auto, slot: Slot,
trailing_distance: uint64) =
# The index is via a
# locally attested validator. Unlike in handleAttestations(...) there's a
# single one at most per slot (because that's how aggregation attestation
# works), so the machinery that has to handle looping across, basically a
# set of locally attached validators is in principle not necessary, but a
# way to organize this. Then the private key for that validator should be
# the corresponding one -- whatver they are, they match.
let
bs = BlockSlot(blck: head, slot: slot)
committees_per_slot = get_committee_count_at_slot(state, slot)
var cache = get_empty_per_epoch_cache()
for committee_index in 0'u64..<committees_per_slot:
let
committee = get_beacon_committee(state, slot, committee_index, cache)
for index_in_committee, validatorIdx in committee:
let validator = node.getAttachedValidator(state, validatorIdx)
if validator != nil:
# This is slightly strange/inverted control flow, since really it's
# going to happen once per slot, but this is the best way to get at
# the validator index and private key pair. TODO verify it only has
# one isSome() with test.
let option_aggregateandproof =
aggregate_attestations(node.attestationPool, state,
committee_index,
# TODO https://github.com/status-im/nim-beacon-chain/issues/545
# this assumes in-process private keys
validator.privKey,
trailing_distance)
# Don't broadcast when, e.g., this node isn't an aggregator
if option_aggregateandproof.isSome:
node.network.broadcast(
topicAggregateAndProof, option_aggregateandproof.get)
proc onSlotStart(node: BeaconNode, lastSlot, scheduledSlot: Slot) {.gcsafe, async.} = proc onSlotStart(node: BeaconNode, lastSlot, scheduledSlot: Slot) {.gcsafe, async.} =
## Called at the beginning of a slot - usually every slot, but sometimes might ## Called at the beginning of a slot - usually every slot, but sometimes might
## skip a few in case we're running late. ## skip a few in case we're running late.
@ -697,7 +737,7 @@ proc onSlotStart(node: BeaconNode, lastSlot, scheduledSlot: Slot) {.gcsafe, asyn
var head = node.updateHead() var head = node.updateHead()
# TODO is the slot of the clock or the head block more interestion? provide # TODO is the slot of the clock or the head block more interesting? provide
# rationale in comment # rationale in comment
beacon_head_slot.set slot.int64 beacon_head_slot.set slot.int64
@ -762,11 +802,12 @@ proc onSlotStart(node: BeaconNode, lastSlot, scheduledSlot: Slot) {.gcsafe, asyn
# with any clock discrepancies once only, at the start of slot timer # with any clock discrepancies once only, at the start of slot timer
# processing.. # processing..
# https://github.com/ethereum/eth2.0-specs/blob/v0.9.4/specs/validator/0_beacon-chain-validator.md#attesting # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#attesting
# A validator should create and broadcast the attestation to the # A validator should create and broadcast the attestation to the associated
# associated attestation subnet one-third of the way through the slot # attestation subnet when either (a) the validator has received a valid
# during which the validator is assigned―that is, SECONDS_PER_SLOT / 3 # block from the expected block proposer for the assigned slot or
# seconds after the start of slot. # (b) one-third of the slot has transpired (`SECONDS_PER_SLOT / 3` seconds
# after the start of slot) -- whichever comes first.
let let
attestationStart = node.beaconClock.fromNow(slot) attestationStart = node.beaconClock.fromNow(slot)
thirdSlot = seconds(int64(SECONDS_PER_SLOT)) div 3 thirdSlot = seconds(int64(SECONDS_PER_SLOT)) div 3
@ -788,6 +829,25 @@ proc onSlotStart(node: BeaconNode, lastSlot, scheduledSlot: Slot) {.gcsafe, asyn
handleAttestations(node, head, slot) handleAttestations(node, head, slot)
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#broadcast-aggregate
# If the validator is selected to aggregate (is_aggregator), then they
# broadcast their best aggregate as a SignedAggregateAndProof to the global
# aggregate channel (beacon_aggregate_and_proof) two-thirds of the way
# through the slot-that is, SECONDS_PER_SLOT * 2 / 3 seconds after the start
# of slot.
if slot > 2:
const TRAILING_DISTANCE = 1
let aggregationSlot = slot - TRAILING_DISTANCE
var aggregationHead = getAncestorAt(head, aggregationSlot)
let bs = BlockSlot(blck: aggregationHead, slot: aggregationSlot)
node.blockPool.withState(node.blockPool.tmpState, bs):
let twoThirdsSlot =
toBeaconTime(slot, seconds(2*int64(SECONDS_PER_SLOT)) div 3)
addTimer(saturate(node.beaconClock.fromNow(twoThirdsSlot))) do (p: pointer):
broadcastAggregatedAttestations(
node, state, aggregationHead, aggregationSlot, TRAILING_DISTANCE)
# TODO ... and beacon clock might jump here also. sigh. # TODO ... and beacon clock might jump here also. sigh.
let let
nextSlotStart = saturate(node.beaconClock.fromNow(nextSlot)) nextSlotStart = saturate(node.beaconClock.fromNow(nextSlot))

View File

@ -13,6 +13,7 @@ const
topicVoluntaryExits* = "/eth2/voluntary_exit/ssz" topicVoluntaryExits* = "/eth2/voluntary_exit/ssz"
topicProposerSlashings* = "/eth2/proposer_slashing/ssz" topicProposerSlashings* = "/eth2/proposer_slashing/ssz"
topicAttesterSlashings* = "/eth2/attester_slashing/ssz" topicAttesterSlashings* = "/eth2/attester_slashing/ssz"
topicAggregateAndProof* = "/eth2/beacon_aggregate_and_proof/ssz"
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.0/specs/phase0/p2p-interface.md#configuration # https://github.com/ethereum/eth2.0-specs/blob/v0.11.0/specs/phase0/p2p-interface.md#configuration
ATTESTATION_SUBNET_COUNT* = 64 ATTESTATION_SUBNET_COUNT* = 64