Update database and block replayer to handle payload envelopes (#8886)

Closes:

- https://github.com/sigp/lighthouse/issues/8869


  - Update `BlockReplayer` to support replay of execution payload envelopes.
- Update `HotColdDB` to load payload envelopes and feed them to the `BlockReplayer` for both hot + cold states. However the cold DB code is not fully working yet (see: https://github.com/sigp/lighthouse/issues/8958).
- Add `StatePayloadStatus` to allow callers to specify whether they want a state with a payload applied, or not.
- Fix the state cache to key by `StatePayloadStatus`.
- Lots of fixes to block production and block processing regarding state management.
- Initial test harness support for producing+processing Gloas blocks+envelopes
- A few new tests to cover Gloas DB operations


Co-Authored-By: Eitan Seri- Levi <eserilev@gmail.com>

Co-Authored-By: Eitan Seri-Levi <eserilev@ucsc.edu>

Co-Authored-By: Michael Sproul <michael@sigmaprime.io>

Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com>

Co-Authored-By: Jimmy Chen <jchen.tc@gmail.com>
This commit is contained in:
Michael Sproul
2026-03-12 10:06:25 +11:00
committed by GitHub
parent 6350a27031
commit bff72a920d
30 changed files with 1243 additions and 84 deletions

View File

@@ -1,6 +1,11 @@
use crate::{
BlockProcessingError, BlockSignatureStrategy, ConsensusContext, SlotProcessingError,
VerifyBlockRoot, per_block_processing, per_epoch_processing::EpochProcessingSummary,
VerifyBlockRoot, VerifySignatures,
envelope_processing::{
EnvelopeProcessingError, VerifyStateRoot, process_execution_payload_envelope,
},
per_block_processing,
per_epoch_processing::EpochProcessingSummary,
per_slot_processing,
};
use itertools::Itertools;
@@ -8,7 +13,7 @@ use std::iter::Peekable;
use std::marker::PhantomData;
use types::{
BeaconState, BeaconStateError, BlindedPayload, ChainSpec, EthSpec, Hash256, SignedBeaconBlock,
Slot,
SignedExecutionPayloadEnvelope, Slot, execution::StatePayloadStatus,
};
pub type PreBlockHook<'a, E, Error> = Box<
@@ -24,7 +29,7 @@ pub type PostSlotHook<'a, E, Error> = Box<
>;
pub type StateRootIterDefault<Error> = std::iter::Empty<Result<(Hash256, Slot), Error>>;
/// Efficiently apply blocks to a state while configuring various parameters.
/// Efficiently apply blocks and payloads to a state while configuring various parameters.
///
/// Usage follows a builder pattern.
pub struct BlockReplayer<
@@ -41,8 +46,21 @@ pub struct BlockReplayer<
post_block_hook: Option<PostBlockHook<'a, Spec, Error>>,
pre_slot_hook: Option<PreSlotHook<'a, Spec, Error>>,
post_slot_hook: Option<PostSlotHook<'a, Spec, Error>>,
/// Iterator over state roots for all *block* states.
///
/// Pre-Gloas, this is all states. Post-Gloas, this is *just* the states corresponding to beacon
/// blocks. For states corresponding to payloads, we read the state root from the payload
/// envelope.
// TODO(gloas): this concept might need adjusting when we implement the cold DB.
pub(crate) state_root_iter: Option<Peekable<StateRootIter>>,
state_root_miss: bool,
/// The payload status of the state desired as the end result of block replay.
///
/// This dictates whether a payload should be applied after applying the last block.
///
/// Prior to Gloas, this should always be set to `StatePayloadStatus::Pending` to indicate
/// that no envelope needs to be applied.
desired_state_payload_status: StatePayloadStatus,
_phantom: PhantomData<Error>,
}
@@ -50,7 +68,12 @@ pub struct BlockReplayer<
pub enum BlockReplayError {
SlotProcessing(SlotProcessingError),
BlockProcessing(BlockProcessingError),
EnvelopeProcessing(EnvelopeProcessingError),
BeaconState(BeaconStateError),
/// A payload envelope for this `slot` was required but not provided.
MissingPayloadEnvelope {
slot: Slot,
},
}
impl From<SlotProcessingError> for BlockReplayError {
@@ -65,6 +88,12 @@ impl From<BlockProcessingError> for BlockReplayError {
}
}
impl From<EnvelopeProcessingError> for BlockReplayError {
fn from(e: EnvelopeProcessingError) -> Self {
Self::EnvelopeProcessing(e)
}
}
impl From<BeaconStateError> for BlockReplayError {
fn from(e: BeaconStateError) -> Self {
Self::BeaconState(e)
@@ -96,6 +125,7 @@ where
post_slot_hook: None,
state_root_iter: None,
state_root_miss: false,
desired_state_payload_status: StatePayloadStatus::Pending,
_phantom: PhantomData,
}
}
@@ -161,6 +191,14 @@ where
self
}
/// Set the desired payload status of the state reached by replay.
///
/// This determines whether to apply a payload after applying the last block.
pub fn desired_state_payload_status(mut self, payload_status: StatePayloadStatus) -> Self {
self.desired_state_payload_status = payload_status;
self
}
/// Compute the state root for `self.state` as efficiently as possible.
///
/// This function MUST only be called when `self.state` is a post-state, i.e. it MUST not be
@@ -208,6 +246,38 @@ where
Ok(state_root)
}
/// Apply an execution payload envelope to `self.state`.
///
/// The `block_state_root` MUST be the `state_root` of the most recently applied block.
///
/// Returns the `state_root` of `self.state` after payload application.
fn apply_payload_envelope(
&mut self,
envelope: &SignedExecutionPayloadEnvelope<E>,
block_state_root: Hash256,
) -> Result<Hash256, Error> {
// TODO(gloas): bulk signature verification could be relevant here?
let verify_payload_signatures =
if let BlockSignatureStrategy::NoVerification = self.block_sig_strategy {
VerifySignatures::False
} else {
VerifySignatures::True
};
// TODO(gloas): state root verif enabled during initial prototyping
let verify_state_root = VerifyStateRoot::True;
process_execution_payload_envelope(
&mut self.state,
Some(block_state_root),
envelope,
verify_payload_signatures,
verify_state_root,
self.spec,
)
.map_err(BlockReplayError::from)?;
Ok(envelope.message.state_root)
}
/// Apply `blocks` atop `self.state`, taking care of slot processing.
///
/// If `target_slot` is provided then the state will be advanced through to `target_slot`
@@ -215,8 +285,21 @@ where
pub fn apply_blocks(
mut self,
blocks: Vec<SignedBeaconBlock<E, BlindedPayload<E>>>,
payload_envelopes: Vec<SignedExecutionPayloadEnvelope<E>>,
target_slot: Option<Slot>,
) -> Result<Self, Error> {
let mut envelopes_iter = payload_envelopes.into_iter();
let mut next_envelope_at_slot = |slot| {
if let Some(envelope) = envelopes_iter.next()
&& envelope.message.slot == slot
{
Ok(envelope)
} else {
Err(BlockReplayError::MissingPayloadEnvelope { slot })
}
};
for (i, block) in blocks.iter().enumerate() {
// Allow one additional block at the start which is only used for its state root.
if i == 0 && block.slot() <= self.state.slot() {
@@ -224,7 +307,35 @@ where
}
while self.state.slot() < block.slot() {
let state_root = self.get_state_root(&blocks, i)?;
let mut state_root = self.get_state_root(&blocks, i)?;
// Apply the payload for the *previous* block if the bid in the current block
// indicates that the parent is full (and it hasn't already been applied).
state_root = if block.fork_name_unchecked().gloas_enabled()
&& self.state.slot() == self.state.latest_block_header().slot
{
let latest_bid_block_hash = self
.state
.latest_execution_payload_bid()
.map_err(BlockReplayError::from)?
.block_hash;
// Similar to `is_parent_block_full`, but reading the block hash from the
// not-yet-applied `block`. The slot 0 case covers genesis (no block replay reqd).
if self.state.slot() != 0 && block.is_parent_block_full(latest_bid_block_hash) {
let envelope = next_envelope_at_slot(self.state.slot())?;
// State root for the next slot processing is now the envelope's state root.
self.apply_payload_envelope(&envelope, state_root)?
} else {
// Empty payload at this slot, the state root is unchanged from when the
// beacon block was applied.
state_root
}
} else {
// Pre-Gloas or at skipped slots post-Gloas, the state root of the parent state
// is always the output from `self.get_state_root`.
state_root
};
if let Some(ref mut pre_slot_hook) = self.pre_slot_hook {
pre_slot_hook(state_root, &mut self.state)?;
@@ -268,9 +379,24 @@ where
}
}
// Apply the last payload if desired.
let mut opt_state_root = if let StatePayloadStatus::Full = self.desired_state_payload_status
&& let Some(last_block) = blocks.last()
{
let envelope = next_envelope_at_slot(self.state.slot())?;
Some(self.apply_payload_envelope(&envelope, last_block.state_root())?)
} else {
None
};
if let Some(target_slot) = target_slot {
while self.state.slot() < target_slot {
let state_root = self.get_state_root(&blocks, blocks.len())?;
// Read state root from `opt_state_root` if a payload was just applied.
let state_root = if let Some(root) = opt_state_root.take() {
root
} else {
self.get_state_root(&blocks, blocks.len())?
};
if let Some(ref mut pre_slot_hook) = self.pre_slot_hook {
pre_slot_hook(state_root, &mut self.state)?;

View File

@@ -241,8 +241,6 @@ pub fn process_execution_payload_envelope<E: EthSpec>(
// TODO(gloas): newPayload happens here in the spec, ensure we wire that up correctly
process_deposit_requests_post_gloas(state, &execution_requests.deposits, spec)?;
// TODO(gloas): gotta update these
process_withdrawal_requests(state, &execution_requests.withdrawals, spec)?;
process_consolidation_requests(state, &execution_requests.consolidations, spec)?;

View File

@@ -1014,7 +1014,7 @@ async fn block_replayer_peeking_state_roots() {
let block_replayer = BlockReplayer::new(parent_state, &harness.chain.spec)
.state_root_iter(state_root_iter.into_iter())
.no_signature_verification()
.apply_blocks(vec![target_block], None)
.apply_blocks(vec![target_block], vec![], None)
.unwrap();
assert_eq!(

View File

@@ -77,6 +77,11 @@ pub fn partial_state_advance<E: EthSpec>(
// (all-zeros) state root.
let mut initial_state_root = Some(if state.slot() > state.latest_block_header().slot {
state_root_opt.unwrap_or_else(Hash256::zero)
} else if state.slot() == state.latest_block_header().slot
&& !state.latest_block_header().state_root.is_zero()
{
// Post-Gloas Full state case.
state.latest_block_header().state_root
} else {
state_root_opt.ok_or(Error::StateRootNotProvided)?
});