allow `getBlockIdAtSlot` to answer queries from available states (#5869)

After checkpoint sync, historical block IDs cannot yet be queried.
However, they are needed to compute dependent roots of `ShufflingRef`.
To allow lookup, enable `getBlockIdAtSlot` to answer from compatible
states in memory; as long as they descend from the finalized checkpoint
and the requested slot is sufficiently recent, `block_roots` contains
everything to recover `BlockSlotId` up to `SLOTS_PER_HISTORICAL_ROOT`.
This is similar to how `attester_dependent_root` etc. are computed.

This accelerates the first couple minutes of checkpoint sync on Mainnet,
especially the time until finality advances past the synced checkpoint.
This commit is contained in:
Etan Kissling 2024-02-09 11:13:00 +01:00 committed by GitHub
parent 91cf50a5ad
commit 4266e16835
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 192 additions and 12 deletions

View File

@ -814,6 +814,11 @@ OK: 2/2 Fail: 0/2 Skip: 0/2
+ Starting state without block OK
```
OK: 1/1 Fail: 0/1 Skip: 0/1
## State history
```diff
+ getBlockIdAtSlot OK
```
OK: 1/1 Fail: 0/1 Skip: 0/1
## Sync committee pool
```diff
+ Aggregating votes OK
@ -986,4 +991,4 @@ OK: 2/2 Fail: 0/2 Skip: 0/2
OK: 9/9 Fail: 0/9 Skip: 0/9
---TOTAL---
OK: 667/672 Fail: 0/672 Skip: 5/672
OK: 668/673 Fail: 0/673 Skip: 5/673

View File

@ -173,6 +173,36 @@ func getBlockRef*(dag: ChainDAGRef, root: Eth2Digest): Opt[BlockRef] =
else:
err()
func getBlockIdAtSlot*(
state: ForkyHashedBeaconState, slot: Slot): Opt[BlockSlotId] =
## Use given state to attempt to find a historical `BlockSlotId`.
if slot > state.data.slot:
return Opt.none(BlockSlotId) # State does not know about requested slot
if state.data.slot > slot + SLOTS_PER_HISTORICAL_ROOT:
return Opt.none(BlockSlotId) # Cache has expired
var idx = slot mod SLOTS_PER_HISTORICAL_ROOT
let root =
if slot == state.data.slot:
state.latest_block_root
else:
state.data.block_roots[idx]
var bid = BlockId(slot: slot, root: root)
let availableSlots =
min(slot.uint64, slot + SLOTS_PER_HISTORICAL_ROOT - state.data.slot)
for i in 0 ..< availableSlots:
if idx == 0:
idx = SLOTS_PER_HISTORICAL_ROOT
dec idx
if state.data.block_roots[idx] != root:
return Opt.some BlockSlotId.init(bid, slot)
dec bid.slot
if bid.slot == GENESIS_SLOT:
return Opt.some BlockSlotId.init(bid, slot)
Opt.none(BlockSlotId) # Unknown if there are more empty slots before
func getBlockIdAtSlot*(dag: ChainDAGRef, slot: Slot): Opt[BlockSlotId] =
## Retrieve the canonical block at the given slot, or the last block that
## comes before - similar to atSlot, but without the linear scan - may hit
@ -188,6 +218,24 @@ func getBlockIdAtSlot*(dag: ChainDAGRef, slot: Slot): Opt[BlockSlotId] =
# finalized head is still in memory
return dag.finalizedHead.blck.atSlot(slot).toBlockSlotId()
# Load from memory, if the block ID is sufficiently recent.
# For checkpoint sync, this is the only available of historical block IDs
# until sufficient blocks have been backfilled.
template tryWithState(state: ForkedHashedBeaconState) =
block:
withState(state):
# State must be a descendent of the finalized chain to be viable
let finBsi = forkyState.getBlockIdAtSlot(dag.finalizedHead.slot)
if finBsi.isSome and # DAG finalized bid slot wrong if CP not @ epoch
finBsi.unsafeGet.bid.root == dag.finalizedHead.blck.bid.root:
let bsi = forkyState.getBlockIdAtSlot(slot)
if bsi.isSome:
return bsi
tryWithState dag.headState
tryWithState dag.epochRefState
tryWithState dag.clearanceState
# Fallback to database, this only works for backfilled blocks
let finlow = dag.db.finalizedBlocks.low.expect("at least tailRef written")
if slot >= finlow:
var pos = slot

View File

@ -839,10 +839,11 @@ suite "Backfill":
dag.getBlockId(blocks[^2].root).isNone()
dag.getBlockIdAtSlot(dag.tail.slot).get().bid == dag.tail
dag.getBlockIdAtSlot(dag.tail.slot - 1).isNone()
dag.getBlockIdAtSlot(dag.tail.slot - 1).get().bid ==
blocks[^2].toBlockId() # recovered from tailState
dag.getBlockIdAtSlot(Slot(0)).isSome() # genesis stored in db
dag.getBlockIdAtSlot(Slot(1)).isNone()
dag.getBlockIdAtSlot(Slot(0)).isSome() # genesis stored in db
dag.getBlockIdAtSlot(Slot(1)).isSome() # recovered from tailState
# No EpochRef for pre-tail epochs
dag.getEpochRef(dag.tail, dag.tail.slot.epoch - 1, true).isErr()
@ -853,7 +854,7 @@ suite "Backfill":
# Should not get EpochRef for random block
dag.getEpochRef(
BlockId(root: blocks[^2].root, slot: dag.tail.slot), # root/slot mismatch
BlockId(root: blocks[^2].root, slot: dag.tail.slot), # incorrect slot
dag.tail.slot.epoch, true).isErr()
dag.getEpochRef(dag.tail, dag.tail.slot.epoch + 1, true).isOk()
@ -892,7 +893,8 @@ suite "Backfill":
dag.getBlockIdAtSlot(dag.tail.slot).get().bid == dag.tail
dag.getBlockIdAtSlot(dag.tail.slot - 1).get() ==
blocks[^2].toBlockId().atSlot()
dag.getBlockIdAtSlot(dag.tail.slot - 2).isNone
dag.getBlockIdAtSlot(dag.tail.slot - 2).get() ==
blocks[^3].toBlockId().atSlot() # recovered from tailState
dag.backfill == blocks[^2].phase0Data.message.toBeaconBlockSummary()
@ -901,7 +903,8 @@ suite "Backfill":
dag.getBlockIdAtSlot(dag.tail.slot - 2).get() ==
blocks[^3].toBlockId().atSlot()
dag.getBlockIdAtSlot(dag.tail.slot - 3).isNone
dag.getBlockIdAtSlot(dag.tail.slot - 3).get() ==
blocks[^4].toBlockId().atSlot() # recovered from tailState
for i in 3..<blocks.len:
check: dag.addBackfillBlock(blocks[blocks.len - i - 1].phase0Data).isOk()
@ -947,7 +950,8 @@ suite "Backfill":
dag2.getBlockIdAtSlot(dag.tail.slot - 1).get() ==
blocks[^2].toBlockId().atSlot()
dag2.getBlockIdAtSlot(dag.tail.slot - 2).isNone
dag2.getBlockIdAtSlot(dag.tail.slot - 2).get() ==
blocks[^3].toBlockId().atSlot() # recovered from tailState
dag2.backfill == blocks[^2].phase0Data.message.toBeaconBlockSummary()
test "Init without genesis / block":
@ -1039,8 +1043,8 @@ suite "Starting states":
dag.getBlockId(tailBlock.root).get() == dag.tail
dag.getBlockId(blocks[^2].root).isNone()
dag.getBlockIdAtSlot(Slot(0)).isNone() # no genesis stored in db
dag.getBlockIdAtSlot(Slot(1)).isNone()
dag.getBlockIdAtSlot(Slot(0)).isSome() # recovered from tailState
dag.getBlockIdAtSlot(Slot(1)).isSome() # recovered from tailState
# Should get EpochRef for the tail however
# dag.getEpochRef(dag.tail, dag.tail.slot.epoch, true).isOk()
@ -1048,7 +1052,7 @@ suite "Starting states":
# Should not get EpochRef for random block
dag.getEpochRef(
BlockId(root: blocks[^2].root, slot: dag.tail.slot), # root/slot mismatch
BlockId(root: blocks[^2].root, slot: dag.tail.slot), # incorrect slot
dag.tail.slot.epoch, true).isErr()
dag.getEpochRef(dag.tail, dag.tail.slot.epoch + 1, true).isOk()
@ -1092,7 +1096,8 @@ suite "Starting states":
dag.getBlockIdAtSlot(dag.tail.slot - 2).get() ==
blocks[^3].toBlockId().atSlot()
dag.getBlockIdAtSlot(dag.tail.slot - 3).isNone
dag.getBlockIdAtSlot(dag.tail.slot - 3).get() ==
blocks[^4].toBlockId().atSlot() # recovered from tailState
for i in 3..<blocks.len:
check: dag.addBackfillBlock(blocks[blocks.len - i - 1].phase0Data).isOk()
@ -1230,6 +1235,128 @@ suite "Pruning":
dag.tail.slot == Epoch(EPOCHS_PER_STATE_SNAPSHOT).start_slot - 1
not db.containsBlock(blocks[1].root)
suite "State history":
test "getBlockIdAtSlot":
const numValidators = SLOTS_PER_EPOCH
let
cfg = defaultRuntimeConfig
validatorMonitor = newClone(ValidatorMonitor.init())
dag = ChainDAGRef.init(
cfg, makeTestDB(numValidators, cfg = cfg),
validatorMonitor, {})
quarantine = newClone(Quarantine.init())
rng = HmacDrbgContext.new()
taskpool = Taskpool.new()
var verifier = BatchVerifier.init(rng, taskpool)
var
cache: StateCache
info: ForkedEpochInfo
res: Result[void, cstring]
template state: untyped = dag.headState.phase0Data
let gen = get_initial_beacon_block(dag.headState).toBlockId()
check:
state.getBlockIdAtSlot(0.Slot) ==
Opt.some BlockSlotId.init(gen, 0.Slot)
state.getBlockIdAtSlot(1.Slot).isNone
# Miss 5 slots
res = process_slots(cfg, dag.headState, 5.Slot, cache, info, flags = {})
check res.isOk
for i in 0.Slot .. 5.Slot:
check state.getBlockIdAtSlot(i) ==
Opt.some BlockSlotId.init(gen, i.Slot)
check state.getBlockIdAtSlot(6.Slot).isNone
# Fill 5 slots
var bids: seq[BlockId]
for i in 0 ..< 5:
let blck = dag.headState.addTestBlock(cache, cfg = cfg)
bids.add blck.toBlockId()
let added = dag.addHeadBlock(verifier, blck.phase0Data, nilPhase0Callback)
check added.isOk()
dag.updateHead(added[], quarantine[], [])
for i in 0.Slot .. 5.Slot:
check state.getBlockIdAtSlot(i) ==
Opt.some BlockSlotId.init(gen, i)
for i in 6.Slot .. 10.Slot:
check state.getBlockIdAtSlot(i) ==
Opt.some BlockSlotId.init(bids[(i - 6).int], i)
check state.getBlockIdAtSlot(11.Slot).isNone
# Jump to SLOTS_PER_HISTORICAL_ROOT
let periodSlot = SLOTS_PER_HISTORICAL_ROOT.Slot
res = process_slots(cfg, dag.headState, periodSlot, cache, info, flags = {})
for i in 0.Slot .. 5.Slot:
check state.getBlockIdAtSlot(i) ==
Opt.some BlockSlotId.init(gen, i)
for i in 6.Slot .. 10.Slot:
check state.getBlockIdAtSlot(i) ==
Opt.some BlockSlotId.init(bids[(i - 6).int], i)
check:
state.getBlockIdAtSlot(11.Slot) ==
Opt.some BlockSlotId.init(bids[^1], 11.Slot)
state.getBlockIdAtSlot(periodSlot) ==
Opt.some BlockSlotId.init(bids[^1], periodSlot)
state.getBlockIdAtSlot(periodSlot + 1).isNone
# Create a block at periodSlot + 1
let
blck = dag.headState.addTestBlock(cache, cfg = cfg)
added = dag.addHeadBlock(verifier, blck.phase0Data, nilPhase0Callback)
check added.isOk()
dag.updateHead(added[], quarantine[], [])
for i in 0.Slot .. 5.Slot:
check state.getBlockIdAtSlot(i).isNone
for i in 6.Slot .. 10.Slot:
check state.getBlockIdAtSlot(i) ==
Opt.some BlockSlotId.init(bids[(i - 6).int], i)
check:
state.getBlockIdAtSlot(11.Slot) ==
Opt.some BlockSlotId.init(bids[^1], 11.Slot)
state.getBlockIdAtSlot(periodSlot) ==
Opt.some BlockSlotId.init(bids[^1], periodSlot)
state.getBlockIdAtSlot(periodSlot + 1) ==
Opt.some BlockSlotId.init(blck.toBlockId(), periodSlot + 1)
state.getBlockIdAtSlot(periodSlot + 2).isNone
# Go to periodSlot + 5
let plusFive = periodSlot + 5
res = process_slots(cfg, dag.headState, plusFive, cache, info, flags = {})
for i in 0.Slot .. 5.Slot:
check state.getBlockIdAtSlot(i).isNone
for i in 6.Slot .. 10.Slot:
check state.getBlockIdAtSlot(i) ==
Opt.some BlockSlotId.init(bids[(i - 6).int], i)
check:
state.getBlockIdAtSlot(11.Slot) ==
Opt.some BlockSlotId.init(bids[^1], 11.Slot)
state.getBlockIdAtSlot(periodSlot) ==
Opt.some BlockSlotId.init(bids[^1], periodSlot)
for i in periodSlot + 1 .. plusFive:
check state.getBlockIdAtSlot(i) ==
Opt.some BlockSlotId.init(blck.toBlockId(), i)
check state.getBlockIdAtSlot(plusFive + 1).isNone
# Go to periodSlot + 6
let plusSix = periodSlot + 6
res = process_slots(cfg, dag.headState, plusSix, cache, info, flags = {})
for i in 0.Slot .. 6.Slot:
check state.getBlockIdAtSlot(i).isNone
for i in 7.Slot .. 10.Slot:
check state.getBlockIdAtSlot(i) ==
Opt.some BlockSlotId.init(bids[(i - 6).int], i)
check:
state.getBlockIdAtSlot(11.Slot) ==
Opt.some BlockSlotId.init(bids[^1], 11.Slot)
state.getBlockIdAtSlot(periodSlot) ==
Opt.some BlockSlotId.init(bids[^1], periodSlot)
for i in periodSlot + 1 .. plusSix:
check state.getBlockIdAtSlot(i) ==
Opt.some BlockSlotId.init(blck.toBlockId(), i)
check state.getBlockIdAtSlot(plusSix + 1).isNone
suite "Ancestry":
test "ancestorSlot":
const numValidators = SLOTS_PER_EPOCH