Gloas cold DB (#8991)

Closes:

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


  - Update the `HotColdStore` to handle storage of cold states.
- Update `BeaconSnapshot` to hold the execution envelope. This is required to make `chain_dump`-related checks sane, and will be generally useful (see: https://github.com/sigp/lighthouse/issues/8956).
- Bug fix in the `BlockReplayer` for the case where the starting state is already `Full` (we should not try to apply another payload). This happens on the cold DB path because we try to replay from the closest cached state (which is often full).
- Update `test_gloas_hot_state_hierarchy` to cover the cold DB migration.


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

Co-Authored-By: Michael Sproul <michaelsproul@users.noreply.github.com>
This commit is contained in:
Michael Sproul
2026-03-19 20:09:13 +11:00
committed by GitHub
parent a965bfdf77
commit 06025228ae
9 changed files with 139 additions and 25 deletions

View File

@@ -1906,6 +1906,51 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
}
}
/// Recompute the payload status for a state at `slot` that is stored in the cold DB.
///
/// This function returns an error for any `slot` that is outside the range of slots stored in
/// the freezer DB.
///
/// For all slots prior to Gloas, it returns `Pending`.
///
/// For post-Gloas slots the algorithm is:
///
/// 1. Load the most recently applied block at `slot` (may not be from `slot` in case of a skip)
/// 2. Load the canonical `state_root` at the slot of the block. If this `state_root` matches
/// the one in the block then we know the state at *that* slot is canonically empty (no
/// payload). Conversely, if it is different, we know that the block's slot is full (assuming
/// no database corruption).
/// 3. The payload status of `slot` is the same as the payload status of `block.slot()`, because
/// we only care about whether a beacon block or payload was applied most recently, and
/// `block` is by definition the most-recently-applied block.
///
/// All of this mucking around could be avoided if we do a schema migration to record the
/// payload status in the database. For now, this is simpler.
fn get_cold_state_payload_status(&self, slot: Slot) -> Result<StatePayloadStatus, Error> {
// Pre-Gloas states are always `Pending`.
if !self.spec.fork_name_at_slot::<E>(slot).gloas_enabled() {
return Ok(StatePayloadStatus::Pending);
}
let block_root = self
.get_cold_block_root(slot)?
.ok_or(HotColdDBError::MissingFrozenBlock(slot))?;
let block = self
.get_blinded_block(&block_root)?
.ok_or(Error::MissingBlock(block_root))?;
let state_root = self
.get_cold_state_root(block.slot())?
.ok_or(HotColdDBError::MissingRestorePointState(block.slot()))?;
if block.state_root() != state_root {
Ok(StatePayloadStatus::Full)
} else {
Ok(StatePayloadStatus::Pending)
}
}
fn load_hot_hdiff_buffer(&self, state_root: Hash256) -> Result<HDiffBuffer, Error> {
if let Some(buffer) = self
.state_cache
@@ -2454,8 +2499,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
self.forwards_state_roots_iterator_until(base_state.slot(), slot, || {
Err(Error::StateShouldNotBeRequired(slot))
})?;
// TODO(gloas): calculate correct payload status for cold states
let payload_status = StatePayloadStatus::Pending;
let payload_status = self.get_cold_state_payload_status(slot)?;
let state = self.replay_blocks(
base_state,
blocks,
@@ -2591,9 +2635,10 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
{
return Ok((blocks, vec![]));
}
// TODO(gloas): wire this up
let end_block_root = Hash256::ZERO;
let desired_payload_status = StatePayloadStatus::Pending;
let end_block_root = self
.get_cold_block_root(end_slot)?
.ok_or(HotColdDBError::MissingFrozenBlock(end_slot))?;
let desired_payload_status = self.get_cold_state_payload_status(end_slot)?;
let envelopes = self.load_payload_envelopes_for_blocks(
&blocks,
end_block_root,

View File

@@ -319,6 +319,10 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
.spec
.fulu_fork_epoch
.map(|epoch| epoch.start_slot(E::slots_per_epoch()));
let gloas_fork_slot = self
.spec
.gloas_fork_epoch
.map(|epoch| epoch.start_slot(E::slots_per_epoch()));
let oldest_blob_slot = self.get_blob_info().oldest_blob_slot;
let oldest_data_column_slot = self.get_data_column_info().oldest_data_column_slot;
@@ -343,17 +347,28 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
}
// Invariant 5: execution payload consistency.
// TODO(gloas): reconsider this invariant
if check_payloads
&& let Some(bellatrix_slot) = bellatrix_fork_slot
&& slot >= bellatrix_slot
&& !self.execution_payload_exists(&block_root)?
&& !self.payload_envelope_exists(&block_root)?
{
result.add_violation(InvariantViolation::ExecutionPayloadMissing {
block_root,
slot,
});
if let Some(gloas_slot) = gloas_fork_slot
&& slot >= gloas_slot
{
// For Gloas there is never a true payload stored at slot 0.
// TODO(gloas): still need to account for non-canonical payloads once pruning
// is implemented.
if slot != 0 && !self.payload_envelope_exists(&block_root)? {
result.add_violation(InvariantViolation::ExecutionPayloadMissing {
block_root,
slot,
});
}
} else if !self.execution_payload_exists(&block_root)? {
result.add_violation(InvariantViolation::ExecutionPayloadMissing {
block_root,
slot,
});
}
}
// Invariant 6: blob sidecar consistency.