simplify RANDAO recovery in `ShufflingRef` acceleration (#5183)

Current RANDAO recovery logic is quite complex as it optimizes for the
minimum amount of database reads. Loading blocks isn't the bottleneck
though, so rather make the implementation more concise by avoiding the
complex strategy planning step. Note that this also prepares for an even
faster implementation for post-merge blocks in the future that extracts
RANDAO from `ExecutionPayload` directly if available, so even in cases
where efficiency is slightly lower, only historical data is affected.

`time nim c -r tests/test_blockchain_dag` (cached binary):

- new: 145.45s, 133.59s, 144.65s, 127.69s, 136.14s
- old: 149.15s, 150.84s, 135.77s, 137.49s, 133.89s
This commit is contained in:
Etan Kissling 2023-07-12 17:27:05 +02:00 committed by GitHub
parent 0660ffcd3e
commit 74bb4b1411
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 44 additions and 108 deletions

View File

@ -1330,7 +1330,7 @@ proc getFinalizedEpochRef*(dag: ChainDAGRef): EpochRef =
dag.finalizedHead.blck, dag.finalizedHead.slot.epoch, false).expect( dag.finalizedHead.blck, dag.finalizedHead.slot.epoch, false).expect(
"getEpochRef for finalized head should always succeed") "getEpochRef for finalized head should always succeed")
func ancestorSlotForShuffling*( func ancestorSlotForAttesterShuffling*(
dag: ChainDAGRef, state: ForkyHashedBeaconState, dag: ChainDAGRef, state: ForkyHashedBeaconState,
blck: BlockRef, epoch: Epoch): Opt[Slot] = blck: BlockRef, epoch: Epoch): Opt[Slot] =
## Return slot of `blck` ancestor to which `state` can be rewinded ## Return slot of `blck` ancestor to which `state` can be rewinded
@ -1385,116 +1385,57 @@ func ancestorSlotForShuffling*(
doAssert dependentSlot >= lowSlot doAssert dependentSlot >= lowSlot
ok min(min(stateBid.slot, ancestorBlck.slot), dependentSlot) ok min(min(stateBid.slot, ancestorBlck.slot), dependentSlot)
proc mixRandao( type AttesterRandaoMix = tuple[dependentBid: BlockId, mix: Eth2Digest]
dag: ChainDAGRef, mix: var Eth2Digest,
bid: BlockId): Opt[void] =
## Mix in/out the RANDAO reveal from the given block.
let bdata = ? dag.getForkedBlock(bid)
withBlck(bdata): # See `process_randao` / `process_randao_mixes_reset`
mix.data.mxor eth2digest(blck.message.body.randao_reveal.toRaw()).data
ok()
proc computeRandaoMix( proc computeAttesterRandaoMix(
dag: ChainDAGRef, state: ForkyHashedBeaconState, dag: ChainDAGRef, state: ForkyHashedBeaconState,
blck: BlockRef, epoch: Epoch blck: BlockRef, epoch: Epoch): Opt[AttesterRandaoMix] =
): Opt[tuple[dependentBid: BlockId, mix: Eth2Digest]] =
## Compute the requested RANDAO mix for `blck@epoch` based on `state`. ## Compute the requested RANDAO mix for `blck@epoch` based on `state`.
## `state` must have the correct `get_active_validator_indices` for `epoch`. ## If `state` has unviable `get_active_validator_indices`, return `none`.
## RANDAO reveals of blocks from `state.data.slot` back to `ancestorSlot` are
## mixed out from `state.data.randao_mixes`, and RANDAO reveals from blocks # Check `state` has locked-in `get_active_validator_indices` for `epoch`
## up through `epoch.attester_dependent_slot` are mixed in.
let let
stateSlot = state.data.slot stateSlot = state.data.slot
dependentSlot = epoch.attester_dependent_slot dependentSlot = epoch.attester_dependent_slot
# Check `state` has locked-in `get_active_validator_indices` for `epoch` ancestorSlot = ? dag.ancestorSlotForAttesterShuffling(state, blck, epoch)
ancestorSlot = ? dag.ancestorSlotForShuffling(state, blck, epoch)
doAssert ancestorSlot <= stateSlot doAssert ancestorSlot <= stateSlot
doAssert ancestorSlot <= dependentSlot doAssert ancestorSlot <= dependentSlot
# Load initial mix var mix: Eth2Digest
var mix {.noinit.}: Eth2Digest proc mixToAncestor(highBid: BlockId): Opt[void] =
let ## Mix in/out RANDAO reveals back to `ancestorSlot`
stateEpoch = stateSlot.epoch var bid = highBid
ancestorEpoch = ancestorSlot.epoch while bid.slot > ancestorSlot:
highRandaoSlot = let bdata = ? dag.getForkedBlock(bid)
# `randao_mixes[ancestorEpoch]` withBlck(bdata): # See `process_randao` / `process_randao_mixes_reset`
if stateEpoch == ancestorEpoch: mix.data.mxor eth2digest(blck.message.body.randao_reveal.toRaw()).data
stateSlot
else:
(ancestorEpoch + 1).start_slot - 1
startSlot =
if ancestorEpoch == GENESIS_EPOCH:
# Can only move backward
mix = state.data.get_randao_mix(ancestorEpoch)
highRandaoSlot
else:
# `randao_mixes[ancestorEpoch - 1]`
let lowRandaoSlot = ancestorEpoch.start_slot - 1
if highRandaoSlot - ancestorSlot < ancestorSlot - lowRandaoSlot:
mix = state.data.get_randao_mix(ancestorEpoch)
highRandaoSlot
else:
mix = state.data.get_randao_mix(ancestorEpoch - 1)
lowRandaoSlot
slotsToMix =
if startSlot > ancestorSlot:
(ancestorSlot + 1) .. startSlot
else:
(startSlot + 1) .. ancestorSlot
highRoot =
if slotsToMix.b == stateSlot:
state.latest_block_root
else:
doAssert slotsToMix.b < stateSlot
state.data.get_block_root_at_slot(slotsToMix.b)
# Move `mix` from `startSlot` to `ancestorSlot`
var bid =
if slotsToMix.b >= dag.finalizedHead.slot:
var b = ? dag.getBlockRef(highRoot)
let lowSlot = max(slotsToMix.a, dag.finalizedHead.slot)
while b.bid.slot > lowSlot:
? dag.mixRandao(mix, b.bid)
b = b.parent
doAssert b != nil
b.bid
else:
var highSlot = slotsToMix.b
const availableSlots = SLOTS_PER_HISTORICAL_ROOT
let lowSlot = max(state.data.slot, availableSlots.Slot) - availableSlots
while highSlot > lowSlot and
state.data.get_block_root_at_slot(highSlot - 1) == highRoot:
dec highSlot
if highSlot + SLOTS_PER_HISTORICAL_ROOT > state.data.slot:
BlockId(slot: highSlot, root: highRoot)
else:
let bsi = ? dag.getBlockIdAtSlot(highSlot)
doAssert bsi.bid.root == highRoot
bsi.bid
while bid.slot >= slotsToMix.a:
? dag.mixRandao(mix, bid)
bid = ? dag.parent(bid) bid = ? dag.parent(bid)
ok()
# Move `mix` from `ancestorSlot` to `dependentSlot` # Determine block for obtaining RANDAO mix
var dependentBid {.noinit.}: BlockId let
bid = dependentBid =
if dependentSlot >= dag.finalizedHead.slot: if dependentSlot >= dag.finalizedHead.slot:
var b = blck.get_ancestor(dependentSlot) var b = blck.get_ancestor(dependentSlot)
doAssert b != nil doAssert b != nil
dependentBid = b.bid
let lowSlot = max(ancestorSlot, dag.finalizedHead.slot)
while b.bid.slot > lowSlot:
? dag.mixRandao(mix, b.bid)
b = b.parent
doAssert b != nil
b.bid b.bid
else: else:
let bsi = ? dag.getBlockIdAtSlot(dependentSlot) let bsi = ? dag.getBlockIdAtSlot(dependentSlot)
dependentBid = bsi.bid
bsi.bid bsi.bid
while bid.slot > ancestorSlot:
? dag.mixRandao(mix, bid) # Mix in RANDAO from `blck`
bid = ? dag.parent(bid) if ancestorSlot < dependentBid.slot:
? mixToAncestor(dependentBid)
# Mix in RANDAO from `state`
let ancestorEpoch = ancestorSlot.epoch
if ancestorEpoch + EPOCHS_PER_HISTORICAL_VECTOR <= stateSlot.epoch:
return Opt.none(AttesterRandaoMix)
let mixRoot = state.dependent_root(ancestorEpoch + 1)
if mixRoot.isZero:
return Opt.none(AttesterRandaoMix)
? mixToAncestor(? dag.getBlockId(mixRoot))
mix.data.mxor state.data.get_randao_mix(ancestorEpoch).data
ok (dependentBid: dependentBid, mix: mix) ok (dependentBid: dependentBid, mix: mix)
@ -1502,7 +1443,7 @@ proc computeShufflingRefFromState*(
dag: ChainDAGRef, state: ForkyHashedBeaconState, dag: ChainDAGRef, state: ForkyHashedBeaconState,
blck: BlockRef, epoch: Epoch): Opt[ShufflingRef] = blck: BlockRef, epoch: Epoch): Opt[ShufflingRef] =
let (dependentBid, mix) = let (dependentBid, mix) =
? dag.computeRandaoMix(state, blck, epoch) ? dag.computeAttesterRandaoMix(state, blck, epoch)
return ok ShufflingRef( return ok ShufflingRef(
epoch: epoch, epoch: epoch,
@ -1555,15 +1496,9 @@ proc computeShufflingRefFromDatabase*(
proc computeShufflingRef*( proc computeShufflingRef*(
dag: ChainDAGRef, blck: BlockRef, epoch: Epoch): Opt[ShufflingRef] = dag: ChainDAGRef, blck: BlockRef, epoch: Epoch): Opt[ShufflingRef] =
# Try to compute `ShufflingRef` from states available in memory # Try to compute `ShufflingRef` from states available in memory
template tryWithState(state: ForkedHashedBeaconState) = let shufflingRef = dag.computeShufflingRefFromMemory(blck, epoch)
withState(state):
let shufflingRef =
dag.computeShufflingRefFromState(forkyState, blck, epoch)
if shufflingRef.isOk: if shufflingRef.isOk:
return shufflingRef return shufflingRef
tryWithState dag.headState
tryWithState dag.epochRefState
tryWithState dag.clearanceState
# Fall back to database # Fall back to database
dag.computeShufflingRefFromDatabase(blck, epoch) dag.computeShufflingRefFromDatabase(blck, epoch)

View File

@ -1311,7 +1311,8 @@ suite "Shufflings":
blckEpoch = blck.bid.slot.epoch blckEpoch = blck.bid.slot.epoch
minEpoch = min(stateEpoch, blckEpoch) minEpoch = min(stateEpoch, blckEpoch)
if compute_activation_exit_epoch(minEpoch) <= epoch or if compute_activation_exit_epoch(minEpoch) <= epoch or
dag.ancestorSlotForShuffling(forkyState, blck, epoch).isNone: dag.ancestorSlotForAttesterShuffling(
forkyState, blck, epoch).isNone:
check shufflingRef.isErr check shufflingRef.isErr
else: else:
check shufflingRef.isOk check shufflingRef.isOk