diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index b06a237c9..71e6332b9 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -5,6 +5,11 @@ AllTests-mainnet + ancestorSlot OK ``` OK: 1/1 Fail: 0/1 Skip: 0/1 +## Attestation pool electra processing [Preset: mainnet] +```diff ++ Can add and retrieve simple electra attestations [Preset: mainnet] OK +``` +OK: 1/1 Fail: 0/1 Skip: 0/1 ## Attestation pool processing [Preset: mainnet] ```diff + Attestation from different branch [Preset: mainnet] OK @@ -1020,4 +1025,4 @@ OK: 2/2 Fail: 0/2 Skip: 0/2 OK: 9/9 Fail: 0/9 Skip: 0/9 ---TOTAL--- -OK: 685/690 Fail: 0/690 Skip: 5/690 +OK: 686/691 Fail: 0/691 Skip: 5/691 diff --git a/beacon_chain/consensus_object_pools/attestation_pool.nim b/beacon_chain/consensus_object_pools/attestation_pool.nim index 3874c3a4c..9136d88a2 100644 --- a/beacon_chain/consensus_object_pools/attestation_pool.nim +++ b/beacon_chain/consensus_object_pools/attestation_pool.nim @@ -42,31 +42,25 @@ type ## be further combined. aggregation_bits: CVBType aggregate_signature: AggregateSignature + Phase0Validation = Validation[CommitteeValidatorsBits] ElectraValidation = Validation[ElectraCommitteeValidatorsBits] - Phase0AttestationEntry = object + AttestationEntry[CVBType] = object ## Each entry holds the known signatures for a particular, distinct vote + ## For electra+, the data has been changed to hold the committee index data: AttestationData committee_len: int singles: Table[int, CookedSig] ## \ ## On the attestation subnets, only attestations with a single vote are ## allowed - these can be collected separately to top up aggregates with - ## here we collect them by mapping index in committee to a vote - aggregates: seq[Phase0Validation] + aggregates: seq[Validation[CVBType]] - ElectraAttestationEntry = object - ## Each entry holds the known signatures for a particular, distinct vote - data: AttestationData - committee_bits: AttestationCommitteeBits - committee_len: int - singles: Table[int, CookedSig] ## \ - ## On the attestation subnets, only attestations with a single vote are - ## allowed - these can be collected separately to top up aggregates with - - ## here we collect them by mapping index in committee to a vote - aggregates: seq[ElectraValidation] + Phase0AttestationEntry = AttestationEntry[CommitteeValidatorsBits] + ElectraAttestationEntry = AttestationEntry[ElectraCommitteeValidatorsBits] - AttestationTable[AEType] = Table[Eth2Digest, AEType] + AttestationTable[CVBType] = Table[Eth2Digest, AttestationEntry[CVBType]] ## Depending on the world view of the various validators, they may have ## voted on different states - this map keeps track of each vote keyed by ## getAttestationCandidateKey() @@ -79,12 +73,12 @@ type ## are tracked separately in the fork choice. phase0Candidates: array[ATTESTATION_LOOKBACK.int, - AttestationTable[Phase0AttestationEntry]] ## \ + AttestationTable[CommitteeValidatorsBits]] ## \ ## We keep one item per slot such that indexing matches slot number ## together with startingSlot electraCandidates: array[ATTESTATION_LOOKBACK.int, - AttestationTable[ElectraAttestationEntry]] ## \ + AttestationTable[ElectraCommitteeValidatorsBits]] ## \ ## We keep one item per slot such that indexing matches slot number ## together with startingSlot @@ -249,7 +243,7 @@ func oneIndex( return Opt.none(int) res -func toAttestation(entry: Phase0AttestationEntry, validation: Phase0Validation): +func toAttestation(entry: AttestationEntry, validation: Phase0Validation): phase0.Attestation = phase0.Attestation( aggregation_bits: validation.aggregation_bits, @@ -258,17 +252,24 @@ func toAttestation(entry: Phase0AttestationEntry, validation: Phase0Validation): ) func toElectraAttestation( - entry: ElectraAttestationEntry, validation: ElectraValidation): + entry: AttestationEntry, validation: ElectraValidation): electra.Attestation = + var committee_bits: AttestationCommitteeBits + committee_bits[int(entry.data.index)] = true + electra.Attestation( aggregation_bits: validation.aggregation_bits, - committee_bits: entry.committee_bits, - data: entry.data, + committee_bits: committee_bits, + data: AttestationData( + slot: entry.data.slot, + index: 0, + beacon_block_root: entry.data.beacon_block_root, + source: entry.data.source, + target: entry.data.target), signature: validation.aggregate_signature.finish().toValidatorSig() ) -func updateAggregates( - entry: var (Phase0AttestationEntry | ElectraAttestationEntry)) = +func updateAggregates(entry: var AttestationEntry) = # Upgrade the list of aggregates to ensure that there is at least one # aggregate (assuming there are singles) and all aggregates have all # singles incorporated @@ -334,7 +335,7 @@ func updateAggregates( inc i func covers( - entry: Phase0AttestationEntry | ElectraAttestationEntry, + entry: AttestationEntry, bits: CommitteeValidatorsBits | ElectraCommitteeValidatorsBits): bool = for i in 0.. maxAttestationSlot: # Around genesis.. @@ -820,9 +823,6 @@ proc getAttestationsForBlock*(pool: var AttestationPool, proc getElectraAttestationsForBlock*( pool: var AttestationPool, state: electra.HashedBeaconState, cache: var StateCache): seq[electra.Attestation] = - ## Retrieve attestations that may be added to a new block at the slot of the - ## given state - ## https://github.com/ethereum/consensus-specs/blob/v1.4.0/specs/phase0/validator.md#attestations let newBlockSlot = state.data.slot.uint64 if newBlockSlot < MIN_ATTESTATION_INCLUSION_DELAY: @@ -836,8 +836,9 @@ proc getElectraAttestationsForBlock*( var candidates: seq[tuple[ - score: int, slot: Slot, entry: ptr ElectraAttestationEntry, validation: int]] - attCache = AttestationCache.init(state, cache) + score: int, slot: Slot, entry: ptr ElectraAttestationEntry, + validation: int]] + attCache = AttestationCache[ElectraCommitteeValidatorsBits].init(state, cache) for i in 0.. maxAttestationSlot: # Around genesis.. @@ -853,24 +854,25 @@ proc getElectraAttestationsForBlock*( break for _, entry in pool.electraCandidates[candidateIdx.get()].mpairs(): - entry.updateAggregates() # TODO doesn't handle electra ones + entry.updateAggregates() for j in 0.. 0 and res.lenu64() < MAX_ATTESTATIONS_ELECTRA: + while candidates.len > 0 and res.lenu64() < + MAX_ATTESTATIONS_ELECTRA * MAX_COMMITTEES_PER_SLOT: let entryCacheKey = block: # Find the candidate with the highest score - slot is used as a # tie-breaker so that more recent attestations are added first let candidate = # Fast path for when all remaining candidates fit - if candidates.lenu64 < MAX_ATTESTATIONS_ELECTRA: candidates.len - 1 - else: maxIndex(candidates) + if candidates.lenu64 < MAX_ATTESTATIONS_ELECTRA * MAX_COMMITTEES_PER_SLOT: + candidates.len - 1 + else: + maxIndex(candidates) (_, _, entry, j) = candidates[candidate] candidates.del(candidate) # careful, `del` reorders candidates @@ -929,7 +929,7 @@ proc getElectraAttestationsForBlock*( # Update cache so that the new votes are taken into account when updating # the score below - #attCache.add(entry[].data, entry[].aggregates[j].aggregation_bits) + attCache.add(entry[].data, entry[].aggregates[j].aggregation_bits) entry[].data.getAttestationCacheKey @@ -941,21 +941,43 @@ proc getElectraAttestationsForBlock*( if it.entry[].data.getAttestationCacheKey != entryCacheKey: continue - it.score = 1 + it.score = attCache.score( + it.entry[].data, + it.entry[].aggregates[it.validation].aggregation_bits) candidates.keepItIf: # Only keep candidates that might add coverage it.score > 0 + # TODO sort candidates by score - or really, rewrite the whole loop above ;) + var res2: seq[electra.Attestation] + var perBlock: Table[(Eth2Digest, Slot), seq[electra.Attestation]] + + for a in res: + let key = (a.data.beacon_block_root, a.data.slot) + perBlock.mGetOrPut(key, newSeq[electra.Attestation](0)).add(a) + + for a in perBlock.values(): + # TODO this will create on-chain aggregates that contain only one + # committee index - this is obviously wrong but fixing requires + # a more significant rewrite - we should combine the best aggregates + # for each beacon block root + let x = compute_on_chain_aggregate(a).valueOr: + continue + + res2.add(x) + if res2.lenu64 == MAX_ATTESTATIONS_ELECTRA: + break + let packingDur = Moment.now() - startPackingTick debug "Packed attestations for block", - newBlockSlot, packingDur, totalCandidates, attestations = res.len() + newBlockSlot, packingDur, totalCandidates, attestations = res2.len() attestation_pool_block_attestation_packing_time.set( packingDur.toFloatSeconds()) - res + res2 proc getElectraAttestationsForBlock*( pool: var AttestationPool, state: ForkedHashedBeaconState, diff --git a/beacon_chain/consensus_object_pools/spec_cache.nim b/beacon_chain/consensus_object_pools/spec_cache.nim index 633bad21b..b808c214e 100644 --- a/beacon_chain/consensus_object_pools/spec_cache.nim +++ b/beacon_chain/consensus_object_pools/spec_cache.nim @@ -90,7 +90,7 @@ func compatible_with_shuffling*( iterator get_attesting_indices*(shufflingRef: ShufflingRef, slot: Slot, committee_index: CommitteeIndex, - bits: CommitteeValidatorsBits): + bits: CommitteeValidatorsBits | ElectraCommitteeValidatorsBits): ValidatorIndex = if not bits.compatible_with_shuffling(shufflingRef, slot, committee_index): trace "get_attesting_indices: inconsistent aggregation and committee length" @@ -104,34 +104,26 @@ iterator get_attesting_indices*(shufflingRef: ShufflingRef, iterator get_attesting_indices*(shufflingRef: ShufflingRef, slot: Slot, committee_bits: AttestationCommitteeBits, - aggregation_bits: ElectraCommitteeValidatorsBits): + aggregation_bits: ElectraCommitteeValidatorsBits, on_chain: static bool): ValidatorIndex = - debugRaiseAssert "compatible with shuffling? needs checking" - #if not aggregation_bits.compatible_with_shuffling(shufflingRef, slot, committee_index): - if false: - trace "get_attesting_indices: inconsistent aggregation and committee length" + when on_chain: + var pos = 0 + for committee_index in get_committee_indices(committee_bits): + for _, validator_index in get_beacon_committee( + shufflingRef, slot, committee_index): + + if aggregation_bits[pos]: + yield validator_index + pos += 1 else: - debugComment "replace this implementation with actual iterator, after checking on conditions re repeat vals, ordering, etc; this is almost direct transcription of spec link algorithm in one of the places it doesn't make sense" - ## Return the set of attesting indices corresponding to ``aggregation_bits`` - ## and ``committee_bits``. - var output: HashSet[ValidatorIndex] - let committee_indices = toSeq(committee_bits.oneIndices) - var committee_offset = 0 - for index in committee_indices: - let committee = get_beacon_committee(shufflingRef, slot, index.CommitteeIndex) - var committee_attesters: HashSet[ValidatorIndex] - for i, index in committee: - if aggregation_bits[committee_offset + i]: - committee_attesters.incl index - output.incl committee_attesters - - committee_offset += len(committee) - - for validatorIndex in output: - yield validatorIndex + let committee_index = get_committee_index_one(committee_bits) + for validator_index in get_attesting_indices( + shufflingRef, slot, committee_index, aggregation_bits, on_chain): + yield validator_index iterator get_attesting_indices*( - dag: ChainDAGRef, attestation: phase0.TrustedAttestation): ValidatorIndex = + dag: ChainDAGRef, attestation: phase0.TrustedAttestation, + on_chain: static bool = true): ValidatorIndex = block: # `return` is not allowed in an inline iterator let slot = @@ -182,8 +174,8 @@ iterator get_attesting_indices*( yield validator iterator get_attesting_indices*( - dag: ChainDAGRef, attestation: electra.TrustedAttestation): ValidatorIndex = - debugRaiseAssert "bad duplication, mostly to avoid the get_attesting_index call from potentially getting screwed up in deployment version" + dag: ChainDAGRef, attestation: electra.TrustedAttestation, + on_chain: static bool): ValidatorIndex = block: # `return` is not allowed in an inline iterator let slot = @@ -220,13 +212,14 @@ iterator get_attesting_indices*( break for validator in get_attesting_indices( - shufflingRef, slot, attestation.committee_bits, attestation.aggregation_bits): + shufflingRef, slot, attestation.committee_bits, + attestation.aggregation_bits, on_chain): yield validator func get_attesting_indices_one*(shufflingRef: ShufflingRef, slot: Slot, committee_index: CommitteeIndex, - bits: CommitteeValidatorsBits): + bits: CommitteeValidatorsBits | ElectraCommitteeValidatorsBits): Option[ValidatorIndex] = # A variation on get_attesting_indices that returns the validator index only # if only one validator index is set @@ -239,16 +232,20 @@ func get_attesting_indices_one*(shufflingRef: ShufflingRef, func get_attesting_indices_one*(shufflingRef: ShufflingRef, slot: Slot, - committee_indices: AttestationCommitteeBits, - aggregation_bits: ElectraCommitteeValidatorsBits): - Option[ValidatorIndex] = + committee_bits: AttestationCommitteeBits, + aggregation_bits: ElectraCommitteeValidatorsBits, + on_chain: static bool): + Opt[ValidatorIndex] = # A variation on get_attesting_indices that returns the validator index only # if only one validator index is set - var res = none(ValidatorIndex) + static: doAssert not on_chain, "only on_chain supported" + + var res = Opt.none(ValidatorIndex) + let committee_index = ? get_committee_index_one(committee_bits) for validator_index in get_attesting_indices( - shufflingRef, slot, committee_indices, aggregation_bits): - if res.isSome(): return none(ValidatorIndex) - res = some(validator_index) + shufflingRef, slot, committee_index, aggregation_bits): + if res.isSome(): return Opt.none(ValidatorIndex) + res = Opt.some(validator_index) res # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/beacon-chain.md#get_attesting_indices @@ -263,8 +260,11 @@ func get_attesting_indices*(shufflingRef: ShufflingRef, func get_attesting_indices*(shufflingRef: ShufflingRef, slot: Slot, committee_index: CommitteeIndex, - bits: ElectraCommitteeValidatorsBits): + bits: ElectraCommitteeValidatorsBits, + on_chain: static bool): seq[ValidatorIndex] = + static: doAssert not on_chain, "only on_chain supported" + for idx in get_attesting_indices(shufflingRef, slot, committee_index, bits): result.add(idx) diff --git a/beacon_chain/fork_choice/fork_choice.nim b/beacon_chain/fork_choice/fork_choice.nim index 278c47961..d6f3ed94d 100644 --- a/beacon_chain/fork_choice/fork_choice.nim +++ b/beacon_chain/fork_choice/fork_choice.nim @@ -285,7 +285,7 @@ proc process_block*(self: var ForkChoice, for attestation in blck.body.attestations: if attestation.data.beacon_block_root in self.backend: - for validator_index in dag.get_attesting_indices(attestation): + for validator_index in dag.get_attesting_indices(attestation, true): self.backend.process_attestation( validator_index, attestation.data.beacon_block_root, diff --git a/beacon_chain/gossip_processing/block_processor.nim b/beacon_chain/gossip_processing/block_processor.nim index 02bdf617c..082031211 100644 --- a/beacon_chain/gossip_processing/block_processor.nim +++ b/beacon_chain/gossip_processing/block_processor.nim @@ -603,7 +603,7 @@ proc storeBlock( src, wallTime, trustedBlock.message) for attestation in trustedBlock.message.body.attestations: - for validator_index in dag.get_attesting_indices(attestation): + for validator_index in dag.get_attesting_indices(attestation, true): vm[].registerAttestationInBlock(attestation.data, validator_index, trustedBlock.message.slot) diff --git a/beacon_chain/gossip_processing/gossip_validation.nim b/beacon_chain/gossip_processing/gossip_validation.nim index 169b591e7..9021f591f 100644 --- a/beacon_chain/gossip_processing/gossip_validation.nim +++ b/beacon_chain/gossip_processing/gossip_validation.nim @@ -879,7 +879,8 @@ proc validateAttestation*( let fork = pool.dag.forkAtEpoch(attestation.data.slot.epoch) attesting_index = get_attesting_indices_one( - shufflingRef, slot, attestation.committee_bits, attestation.aggregation_bits) + shufflingRef, slot, attestation.committee_bits, + attestation.aggregation_bits, false) # The number of aggregation bits matches the committee size, which ensures # this condition holds. @@ -1158,7 +1159,7 @@ proc validateAggregate*( let fork = pool.dag.forkAtEpoch(aggregate.data.slot.epoch) attesting_indices = get_attesting_indices( - shufflingRef, slot, committee_index, aggregate.aggregation_bits) + shufflingRef, slot, committee_index, aggregate.aggregation_bits, false) let sig = diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index 8cd7a8eaf..2de5843fd 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -596,24 +596,16 @@ iterator get_attesting_indices_iter*( aggregation_bits: ElectraCommitteeValidatorsBits, committee_bits: auto, cache: var StateCache): ValidatorIndex = - debugComment "replace this implementation with actual iterator, after checking on conditions re repeat vals, ordering, etc; this is almost direct transcription of spec link algorithm in one of the places it doesn't make sense" ## Return the set of attesting indices corresponding to ``aggregation_bits`` ## and ``committee_bits``. - var output: HashSet[ValidatorIndex] - let committee_indices = toSeq(committee_bits.oneIndices) - var committee_offset = 0 - for index in committee_indices: - let committee = get_beacon_committee(state, data.slot, index.CommitteeIndex, cache) - var committee_attesters: HashSet[ValidatorIndex] - for i, index in committee: - if aggregation_bits[committee_offset + i]: - committee_attesters.incl index - output.incl committee_attesters + var pos = 0 + for committee_index in get_committee_indices(committee_bits): + for _, validator_index in get_beacon_committee( + state, data.slot, committee_index, cache): - committee_offset += len(committee) - - for validatorIndex in output: - yield validatorIndex + if aggregation_bits[pos]: + yield validator_index + pos += 1 # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/beacon-chain.md#get_attesting_indices func get_attesting_indices*( @@ -886,7 +878,7 @@ func get_base_reward( # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/beacon-chain.md#attestations proc check_attestation*( state: ForkyBeaconState, attestation: SomeAttestation, flags: UpdateFlags, - cache: var StateCache): Result[void, cstring] = + cache: var StateCache, on_chain: static bool = true): 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! @@ -921,7 +913,7 @@ proc check_attestation*( proc check_attestation*( state: electra.BeaconState, attestation: electra.Attestation | electra.TrustedAttestation, - flags: UpdateFlags, cache: var StateCache): Result[void, cstring] = + flags: UpdateFlags, cache: var StateCache, on_chain: static bool): 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! @@ -937,17 +929,26 @@ proc check_attestation*( if not (data.index == 0): return err("Electra attestation data index not 0") - var participants_count = 0 - debugComment "cache doesn't know about forks" - for index in attestation.committee_bits.oneIndices: - if not (index.uint64 < get_committee_count_per_slot( - state, data.target.epoch, cache)): - return err("foo") - let committee = get_beacon_committee(state, data.slot, index.CommitteeIndex, cache) - participants_count += len(committee) + when on_chain: + var participants_count = 0'u64 + debugComment "cache doesn't know about forks" + for index in attestation.committee_bits.oneIndices: + if not (index.uint64 < get_committee_count_per_slot( + state, data.target.epoch, cache)): + return err("attestation wrong committee index len") + participants_count += + get_beacon_committee_len(state, data.slot, index.CommitteeIndex, cache) - if not (len(attestation.aggregation_bits) == participants_count): - return err("") + if not (lenu64(attestation.aggregation_bits) == participants_count): + return err("attestation wrong aggregation bit length") + else: + let + committee_index = get_committee_index_one(attestation.committee_bits).valueOr: + return err("Network attestation without single committee index") + + if not (lenu64(attestation.aggregation_bits) == + get_beacon_committee_len(state, data.slot, committee_index, cache)): + return err("attestation wrong aggregation bit length") if epoch == get_current_epoch(state): if not (data.source == state.current_justified_checkpoint): @@ -1100,7 +1101,7 @@ proc process_attestation*( attestation: electra.Attestation | electra.TrustedAttestation, flags: UpdateFlags, base_reward_per_increment: Gwei, cache: var StateCache): Result[Gwei, cstring] = - ? check_attestation(state, attestation, flags, cache) + ? check_attestation(state, attestation, flags, cache, true) let proposer_index = get_beacon_proposer_index(state, cache).valueOr: return err("process_attestation: no beacon proposer index and probably no active validators") diff --git a/beacon_chain/spec/datatypes/electra.nim b/beacon_chain/spec/datatypes/electra.nim index 461092189..1118bd697 100644 --- a/beacon_chain/spec/datatypes/electra.nim +++ b/beacon_chain/spec/datatypes/electra.nim @@ -731,6 +731,7 @@ iterator getValidatorIndices*(attester_slashing: AttesterSlashing | TrustedAttes func shortLog*(v: electra.Attestation | electra.TrustedAttestation): auto = ( aggregation_bits: v.aggregation_bits, + committee_bits: v.committee_bits, data: shortLog(v.data), signature: shortLog(v.signature) ) diff --git a/beacon_chain/spec/validator.nim b/beacon_chain/spec/validator.nim index c0cf435ce..7e0d31ed1 100644 --- a/beacon_chain/spec/validator.nim +++ b/beacon_chain/spec/validator.nim @@ -8,7 +8,9 @@ # Helpers and functions pertaining to managing the validator set -import ./helpers +import + std/algorithm, + "."/[crypto, helpers] export helpers const @@ -541,3 +543,58 @@ func compute_subscribed_subnet(node_id: UInt256, epoch: Epoch, index: uint64): iterator compute_subscribed_subnets*(node_id: UInt256, epoch: Epoch): SubnetId = for index in 0'u64 ..< SUBNETS_PER_NODE: yield compute_subscribed_subnet(node_id, epoch, index) + +iterator get_committee_indices*(bits: AttestationCommitteeBits): CommitteeIndex = + for index, b in bits: + if b: + yield CommitteeIndex.init(uint64(index)).valueOr: + break # Too many bits! Shouldn't happen + +func get_committee_index_one*(bits: AttestationCommitteeBits): Opt[CommitteeIndex] = + var res = Opt.none(CommitteeIndex) + for committee_index in get_committee_indices(bits): + if res.isSome(): return Opt.none(CommitteeIndex) + res = Opt.some(committee_index) + res + +proc compute_on_chain_aggregate*( + network_aggregates: openArray[electra.Attestation]): Opt[electra.Attestation] = + # aggregates = sorted(network_aggregates, key=lambda a: get_committee_indices(a.committee_bits)[0]) + let aggregates = network_aggregates.sortedByIt(it.committee_bits.get_committee_index_one().expect("just one")) + + let data = aggregates[0].data + + var agg: AggregateSignature + var committee_bits: AttestationCommitteeBits + + var totalLen = 0 + for i, a in aggregates: + totalLen += a.aggregation_bits.len + + var aggregation_bits = ElectraCommitteeValidatorsBits.init(totalLen) + var pos = 0 + for i, a in aggregates: + let + committee_index = ? get_committee_index_one(a.committee_bits) + first = pos == 0 + + for b in a.aggregation_bits: + aggregation_bits[pos] = b + pos += 1 + + let sig = ? a.signature.load() # Expensive + if first: + agg = AggregateSignature.init(sig) + else: + agg.aggregate(sig) + + committee_bits[int(committee_index)] = true + + let signature = agg.finish() + + ok electra.Attestation( + aggregation_bits: aggregation_bits, + data: data, + committee_bits: committee_bits, + signature: signature.toValidatorSig(), + ) diff --git a/beacon_chain/validators/beacon_validators.nim b/beacon_chain/validators/beacon_validators.nim index 573f1662d..31314e9f0 100644 --- a/beacon_chain/validators/beacon_validators.nim +++ b/beacon_chain/validators/beacon_validators.nim @@ -501,7 +501,7 @@ proc makeBeaconBlockForHeadAndSlot*( let attestations = when PayloadType.kind == ConsensusFork.Electra: - default(seq[electra.Attestation]) + node.attestationPool[].getElectraAttestationsForBlock(state[], cache) else: node.attestationPool[].getAttestationsForBlock(state[], cache) exits = withState(state[]): diff --git a/ncli/ncli_common.nim b/ncli/ncli_common.nim index 21e63eefa..cb543ed69 100644 --- a/ncli/ncli_common.nim +++ b/ncli/ncli_common.nim @@ -381,7 +381,7 @@ func collectFromAttestations( doAssert base_reward_per_increment > 0.Gwei for attestation in forkyBlck.message.body.attestations: doAssert check_attestation( - forkyState.data, attestation, {}, cache).isOk + forkyState.data, attestation, {}, cache, true).isOk let proposerReward = if attestation.data.target.epoch == get_current_epoch(forkyState.data): get_proposer_reward( diff --git a/tests/test_attestation_pool.nim b/tests/test_attestation_pool.nim index 4e2e85356..5cc6ceaf6 100644 --- a/tests/test_attestation_pool.nim +++ b/tests/test_attestation_pool.nim @@ -23,7 +23,7 @@ import ../beacon_chain/spec/[beaconstate, helpers, state_transition, validator], ../beacon_chain/beacon_clock, # Test utilities - ./testutil, ./testdbutil, ./testblockutil + ./testutil, ./testdbutil, ./testblockutil, ./consensus_spec/fixtures_utils from std/sequtils import toSeq from ./testbcutil import addHeadBlock @@ -49,6 +49,8 @@ func combine(tgt: var phase0.Attestation, src: phase0.Attestation) = func loadSig(a: phase0.Attestation): CookedSig = a.signature.load.get() +func loadSig(a: electra.Attestation): CookedSig = + a.signature.load.get() proc pruneAtFinalization(dag: ChainDAGRef, attPool: AttestationPool) = if dag.needStateCachesAndForkChoicePruning(): @@ -729,4 +731,127 @@ suite "Attestation pool processing" & preset(): epochRef, blckRef, unrealized, signedBlock.message, blckRef.slot.start_beacon_time) - doAssert: b10Add_clone.error == VerifierError.Duplicate \ No newline at end of file + doAssert: b10Add_clone.error == VerifierError.Duplicate + +suite "Attestation pool electra processing" & preset(): + ## For now just test that we can compile and execute block processing with + ## mock data. + + setup: + # Genesis state that results in 6 members per committee + let rng = HmacDrbgContext.new() + var + validatorMonitor = newClone(ValidatorMonitor.init()) + cfg = genesisTestRuntimeConfig(ConsensusFork.Electra) + dag = init( + ChainDAGRef, cfg, + makeTestDB(SLOTS_PER_EPOCH * 6, cfg = cfg), + validatorMonitor, {}) + taskpool = Taskpool.new() + verifier = BatchVerifier.init(rng, taskpool) + quarantine = newClone(Quarantine.init()) + pool = newClone(AttestationPool.init(dag, quarantine)) + state = newClone(dag.headState) + cache = StateCache() + info = ForkedEpochInfo() + # Slot 0 is a finalized slot - won't be making attestations for it.. + check: + process_slots( + dag.cfg, state[], getStateField(state[], slot) + 1, cache, info, + {}).isOk() + + + test "Can add and retrieve simple electra attestations" & preset(): + let + # Create an attestation for slot 1! + bc0 = get_beacon_committee( + state[], getStateField(state[], slot), 0.CommitteeIndex, cache) + attestation = makeElectraAttestation( + state[], state[].latest_block_root, bc0[0], cache) + + pool[].addAttestation( + attestation, @[bc0[0]], attestation.loadSig, + attestation.data.slot.start_beacon_time) + + check: + process_slots( + defaultRuntimeConfig, state[], + getStateField(state[], slot) + MIN_ATTESTATION_INCLUSION_DELAY, cache, + info, {}).isOk() + + let attestations = pool[].getElectraAttestationsForBlock(state[], cache) + + check: + attestations.len == 1 + + let + root1 = addTestBlock( + state[], cache, electraAttestations = attestations, + nextSlot = false).electraData.root + bc1 = get_beacon_committee( + state[], getStateField(state[], slot), 0.CommitteeIndex, cache) + att1 = makeElectraAttestation(state[], root1, bc1[0], cache) + + check: + withState(state[]): forkyState.latest_block_root == root1 + + process_slots( + defaultRuntimeConfig, state[], + getStateField(state[], slot) + MIN_ATTESTATION_INCLUSION_DELAY, cache, + info, {}).isOk() + + withState(state[]): forkyState.latest_block_root == root1 + + check: + # shouldn't include already-included attestations + pool[].getElectraAttestationsForBlock(state[], cache) == [] + + pool[].addAttestation( + att1, @[bc1[0]], att1.loadSig, att1.data.slot.start_beacon_time) + + check: + # but new ones should go in + pool[].getElectraAttestationsForBlock(state[], cache).len() == 1 + + let + att2 = makeElectraAttestation(state[], root1, bc1[1], cache) + pool[].addAttestation( + att2, @[bc1[1]], att2.loadSig, att2.data.slot.start_beacon_time) + + let + combined = pool[].getElectraAttestationsForBlock(state[], cache) + + check: + # New attestations should be combined with old attestations + combined.len() == 1 + combined[0].aggregation_bits.countOnes() == 2 + + pool[].addAttestation( + combined[0], @[bc1[1], bc1[0]], combined[0].loadSig, + combined[0].data.slot.start_beacon_time) + + check: + # readding the combined attestation shouldn't have an effect + pool[].getElectraAttestationsForBlock(state[], cache).len() == 1 + + let + # Someone votes for a different root + att3 = makeElectraAttestation(state[], ZERO_HASH, bc1[2], cache) + pool[].addAttestation( + att3, @[bc1[2]], att3.loadSig, att3.data.slot.start_beacon_time) + + check: + # We should now get both attestations for the block, but the aggregate + # should be the one with the most votes + pool[].getElectraAttestationsForBlock(state[], cache).len() == 2 + # pool[].getAggregatedAttestation(2.Slot, 0.CommitteeIndex). + # get().aggregation_bits.countOnes() == 2 + # pool[].getAggregatedAttestation(2.Slot, hash_tree_root(att2.data)). + # get().aggregation_bits.countOnes() == 2 + + let + # Someone votes for a different root + att4 = makeElectraAttestation(state[], ZERO_HASH, bc1[2], cache) + pool[].addAttestation( + att4, @[bc1[2]], att3.loadSig, att3.data.slot.start_beacon_time) + diff --git a/tests/testblockutil.nim b/tests/testblockutil.nim index da4f1a29a..894faee58 100644 --- a/tests/testblockutil.nim +++ b/tests/testblockutil.nim @@ -158,6 +158,7 @@ proc addTestBlock*( cache: var StateCache, eth1_data: Eth1Data = Eth1Data(), attestations: seq[phase0.Attestation] = newSeq[phase0.Attestation](), + electraAttestations: seq[electra.Attestation] = newSeq[electra.Attestation](), deposits: seq[Deposit] = newSeq[Deposit](), sync_aggregate: SyncAggregate = SyncAggregate.init(), graffiti: GraffitiBytes = default(GraffitiBytes), @@ -221,7 +222,7 @@ proc addTestBlock*( block_hash: eth1_data.block_hash), graffiti, when consensusFork == ConsensusFork.Electra: - default(seq[electra.Attestation]) + electraAttestations else: attestations, deposits, @@ -248,6 +249,7 @@ proc makeTestBlock*( cache: var StateCache, eth1_data = Eth1Data(), attestations = newSeq[phase0.Attestation](), + electraAttestations = newSeq[electra.Attestation](), deposits = newSeq[Deposit](), sync_aggregate = SyncAggregate.init(), graffiti = default(GraffitiBytes), @@ -259,7 +261,8 @@ proc makeTestBlock*( let tmpState = assignClone(state) addTestBlock( tmpState[], cache, eth1_data, - attestations, deposits, sync_aggregate, graffiti, cfg = cfg) + attestations, electraAttestations, deposits, sync_aggregate, graffiti, + cfg = cfg) func makeAttestationData*( state: ForkyBeaconState, slot: Slot, committee_index: CommitteeIndex, @@ -290,7 +293,7 @@ func makeAttestationData*( func makeAttestationSig( fork: Fork, genesis_validators_root: Eth2Digest, data: AttestationData, committee: openArray[ValidatorIndex], - bits: CommitteeValidatorsBits): ValidatorSig = + bits: CommitteeValidatorsBits | ElectraCommitteeValidatorsBits): ValidatorSig = let signing_root = compute_attestation_signing_root( fork, genesis_validators_root, data) @@ -402,6 +405,78 @@ func makeFullAttestations*( result.add attestation +func makeElectraAttestation( + state: ForkedHashedBeaconState, beacon_block_root: Eth2Digest, + committee: seq[ValidatorIndex], slot: Slot, committee_index: CommitteeIndex, + validator_index: ValidatorIndex, cache: var StateCache, + flags: UpdateFlags = {}): electra.Attestation = + let + index_in_committee = committee.find(validator_index) + data = makeAttestationData(state, slot, CommitteeIndex(0), beacon_block_root) + + doAssert index_in_committee != -1, "find_beacon_committee should guarantee this" + + var aggregation_bits = ElectraCommitteeValidatorsBits.init(committee.len) + aggregation_bits.setBit index_in_committee + + let sig = if skipBlsValidation in flags: + ValidatorSig() + else: + makeAttestationSig( + getStateField(state, fork), + getStateField(state, genesis_validators_root), + data, committee, aggregation_bits) + + var committee_bits: AttestationCommitteeBits + committee_bits[int committee_index] = true + + electra.Attestation( + data: data, + committee_bits: committee_bits, + aggregation_bits: aggregation_bits, + signature: sig + ) + +func makeElectraAttestation*( + state: ForkedHashedBeaconState, beacon_block_root: Eth2Digest, + validator_index: ValidatorIndex, cache: var StateCache): electra.Attestation = + let (committee, slot, index) = + find_beacon_committee(state, validator_index, cache) + makeElectraAttestation(state, beacon_block_root, committee, slot, index, + validator_index, cache) + +func makeFullElectraAttestations*( + state: ForkedHashedBeaconState, beacon_block_root: Eth2Digest, slot: Slot, + cache: var StateCache, + flags: UpdateFlags = {}): seq[electra.Attestation] = + # Create attestations in which the full committee participates for each shard + # that should be attested to during a particular slot + let committees_per_slot = get_committee_count_per_slot( + state, slot.epoch, cache) + for committee_index in get_committee_indices(committees_per_slot): + let + committee = get_beacon_committee(state, slot, committee_index, cache) + data = makeAttestationData(state, slot, CommitteeIndex(0), beacon_block_root) + var + committee_bits: AttestationCommitteeBits + + committee_bits[int committee_index] = true + + doAssert committee.len() >= 1 + var attestation = electra.Attestation( + aggregation_bits: ElectraCommitteeValidatorsBits.init(committee.len), + committee_bits: committee_bits, + data: data) + for i in 0..