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(
"getEpochRef for finalized head should always succeed")
func ancestorSlotForShuffling*(
func ancestorSlotForAttesterShuffling*(
dag: ChainDAGRef, state: ForkyHashedBeaconState,
blck: BlockRef, epoch: Epoch): Opt[Slot] =
## Return slot of `blck` ancestor to which `state` can be rewinded
@ -1385,116 +1385,57 @@ func ancestorSlotForShuffling*(
doAssert dependentSlot >= lowSlot
ok min(min(stateBid.slot, ancestorBlck.slot), dependentSlot)
proc mixRandao(
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()
type AttesterRandaoMix = tuple[dependentBid: BlockId, mix: Eth2Digest]
proc computeRandaoMix(
proc computeAttesterRandaoMix(
dag: ChainDAGRef, state: ForkyHashedBeaconState,
blck: BlockRef, epoch: Epoch
): Opt[tuple[dependentBid: BlockId, mix: Eth2Digest]] =
blck: BlockRef, epoch: Epoch): Opt[AttesterRandaoMix] =
## Compute the requested RANDAO mix for `blck@epoch` based on `state`.
## `state` must have the correct `get_active_validator_indices` for `epoch`.
## RANDAO reveals of blocks from `state.data.slot` back to `ancestorSlot` are
## mixed out from `state.data.randao_mixes`, and RANDAO reveals from blocks
## up through `epoch.attester_dependent_slot` are mixed in.
## If `state` has unviable `get_active_validator_indices`, return `none`.
# Check `state` has locked-in `get_active_validator_indices` for `epoch`
let
stateSlot = state.data.slot
dependentSlot = epoch.attester_dependent_slot
# Check `state` has locked-in `get_active_validator_indices` for `epoch`
ancestorSlot = ? dag.ancestorSlotForShuffling(state, blck, epoch)
ancestorSlot = ? dag.ancestorSlotForAttesterShuffling(state, blck, epoch)
doAssert ancestorSlot <= stateSlot
doAssert ancestorSlot <= dependentSlot
# Load initial mix
var mix {.noinit.}: Eth2Digest
let
stateEpoch = stateSlot.epoch
ancestorEpoch = ancestorSlot.epoch
highRandaoSlot =
# `randao_mixes[ancestorEpoch]`
if stateEpoch == ancestorEpoch:
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)
var mix: Eth2Digest
proc mixToAncestor(highBid: BlockId): Opt[void] =
## Mix in/out RANDAO reveals back to `ancestorSlot`
var bid = highBid
while bid.slot > ancestorSlot:
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
bid = ? dag.parent(bid)
ok()
# Move `mix` from `ancestorSlot` to `dependentSlot`
var dependentBid {.noinit.}: BlockId
bid =
# Determine block for obtaining RANDAO mix
let
dependentBid =
if dependentSlot >= dag.finalizedHead.slot:
var b = blck.get_ancestor(dependentSlot)
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
else:
let bsi = ? dag.getBlockIdAtSlot(dependentSlot)
dependentBid = bsi.bid
bsi.bid
while bid.slot > ancestorSlot:
? dag.mixRandao(mix, bid)
bid = ? dag.parent(bid)
# Mix in RANDAO from `blck`
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)
@ -1502,7 +1443,7 @@ proc computeShufflingRefFromState*(
dag: ChainDAGRef, state: ForkyHashedBeaconState,
blck: BlockRef, epoch: Epoch): Opt[ShufflingRef] =
let (dependentBid, mix) =
? dag.computeRandaoMix(state, blck, epoch)
? dag.computeAttesterRandaoMix(state, blck, epoch)
return ok ShufflingRef(
epoch: epoch,
@ -1555,15 +1496,9 @@ proc computeShufflingRefFromDatabase*(
proc computeShufflingRef*(
dag: ChainDAGRef, blck: BlockRef, epoch: Epoch): Opt[ShufflingRef] =
# Try to compute `ShufflingRef` from states available in memory
template tryWithState(state: ForkedHashedBeaconState) =
withState(state):
let shufflingRef =
dag.computeShufflingRefFromState(forkyState, blck, epoch)
let shufflingRef = dag.computeShufflingRefFromMemory(blck, epoch)
if shufflingRef.isOk:
return shufflingRef
tryWithState dag.headState
tryWithState dag.epochRefState
tryWithState dag.clearanceState
# Fall back to database
dag.computeShufflingRefFromDatabase(blck, epoch)

View File

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