implement aggregated attestation receiving/validating (#1272)

* implement aggregated attestation receiving/validating

* document the conditions without explicit implementations in isValidAggregatedAttestation()
This commit is contained in:
tersec 2020-07-02 16:15:27 +00:00 committed by GitHub
parent 27ba123d8a
commit c64737e7f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 172 additions and 27 deletions

View File

@ -20,9 +20,7 @@ logScope:
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#aggregation-selection
func is_aggregator(state: BeaconState, slot: Slot, index: CommitteeIndex,
slot_signature: ValidatorSig): bool =
var cache = get_empty_per_epoch_cache()
slot_signature: ValidatorSig, cache: var StateCache): bool =
let
committee = get_beacon_committee(state, slot, index, cache)
modulo = max(1, len(committee) div TARGET_AGGREGATORS_PER_COMMITTEE).uint64
@ -33,7 +31,7 @@ proc aggregate_attestations*(
privkey: ValidatorPrivKey, trailing_distance: uint64): Option[AggregateAndProof] =
doAssert state.slot >= trailing_distance
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/p2p-interface.md#configuration
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/p2p-interface.md#configuration
doAssert trailing_distance <= ATTESTATION_PROPAGATION_SLOT_RANGE
let
@ -49,8 +47,9 @@ proc aggregate_attestations*(
# 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):
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#aggregation-selection
var cache = get_empty_per_epoch_cache()
if not is_aggregator(state, slot, index, slot_signature, cache):
return none(AggregateAndProof)
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#attestation-data
@ -67,7 +66,7 @@ proc aggregate_attestations*(
for attestation in getAttestationsForBlock(pool, state):
# getAttestationsForBlock(...) already aggregates
if attestation.data == attestation_data:
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md#aggregateandproof
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#aggregateandproof
return some(AggregateAndProof(
aggregator_index: index.uint64,
aggregate: attestation,
@ -160,3 +159,120 @@ proc isValidAttestation*(
return false
true
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/p2p-interface.md#global-topics
proc isValidAggregatedAttestation*(
pool: var AttestationPool,
signedAggregateAndProof: SignedAggregateAndProof,
current_slot: Slot): bool =
let
aggregate_and_proof = signedAggregateAndProof.message
aggregate = aggregate_and_proof.aggregate
# 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.
#
# [IGNORE] aggregate.data.slot is within the last
# 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):
debug "isValidAggregatedAttestation: aggregation.data.slot not within ATTESTATION_PROPAGATION_SLOT_RANGE"
return false
# [IGNORE] The valid aggregate attestation defined by
# hash_tree_root(aggregate) has not already been seen (via aggregate gossip,
# within a verified block, or through the creation of an equivalent aggregate
# locally).
#
# This is [IGNORE] and already checked by attestation pool when aggregate is
# added.
# [IGNORE] The aggregate is the first valid aggregate received for the
# aggregator with index aggregate_and_proof.aggregator_index for the epoch
# aggregate.data.target.epoch.
#
# This is [IGNORE] and already effectively checked by attestation pool upon
# attempting to resolve attestations.
# [REJECT] The block being voted for (aggregate.data.beacon_block_root)
# passes validation.
let attestationBlck = pool.blockPool.getRef(aggregate.data.beacon_block_root)
if attestationBlck.isNil:
debug "isValidAggregatedAttestation: block doesn't exist in block pool"
pool.blockPool.addMissing(aggregate.data.beacon_block_root)
return false
# [REJECT] The attestation has participants -- that is,
# len(get_attesting_indices(state, aggregate.data, aggregate.aggregation_bits)) >= 1.
#
# get_attesting_indices() is:
# committee = get_beacon_committee(state, data.slot, data.index)
# return set(index for i, index in enumerate(committee) if bits[i])
#
# the attestation doesn't have participants is iff either:
# (1) the aggregation bits are all 0; or
# (2) the non-zero aggregation bits don't overlap with extant committee
# members, i.e. they counts don't match.
# But (2) would reflect an invalid aggregation in other ways, so reject it
# either way.
if isZeros(aggregate.aggregation_bits):
debug "isValidAggregatedAttestation: attestation has no or invalid aggregation bits"
return false
# [REJECT] aggregate_and_proof.selection_proof selects the validator as an
# aggregator for the slot -- i.e. is_aggregator(state, aggregate.data.slot,
# aggregate.data.index, aggregate_and_proof.selection_proof) returns True.
# TODO use withEpochState when it works more reliably
pool.blockPool.withState(
pool.blockPool.tmpState,
BlockSlot(blck: attestationBlck, slot: aggregate.data.slot)):
var cache = getEpochCache(blck, state)
if not is_aggregator(
state, aggregate.data.slot, aggregate.data.index.CommitteeIndex,
aggregate_and_proof.selection_proof, cache):
debug "isValidAggregatedAttestation: incorrect aggregator"
return false
# [REJECT] The aggregator's validator index is within the committee -- i.e.
# aggregate_and_proof.aggregator_index in get_beacon_committee(state,
# aggregate.data.slot, aggregate.data.index).
if aggregate_and_proof.aggregator_index.ValidatorIndex notin
get_beacon_committee(
state, aggregate.data.slot, aggregate.data.index.CommitteeIndex, cache):
debug "isValidAggregatedAttestation: aggregator's validator index not in committee"
return false
# [REJECT] The aggregate_and_proof.selection_proof is a valid signature of the
# aggregate.data.slot by the validator with index
# aggregate_and_proof.aggregator_index.
# get_slot_signature(state, aggregate.data.slot, privkey)
if aggregate_and_proof.aggregator_index >= state.validators.len.uint64:
debug "isValidAggregatedAttestation: invalid aggregator_index"
return false
if not verify_slot_signature(
state.fork, state.genesis_validators_root, aggregate.data.slot,
state.validators[aggregate_and_proof.aggregator_index].pubkey,
aggregate_and_proof.selection_proof):
debug "isValidAggregatedAttestation: selection_proof signature verification failed"
return false
# [REJECT] The aggregator signature, signed_aggregate_and_proof.signature, is valid.
if not verify_aggregate_and_proof_signature(
state.fork, state.genesis_validators_root, aggregate_and_proof,
state.validators[aggregate_and_proof.aggregator_index].pubkey,
signed_aggregate_and_proof.signature):
debug "isValidAggregatedAttestation: signed_aggregate_and_proof signature verification failed"
return false
# [REJECT] The signature of aggregate is valid.
if not is_valid_indexed_attestation(
state, get_indexed_attestation(state, aggregate, cache), {}):
debug "isValidAggregatedAttestation: aggregate signature verification failed"
return false
debug "isValidAggregatedAttestation: succeeded"
true

View File

@ -751,6 +751,13 @@ proc installAttestationHandlers(node: BeaconNode) =
return false
node.attestationPool.isValidAttestation(attestation, slot, committeeIndex)
proc aggregatedAttestationValidator(
signedAggregateAndProof: SignedAggregateAndProof): bool =
let (afterGenesis, slot) = node.beaconClock.now().toSlot()
if not afterGenesis:
return false
node.attestationPool.isValidAggregatedAttestation(signedAggregateAndProof, slot)
var attestationSubscriptions: seq[Future[void]] = @[]
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/p2p-interface.md#attestations-and-aggregation
@ -764,6 +771,14 @@ proc installAttestationHandlers(node: BeaconNode) =
attestationValidator(attestation, ci)
))
attestationSubscriptions.add(node.network.subscribe(
getAggregateAndProofsTopic(node.forkDigest),
proc(signedAggregateAndProof: SignedAggregateAndProof) =
attestationHandler(signedAggregateAndProof.message.aggregate),
proc(signedAggregateAndProof: SignedAggregateAndProof): bool =
aggregatedAttestationValidator(signedAggregateAndProof)
))
waitFor allFutures(attestationSubscriptions)
proc stop*(node: BeaconNode) =

View File

@ -640,16 +640,9 @@ template withEpochState*(
## cache is unsafe outside of block.
## TODO async transformations will lead to a race where cache gets updated
## while waiting for future to complete - catch this here somehow?
for ancestor in get_ancestors_in_epoch(blockSlot):
if getStateDataCached(dag, cache, ancestor):
break
template hashedState(): HashedBeaconState {.inject, used.} = cache.data
template state(): BeaconState {.inject, used.} = cache.data.data
template blck(): BlockRef {.inject, used.} = cache.blck
template root(): Eth2Digest {.inject, used.} = cache.data.root
# TODO implement the looser constraints allowed by epoch, not precise slot target
# allow expressing preference to opt-in to looser constraints regardless
dag.withState(cache, blockSlot):
body
proc updateStateData*(dag: CandidateChains, state: var StateData, bs: BlockSlot) =

View File

@ -28,6 +28,18 @@ func get_slot_signature*(
blsSign(privKey, signing_root.data)
func verify_slot_signature*(
fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot,
pubkey: ValidatorPubKey, signature: SomeSig): bool =
withTrust(signature):
let
epoch = compute_epoch_at_slot(slot)
domain = get_domain(
fork, DOMAIN_SELECTION_PROOF, epoch, genesis_validators_root)
signing_root = compute_signing_root(slot, domain)
blsVerify(pubkey, signing_root.data, signature)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#randao-reveal
func get_epoch_signature*(
fork: Fork, genesis_validators_root: Eth2Digest, epoch: Epoch,
@ -86,6 +98,18 @@ func get_aggregate_and_proof_signature*(fork: Fork, genesis_validators_root: Eth
blsSign(privKey, signing_root.data)
func verify_aggregate_and_proof_signature*(fork: Fork, genesis_validators_root: Eth2Digest,
aggregate_and_proof: AggregateAndProof,
pubkey: ValidatorPubKey, signature: SomeSig): bool =
withTrust(signature):
let
epoch = compute_epoch_at_slot(aggregate_and_proof.aggregate.data.slot)
domain = get_domain(
fork, DOMAIN_AGGREGATE_AND_PROOF, epoch, genesis_validators_root)
signing_root = compute_signing_root(aggregate_and_proof, domain)
blsVerify(pubKey, signing_root.data, signature)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#aggregate-signature
func get_attestation_signature*(
fork: Fork, genesis_validators_root: Eth2Digest,

View File

@ -136,6 +136,7 @@ template clearBit*(x: var BitList, idx: int) = clearBit(BitSeq(x), idx)
template overlaps*(a, b: BitList): bool = overlaps(BitSeq(a), BitSeq(b))
template combine*(a: var BitList, b: BitList) = combine(BitSeq(a), BitSeq(b))
template isSubsetOf*(a, b: BitList): bool = isSubsetOf(BitSeq(a), BitSeq(b))
template isZeros*(x: BitList): bool = isZeros(BitSeq(x))
template `$`*(a: BitList): string = $(BitSeq(a))
iterator items*(x: BitList): bool =

View File

@ -1,5 +1,5 @@
# beacon_chain
# Copyright (c) 2018-2019 Status Research & Development GmbH
# 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).
@ -7,8 +7,4 @@
func round_multiple_down*(x: uint64, n: uint64): uint64 {.inline.} =
## Round the input to the previous multiple of "n"
result = x - x mod n
func round_multiple_up*(x: uint64, n: uint64): uint64 {.inline.} =
## Round the input to the next multiple of "n"
result = ((x + n - 1) div n) * n
x - x mod n

View File

@ -112,7 +112,7 @@ proc mockAttestation*(
flags: UpdateFlags = {}): Attestation {.inline.}=
mockAttestationImpl(state, slot, flags)
proc fillAggregateAttestation*(state: BeaconState, attestation: var Attestation) =
func fillAggregateAttestation*(state: BeaconState, attestation: var Attestation) =
var cache = get_empty_per_epoch_cache()
let beacon_committee = get_beacon_committee(
state,

View File

@ -1,5 +1,5 @@
# beacon_chain
# Copyright (c) 2018-2019 Status Research & Development GmbH
# 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).
@ -19,7 +19,7 @@ proc genMockPrivKeys(privkeys: var array[MIN_GENESIS_ACTIVE_VALIDATOR_COUNT, Val
let pair = newKeyPair()[]
privkeys[i] = pair.priv
proc genMockPubKeys(
func genMockPubKeys(
pubkeys: var array[MIN_GENESIS_ACTIVE_VALIDATOR_COUNT, ValidatorPubKey],
privkeys: array[MIN_GENESIS_ACTIVE_VALIDATOR_COUNT, ValidatorPrivKey]
) =