Skip to content

Commit 157ddd2

Browse files
authored
Fork choice fixes 5 (#1381)
* limit attestations kept in attestation pool With fork choice updated, the attestation pool only needs to keep track of attestations that will eventually end up in blocks - we can thus limit the horizon of attestations that we keep more aggressively. To get here, we expose getEpochRef which gets metadata about a particular epochref, and make sure to populate it when a block is added - this ensures that state rewinds during block addition are minimized. In addition, we'll use the target root/epoch when validating attestations - this helps minimize the number of different states that we need to rewind to, in general. * remove CandidateChains.justifiedState unused * remove BlockPools.Head object * avoid quadratic quarantine loop * fix
1 parent 6ccfff7 commit 157ddd2

20 files changed

+303
-366
lines changed

beacon_chain/attestation_aggregation.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ proc isValidAttestation*(
161161
# therefore propagate faster, thus reordering their arrival in some nodes
162162
let attestationBlck = pool.blockPool.getRef(attestation.data.beacon_block_root)
163163
if attestationBlck.isNil:
164-
debug "block doesn't exist in block pool"
164+
debug "Block not found"
165165
pool.blockPool.addMissing(attestation.data.beacon_block_root)
166166
return false
167167

beacon_chain/attestation_pool.nim

Lines changed: 50 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99

1010
import
1111
# Standard libraries
12-
std/[algorithm, deques, sequtils, tables, options],
12+
std/[algorithm, deques, sequtils, sets, tables, options],
1313
# Status libraries
14-
chronicles, stew/[byteutils], json_serialization/std/sets,
14+
chronicles, stew/[byteutils], json_serialization/std/sets as jsonSets,
1515
# Internal
1616
./spec/[beaconstate, datatypes, crypto, digest, helpers],
1717
./block_pool, ./block_pools/candidate_chains, ./beacon_node_types,
1818
./fork_choice/fork_choice
1919

20-
export beacon_node_types
20+
export beacon_node_types, sets
2121

2222
logScope: topics = "attpool"
2323

@@ -39,7 +39,7 @@ proc init*(T: type AttestationPool, blockPool: BlockPool): T =
3939
doAssert blockPool.heads.len == 1, "Init only supports a single history"
4040

4141
var blocks: seq[BlockRef]
42-
var cur = blockPool.head.blck
42+
var cur = blockPool.head
4343
while cur != blockPool.finalizedHead.blck:
4444
blocks.add cur
4545
cur = cur.parent
@@ -65,65 +65,11 @@ proc init*(T: type AttestationPool, blockPool: BlockPool): T =
6565
finalized_root = shortlog(blockPool.finalizedHead.blck.root)
6666

6767
T(
68-
mapSlotsToAttestations: initDeque[AttestationsSeen](),
6968
blockPool: blockPool,
7069
unresolved: initTable[Eth2Digest, UnresolvedAttestation](),
7170
forkChoice: forkChoice
7271
)
7372
74-
proc slotIndex(
75-
pool: var AttestationPool, state: BeaconState, attestationSlot: Slot): int =
76-
## Grow and garbage collect pool, returning the deque index of the slot
77-
78-
# We keep a sliding window of attestations, roughly from the last finalized
79-
# epoch to now, because these are the attestations that may affect the voting
80-
# outcome. Some of these attestations will already have been added to blocks,
81-
# while others are fresh off the network.
82-
# TODO only the latest vote of each validator counts. Can we use that somehow?
83-
logScope: pcs = "atp_slot_maintenance"
84-
85-
doAssert attestationSlot >= pool.startingSlot,
86-
"""
87-
We should have checked in addResolved that attestation is newer than
88-
finalized_slot and we never prune things before that, per below condition!
89-
""" &
90-
", attestationSlot: " & $shortLog(attestationSlot) &
91-
", startingSlot: " & $shortLog(pool.startingSlot)
92-
93-
if pool.mapSlotsToAttestations.len == 0:
94-
# Because the first attestations may arrive in any order, we'll make sure
95-
# to start counting at the last finalized epoch start slot - anything
96-
# earlier than that is thrown out by the above check
97-
info "First attestation!",
98-
attestationSlot = shortLog(attestationSlot)
99-
pool.startingSlot =
100-
state.finalized_checkpoint.epoch.compute_start_slot_at_epoch()
101-
102-
if pool.startingSlot + pool.mapSlotsToAttestations.lenu64 <= attestationSlot:
103-
trace "Growing attestation pool",
104-
attestationSlot = shortLog(attestationSlot),
105-
startingSlot = shortLog(pool.startingSlot)
106-
107-
# Make sure there's a pool entry for every slot, even when there's a gap
108-
while pool.startingSlot + pool.mapSlotsToAttestations.lenu64 <= attestationSlot:
109-
pool.mapSlotsToAttestations.addLast(AttestationsSeen())
110-
111-
if pool.startingSlot <
112-
state.finalized_checkpoint.epoch.compute_start_slot_at_epoch():
113-
debug "Pruning attestation pool",
114-
startingSlot = shortLog(pool.startingSlot),
115-
finalizedSlot = shortLog(
116-
state.finalized_checkpoint
117-
.epoch.compute_start_slot_at_epoch())
118-
119-
# TODO there should be a better way to remove a whole epoch of stuff..
120-
while pool.startingSlot <
121-
state.finalized_checkpoint.epoch.compute_start_slot_at_epoch():
122-
pool.mapSlotsToAttestations.popFirst()
123-
pool.startingSlot += 1
124-
125-
int(attestationSlot - pool.startingSlot)
126-
12773
func processAttestation(
12874
pool: var AttestationPool, participants: HashSet[ValidatorIndex],
12975
block_root: Eth2Digest, target_epoch: Epoch) =
@@ -137,61 +83,56 @@ func addUnresolved(pool: var AttestationPool, attestation: Attestation) =
13783
attestation: attestation,
13884
)
13985
140-
proc addResolved(pool: var AttestationPool, blck: BlockRef, attestation: Attestation) =
141-
logScope:
142-
attestation = shortLog(attestation)
143-
144-
doAssert blck.root == attestation.data.beacon_block_root
145-
146-
# TODO Which state should we use to validate the attestation? It seems
147-
# reasonable to involve the head being voted for as well as the intended
148-
# slot of the attestation - double-check this with spec
86+
func candidateIdx(pool: AttestationPool, slot: Slot): Option[uint64] =
87+
if slot >= pool.startingSlot and
88+
slot < (pool.startingSlot + pool.candidates.lenu64):
89+
some(slot mod pool.candidates.lenu64)
90+
else:
91+
none(uint64)
14992
150-
# TODO: filter valid attestation as much as possible before state rewind
151-
# TODO: the below check does not respect the inclusion delay
152-
# we should use isValidAttestationSlot instead
153-
if blck.slot > attestation.data.slot:
154-
notice "Invalid attestation (too new!)",
155-
blockSlot = shortLog(blck.slot)
93+
proc updateCurrent(pool: var AttestationPool, wallSlot: Slot) =
94+
if wallSlot + 1 < pool.candidates.lenu64:
15695
return
15796
158-
if attestation.data.slot < pool.startingSlot:
159-
# It can happen that attestations in blocks for example are included even
160-
# though they no longer are relevant for finalization - let's clear
161-
# these out
162-
debug "Old attestation",
163-
startingSlot = pool.startingSlot
97+
if pool.startingSlot + pool.candidates.lenu64 - 1 > wallSlot:
98+
error "Current slot older than attestation pool view, clock reset?",
99+
poolSlot = pool.startingSlot, wallSlot
164100
return
165101
166-
# if not isValidAttestationSlot(attestation.data.slot, blck.slot):
167-
# # Logging in isValidAttestationSlot
168-
# return
102+
# As time passes we'll clear out any old attestations as they are no longer
103+
# viable to be included in blocks
169104
170-
# Check that the attestation is indeed valid
171-
if (let v = check_attestation_slot_target(attestation.data); v.isErr):
172-
debug "Invalid attestation", err = v.error
173-
return
105+
let newWallSlot = wallSlot + 1 - pool.candidates.lenu64
106+
for i in pool.startingSlot..newWallSlot:
107+
pool.candidates[i.uint64 mod pool.candidates.lenu64] = AttestationsSeen()
174108

175-
# Get a temporary state at the (block, slot) targeted by the attestation
176-
updateStateData(
177-
pool.blockPool, pool.blockPool.tmpState,
178-
BlockSlot(blck: blck, slot: attestation.data.slot),
179-
true)
109+
pool.startingSlot = newWallSlot
180110

181-
template state(): BeaconState = pool.blockPool.tmpState.data.data
111+
proc addResolved(
112+
pool: var AttestationPool, blck: BlockRef, attestation: Attestation,
113+
wallSlot: Slot) =
114+
# Add an attestation whose parent we know
115+
logScope:
116+
attestation = shortLog(attestation)
182117

183-
# TODO inefficient data structures..
118+
updateCurrent(pool, wallSlot)
119+
120+
doAssert blck.root == attestation.data.beacon_block_root
121+
122+
let candidateIdx = pool.candidateIdx(attestation.data.slot)
123+
if candidateIdx.isNone:
124+
debug "Attestation slot out of range",
125+
startingSlot = pool.startingSlot
126+
return
184127

185-
var cache = getEpochCache(blck, state)
186128
let
187-
attestationSlot = attestation.data.slot
188-
idx = pool.slotIndex(state, attestationSlot)
189-
attestationsSeen = addr pool.mapSlotsToAttestations[idx]
129+
epochRef = pool.blockPool.dag.getEpochRef(blck, attestation.data.target.epoch)
130+
attestationsSeen = addr pool.candidates[candidateIdx.get]
190131
validation = Validation(
191132
aggregation_bits: attestation.aggregation_bits,
192133
aggregate_signature: attestation.signature)
193134
participants = get_attesting_indices(
194-
state, attestation.data, validation.aggregation_bits, cache)
135+
epochRef, attestation.data, validation.aggregation_bits)
195136

196137
var found = false
197138
for a in attestationsSeen.attestations.mitems():
@@ -226,7 +167,6 @@ proc addResolved(pool: var AttestationPool, blck: BlockRef, attestation: Attesta
226167
info "Attestation resolved",
227168
attestation = shortLog(attestation),
228169
validations = a.validations.len(),
229-
current_epoch = get_current_epoch(state),
230170
blockSlot = shortLog(blck.slot)
231171

232172
found = true
@@ -244,11 +184,12 @@ proc addResolved(pool: var AttestationPool, blck: BlockRef, attestation: Attesta
244184

245185
info "Attestation resolved",
246186
attestation = shortLog(attestation),
247-
current_epoch = get_current_epoch(state),
248187
validations = 1,
249188
blockSlot = shortLog(blck.slot)
250189

251-
proc addAttestation*(pool: var AttestationPool, attestation: Attestation) =
190+
proc addAttestation*(pool: var AttestationPool,
191+
attestation: Attestation,
192+
wallSlot: Slot) =
252193
## Add a verified attestation to the fork choice context
253194
logScope: pcs = "atp_add_attestation"
254195

@@ -261,15 +202,14 @@ proc addAttestation*(pool: var AttestationPool, attestation: Attestation) =
261202
pool.addUnresolved(attestation)
262203
return
263204

264-
pool.addResolved(blck, attestation)
205+
pool.addResolved(blck, attestation, wallSlot)
265206

266207
proc addForkChoice*(pool: var AttestationPool,
267208
state: BeaconState,
268209
blckRef: BlockRef,
269210
blck: BeaconBlock,
270211
wallSlot: Slot) =
271212
## Add a verified block to the fork choice context
272-
## The current justifiedState of the block pool is used as reference
273213
let state = pool.forkChoice.process_block(
274214
pool.blockPool, state, blckRef, blck, wallSlot)
275215

@@ -288,29 +228,17 @@ proc getAttestationsForSlot*(pool: AttestationPool, newBlockSlot: Slot):
288228
newBlockSlot = shortLog(newBlockSlot)
289229
return none(AttestationsSeen)
290230

291-
if pool.mapSlotsToAttestations.len == 0: # startingSlot not set yet!
292-
info "No attestations found (pool empty)",
293-
newBlockSlot = shortLog(newBlockSlot)
294-
return none(AttestationsSeen)
295-
296231
let
297-
# TODO in theory we could include attestations from other slots also, but
298-
# we're currently not tracking which attestations have already been included
299-
# in blocks on the fork we're aiming for.. this is a conservative approach
300-
# that's guaranteed to not include any duplicates, because it's the first
301-
# time the attestations are up for inclusion!
302232
attestationSlot = newBlockSlot - MIN_ATTESTATION_INCLUSION_DELAY
233+
candidateIdx = pool.candidateIdx(attestationSlot)
303234

304-
if attestationSlot < pool.startingSlot or
305-
attestationSlot >= pool.startingSlot + pool.mapSlotsToAttestations.lenu64:
235+
if candidateIdx.isNone:
306236
info "No attestations matching the slot range",
307237
attestationSlot = shortLog(attestationSlot),
308-
startingSlot = shortLog(pool.startingSlot),
309-
endingSlot = shortLog(pool.startingSlot + pool.mapSlotsToAttestations.lenu64)
238+
startingSlot = shortLog(pool.startingSlot)
310239
return none(AttestationsSeen)
311240

312-
let slotDequeIdx = int(attestationSlot - pool.startingSlot)
313-
some(pool.mapSlotsToAttestations[slotDequeIdx])
241+
some(pool.candidates[candidateIdx.get()])
314242

315243
proc getAttestationsForBlock*(pool: AttestationPool,
316244
state: BeaconState): seq[Attestation] =
@@ -337,8 +265,7 @@ proc getAttestationsForBlock*(pool: AttestationPool,
337265
# addResolved, too, the new attestations get added to the end, while in
338266
# these functions, it's reading from the beginning, et cetera. This all
339267
# needs a single unified strategy.
340-
const LOOKBACK_WINDOW = 3
341-
for i in max(1, newBlockSlot.int64 - LOOKBACK_WINDOW) .. newBlockSlot.int64:
268+
for i in max(1, newBlockSlot.int64 - ATTESTATION_LOOKBACK.int64) .. newBlockSlot.int64:
342269
let maybeSlotData = getAttestationsForSlot(pool, i.Slot)
343270
if maybeSlotData.isSome:
344271
insert(attestations, maybeSlotData.get.attestations)
@@ -390,7 +317,7 @@ proc getAttestationsForBlock*(pool: AttestationPool,
390317
attestationSlot = newBlockSlot - 1
391318
return
392319

393-
proc resolve*(pool: var AttestationPool) =
320+
proc resolve*(pool: var AttestationPool, wallSlot: Slot) =
394321
## Check attestations in our unresolved deque
395322
## if they can be integrated to the fork choice
396323
logScope: pcs = "atp_resolve"
@@ -412,7 +339,7 @@ proc resolve*(pool: var AttestationPool) =
412339
pool.unresolved.del(k)
413340

414341
for a in resolved:
415-
pool.addResolved(a.blck, a.attestation)
342+
pool.addResolved(a.blck, a.attestation, wallSlot)
416343

417344
proc selectHead*(pool: var AttestationPool, wallSlot: Slot): BlockRef =
418345
let newHead = pool.forkChoice.find_head(wallSlot, pool.blockPool)

0 commit comments

Comments
 (0)