electra attestation updates (#6295)

* electra attestation updates

In Electra, we have two attestation formats: on-chain and on-network -
the former combines all committees of a slot in a single committee bit
list.

This PR makes a number of cleanups to move towards fixing this -
attestation packing however still needs to be fixed as it currently
creates attestations with a single committee only which is very
inefficient.

* more attestations in the blocks

* signing and aggregation fixes

* tool fix

* test, import
This commit is contained in:
Jacek Sieka 2024-05-17 14:37:41 +02:00 committed by GitHub
parent 826bf4c3ee
commit 045c4cf185
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 436 additions and 147 deletions

View File

@ -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

View File

@ -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..<entry.aggregates.len():
if bits.isSubsetOf(entry.aggregates[i].aggregation_bits):
@ -342,7 +343,7 @@ func covers(
false
proc addAttestation(
entry: var (Phase0AttestationEntry | ElectraAttestationEntry),
entry: var AttestationEntry,
attestation: phase0.Attestation | electra.Attestation,
signature: CookedSig): bool =
logScope:
@ -374,13 +375,7 @@ proc addAttestation(
entry.aggregates.keepItIf(
not it.aggregation_bits.isSubsetOf(attestation.aggregation_bits))
# If it's not one of the correct ones, compile-time error anyway
when entry is ElectraAttestationEntry:
type ValidationType = ElectraValidation
else:
type ValidationType = Phase0Validation
entry.aggregates.add(ValidationType(
entry.aggregates.add(Validation[typeof(entry).CVBType](
aggregation_bits: attestation.aggregation_bits,
aggregate_signature: AggregateSignature.init(signature)))
@ -431,19 +426,19 @@ proc addAttestation*(
template committee_bits(_: phase0.Attestation): auto =
const res = default(AttestationCommitteeBits)
res
let candidate_key = getAttestationCandidateKey(
attestation.data, attestation.committee_bits)
# TODO withValue is an abomination but hard to use anything else too without
# creating an unnecessary AttestationEntry on the hot path and avoiding
# multiple lookups
template addAttToPool(attCandidates: untyped, entry: untyped) =
attCandidates[candidateIdx.get()].withValue(candidate_key, entry) do:
let attestation_data_root = hash_tree_root(entry.data)
attCandidates[candidateIdx.get()].withValue(attestation_data_root, entry) do:
if not addAttestation(entry[], attestation, signature):
return
do:
if not addAttestation(
attCandidates[candidateIdx.get()].mgetOrPut(candidate_key, entry),
attCandidates[candidateIdx.get()].mgetOrPut(attestation_data_root, entry),
attestation, signature):
# Returns from overall function, not only template
return
@ -461,8 +456,16 @@ proc addAttestation*(
pool.onPhase0AttestationAdded(attestation)
template addAttToPool(_: electra.Attestation) {.used.} =
let
committee_index = get_committee_index_one(attestation.committee_bits).expect("TODO")
data = AttestationData(
slot: attestation.data.slot,
index: uint64 committee_index,
beacon_block_root: attestation.data.beacon_block_root,
source: attestation.data.source,
target: attestation.data.target)
let newAttEntry = ElectraAttestationEntry(
data: attestation.data, committee_bits: attestation.committee_bits,
data: data,
committee_len: attestation.aggregation_bits.len)
addAttToPool(pool.electraCandidates, newAttEntry)
pool.addForkChoiceVotes(
@ -560,7 +563,7 @@ iterator attestations*(
type
AttestationCacheKey = (Slot, uint64)
AttestationCache = Table[AttestationCacheKey, CommitteeValidatorsBits] ##\
AttestationCache[CVBType] = Table[AttestationCacheKey, CVBType] ##\
## Cache for quick lookup during beacon block construction of attestations
## which have already been included, and therefore should be skipped.
@ -571,7 +574,7 @@ func getAttestationCacheKey(ad: AttestationData): AttestationCacheKey =
func add(
attCache: var AttestationCache, data: AttestationData,
aggregation_bits: CommitteeValidatorsBits) =
aggregation_bits: CommitteeValidatorsBits | ElectraCommitteeValidatorsBits) =
let key = data.getAttestationCacheKey()
attCache.withValue(key, v) do:
v[].incl(aggregation_bits)
@ -611,7 +614,7 @@ func init(
let committee = get_beacon_committee(
state.data, slot, committee_index, cache)
var
validator_bits = CommitteeValidatorsBits.init(committee.len)
validator_bits = typeof(result).B.init(committee.len)
for index_in_committee, validator_index in committee:
if participation_bitmap[validator_index] != 0:
# If any flag got set, there was an attestation from this validator.
@ -626,7 +629,7 @@ func init(
func score(
attCache: var AttestationCache, data: AttestationData,
aggregation_bits: CommitteeValidatorsBits): int =
aggregation_bits: CommitteeValidatorsBits | ElectraCommitteeValidatorsBits): int =
# The score of an attestation is loosely based on how many new votes it brings
# to the state - a more accurate score function would also look at inclusion
# distance and effective balance.
@ -635,12 +638,12 @@ func score(
key = data.getAttestationCacheKey()
bitsScore = aggregation_bits.countOnes()
attCache.withValue(key, value):
doAssert aggregation_bits.len() == value[].len(),
attCache.withValue(key, xxx):
doAssert aggregation_bits.len() == xxx[].len(),
"check_attestation ensures committee length"
# How many votes were in the attestation minues the votes that are the same
return bitsScore - aggregation_bits.countOverlap(value[])
return bitsScore - aggregation_bits.countOverlap(xxx[])
# Not found in cache - fresh vote meaning all attestations count
bitsScore
@ -648,7 +651,7 @@ func score(
proc check_attestation_compatible*(
dag: ChainDAGRef,
state: ForkyHashedBeaconState,
attestation: SomeAttestation): Result[void, cstring] =
attestation: SomeAttestation | electra.Attestation | electra.TrustedAttestation): Result[void, cstring] =
let
targetEpoch = attestation.data.target.epoch
compatibleRoot = state.dependent_root(targetEpoch.get_previous_epoch)
@ -685,7 +688,7 @@ proc getAttestationsForBlock*(pool: var AttestationPool,
candidates: seq[tuple[
score: int, slot: Slot, entry: ptr Phase0AttestationEntry,
validation: int]]
attCache = AttestationCache.init(state, cache)
attCache = AttestationCache[CommitteeValidatorsBits].init(state, cache)
for i in 0..<ATTESTATION_LOOKBACK:
if i > 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..<ATTESTATION_LOOKBACK:
if i > 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..<entry.aggregates.len():
let attestation = entry.toElectraAttestation(entry.aggregates[j])
# Filter out attestations that were created with a different shuffling.
# As we don't re-check signatures, this needs to be done separately
#if not pool.dag.check_attestation_compatible(state, attestation).isOk():
# continue
if not pool.dag.check_attestation_compatible(state, attestation).isOk():
continue
# Attestations are checked based on the state that we're adding the
# attestation to - there might have been a fork between when we first
# saw the attestation and the time that we added it
if not check_attestation(
state.data, attestation, {skipBlsValidation}, cache).isOk():
state.data, attestation, {skipBlsValidation}, cache, false).isOk():
continue
let score = 1
let score = attCache.score(
entry.data, entry.aggregates[j].aggregation_bits)
if score == 0:
# 0 score means the attestation would not bring any votes - discard
# it early
@ -897,24 +899,22 @@ proc getElectraAttestationsForBlock*(
# then re-score the other candidates.
var
prevEpoch = state.data.get_previous_epoch()
prevEpochSpace =
when not (state is phase0.HashedBeaconState):
MAX_ATTESTATIONS_ELECTRA
else:
state.data.previous_epoch_attestations.maxLen -
state.data.previous_epoch_attestations.len()
prevEpochSpace = MAX_ATTESTATIONS_ELECTRA
var res: seq[electra.Attestation]
let totalCandidates = candidates.len()
while candidates.len > 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,

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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 =

View File

@ -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")

View File

@ -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)
)

View File

@ -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(),
)

View File

@ -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[]):

View File

@ -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(

View File

@ -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
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)

View File

@ -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..<committee.len:
attestation.aggregation_bits.setBit(i)
attestation.signature = makeAttestationSig(
getStateField(state, fork),
getStateField(state, genesis_validators_root), data, committee,
attestation.aggregation_bits)
result.add attestation
proc makeSyncAggregate(
state: ForkedHashedBeaconState,
syncCommitteeRatio: float,

View File

@ -27,8 +27,10 @@ proc makeTestDB*(
cfg = defaultRuntimeConfig): BeaconChainDB =
# Blob support requires DENEB_FORK_EPOCH != FAR_FUTURE_EPOCH
var cfg = cfg
cfg.CAPELLA_FORK_EPOCH = 90000.Epoch
cfg.DENEB_FORK_EPOCH = 100000.Epoch
if cfg.CAPELLA_FORK_EPOCH == FAR_FUTURE_EPOCH:
cfg.CAPELLA_FORK_EPOCH = 90000.Epoch
if cfg.DENEB_FORK_EPOCH == FAR_FUTURE_EPOCH:
cfg.DENEB_FORK_EPOCH = 100000.Epoch
var genState = (ref ForkedHashedBeaconState)(
kind: ConsensusFork.Phase0,