diff --git a/beacon_chain/attestation_aggregation.nim b/beacon_chain/attestation_aggregation.nim index cc5110210..a8b59355d 100644 --- a/beacon_chain/attestation_aggregation.nim +++ b/beacon_chain/attestation_aggregation.nim @@ -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 diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 2aa59d555..d43ec3483 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -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) = diff --git a/beacon_chain/block_pools/candidate_chains.nim b/beacon_chain/block_pools/candidate_chains.nim index 61309a7e2..70927b648 100644 --- a/beacon_chain/block_pools/candidate_chains.nim +++ b/beacon_chain/block_pools/candidate_chains.nim @@ -640,17 +640,10 @@ 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 - - body + # 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) = ## Rewind or advance state such that it matches the given block and slot - diff --git a/beacon_chain/spec/signatures.nim b/beacon_chain/spec/signatures.nim index f6debb217..9e6b70d85 100644 --- a/beacon_chain/spec/signatures.nim +++ b/beacon_chain/spec/signatures.nim @@ -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, diff --git a/beacon_chain/ssz/types.nim b/beacon_chain/ssz/types.nim index 6d2640530..7cdd990d6 100644 --- a/beacon_chain/ssz/types.nim +++ b/beacon_chain/ssz/types.nim @@ -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 = diff --git a/tests/helpers/math_helpers.nim b/tests/helpers/math_helpers.nim index fb843a73b..ec644f97c 100644 --- a/tests/helpers/math_helpers.nim +++ b/tests/helpers/math_helpers.nim @@ -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 diff --git a/tests/mocking/mock_attestations.nim b/tests/mocking/mock_attestations.nim index cec5337cd..ba9daff6f 100644 --- a/tests/mocking/mock_attestations.nim +++ b/tests/mocking/mock_attestations.nim @@ -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, diff --git a/tests/mocking/mock_validator_keys.nim b/tests/mocking/mock_validator_keys.nim index 5ea3c2ccb..35502ce27 100644 --- a/tests/mocking/mock_validator_keys.nim +++ b/tests/mocking/mock_validator_keys.nim @@ -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] ) =