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

@@ -6689,6 +6689,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
let mut prev_block_root = None; let mut prev_block_root = None;
let mut prev_beacon_state = None; let mut prev_beacon_state = None;
// Collect all blocks.
let mut blocks = vec![];
for res in self.forwards_iter_block_roots(from_slot)? { for res in self.forwards_iter_block_roots(from_slot)? {
let (beacon_block_root, _) = res?; let (beacon_block_root, _) = res?;
@@ -6704,16 +6707,42 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
.ok_or_else(|| { .ok_or_else(|| {
Error::DBInconsistent(format!("Missing block {}", beacon_block_root)) Error::DBInconsistent(format!("Missing block {}", beacon_block_root))
})?; })?;
let beacon_state_root = beacon_block.state_root(); blocks.push((beacon_block_root, Arc::new(beacon_block)));
}
// Collect states, using the next blocks to determine if states are full (have Gloas
// payloads).
for (i, (block_root, block)) in blocks.iter().enumerate() {
let (opt_envelope, state_root) = if block.fork_name_unchecked().gloas_enabled() {
let opt_envelope = self.store.get_payload_envelope(block_root)?.map(Arc::new);
if let Some((_, next_block)) = blocks.get(i + 1) {
let block_hash = block.payload_bid_block_hash()?;
if next_block.is_parent_block_full(block_hash) {
let envelope = opt_envelope.ok_or_else(|| {
Error::DBInconsistent(format!("Missing envelope {block_root:?}"))
})?;
let state_root = envelope.message.state_root;
(Some(envelope), state_root)
} else {
(None, block.state_root())
}
} else {
// TODO(gloas): should use fork choice/cached head for last block in sequence
opt_envelope
.as_ref()
.map_or((None, block.state_root()), |envelope| {
(Some(envelope.clone()), envelope.message.state_root)
})
}
} else {
(None, block.state_root())
};
// This branch is reached from the HTTP API. We assume the user wants
// to cache states so that future calls are faster.
let mut beacon_state = self let mut beacon_state = self
.store .store
.get_state(&beacon_state_root, Some(beacon_block.slot()), true)? .get_state(&state_root, Some(block.slot()), true)?
.ok_or_else(|| { .ok_or_else(|| Error::DBInconsistent(format!("Missing state {:?}", state_root)))?;
Error::DBInconsistent(format!("Missing state {:?}", beacon_state_root))
})?;
// This beacon state might come from the freezer DB, which means it could have pending // This beacon state might come from the freezer DB, which means it could have pending
// updates or lots of untethered memory. We rebase it on the previous state in order to // updates or lots of untethered memory. We rebase it on the previous state in order to
@@ -6726,12 +6755,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
prev_beacon_state = Some(beacon_state.clone()); prev_beacon_state = Some(beacon_state.clone());
let snapshot = BeaconSnapshot { let snapshot = BeaconSnapshot {
beacon_block: Arc::new(beacon_block), beacon_block: block.clone(),
beacon_block_root, execution_envelope: opt_envelope,
beacon_block_root: *block_root,
beacon_state, beacon_state,
}; };
dump.push(snapshot); dump.push(snapshot);
} }
Ok(dump) Ok(dump)
} }

View File

@@ -2,7 +2,7 @@ use serde::Serialize;
use std::sync::Arc; use std::sync::Arc;
use types::{ use types::{
AbstractExecPayload, BeaconState, EthSpec, FullPayload, Hash256, SignedBeaconBlock, AbstractExecPayload, BeaconState, EthSpec, FullPayload, Hash256, SignedBeaconBlock,
SignedBlindedBeaconBlock, SignedBlindedBeaconBlock, SignedExecutionPayloadEnvelope,
}; };
/// Represents some block and its associated state. Generally, this will be used for tracking the /// Represents some block and its associated state. Generally, this will be used for tracking the
@@ -10,6 +10,7 @@ use types::{
#[derive(Clone, Serialize, PartialEq, Debug)] #[derive(Clone, Serialize, PartialEq, Debug)]
pub struct BeaconSnapshot<E: EthSpec, Payload: AbstractExecPayload<E> = FullPayload<E>> { pub struct BeaconSnapshot<E: EthSpec, Payload: AbstractExecPayload<E> = FullPayload<E>> {
pub beacon_block: Arc<SignedBeaconBlock<E, Payload>>, pub beacon_block: Arc<SignedBeaconBlock<E, Payload>>,
pub execution_envelope: Option<Arc<SignedExecutionPayloadEnvelope<E>>>,
pub beacon_block_root: Hash256, pub beacon_block_root: Hash256,
pub beacon_state: BeaconState<E>, pub beacon_state: BeaconState<E>,
} }
@@ -31,33 +32,42 @@ impl<E: EthSpec, Payload: AbstractExecPayload<E>> BeaconSnapshot<E, Payload> {
/// Create a new checkpoint. /// Create a new checkpoint.
pub fn new( pub fn new(
beacon_block: Arc<SignedBeaconBlock<E, Payload>>, beacon_block: Arc<SignedBeaconBlock<E, Payload>>,
execution_envelope: Option<Arc<SignedExecutionPayloadEnvelope<E>>>,
beacon_block_root: Hash256, beacon_block_root: Hash256,
beacon_state: BeaconState<E>, beacon_state: BeaconState<E>,
) -> Self { ) -> Self {
Self { Self {
beacon_block, beacon_block,
execution_envelope,
beacon_block_root, beacon_block_root,
beacon_state, beacon_state,
} }
} }
/// Returns the state root from `self.beacon_block`. /// Returns the state root from `self.beacon_block` or `self.execution_envelope` as
/// appropriate.
/// ///
/// ## Caution /// ## Caution
/// ///
/// It is not strictly enforced that `root(self.beacon_state) == self.beacon_state_root()`. /// It is not strictly enforced that `root(self.beacon_state) == self.beacon_state_root()`.
pub fn beacon_state_root(&self) -> Hash256 { pub fn beacon_state_root(&self) -> Hash256 {
self.beacon_block.message().state_root() if let Some(ref envelope) = self.execution_envelope {
envelope.message.state_root
} else {
self.beacon_block.message().state_root()
}
} }
/// Update all fields of the checkpoint. /// Update all fields of the checkpoint.
pub fn update( pub fn update(
&mut self, &mut self,
beacon_block: Arc<SignedBeaconBlock<E, Payload>>, beacon_block: Arc<SignedBeaconBlock<E, Payload>>,
execution_envelope: Option<Arc<SignedExecutionPayloadEnvelope<E>>>,
beacon_block_root: Hash256, beacon_block_root: Hash256,
beacon_state: BeaconState<E>, beacon_state: BeaconState<E>,
) { ) {
self.beacon_block = beacon_block; self.beacon_block = beacon_block;
self.execution_envelope = execution_envelope;
self.beacon_block_root = beacon_block_root; self.beacon_block_root = beacon_block_root;
self.beacon_state = beacon_state; self.beacon_state = beacon_state;
} }

View File

@@ -358,6 +358,7 @@ where
Ok(( Ok((
BeaconSnapshot { BeaconSnapshot {
beacon_block_root, beacon_block_root,
execution_envelope: None,
beacon_block: Arc::new(beacon_block), beacon_block: Arc::new(beacon_block),
beacon_state, beacon_state,
}, },
@@ -616,8 +617,10 @@ where
.map_err(|e| format!("Failed to initialize data column info: {:?}", e))?, .map_err(|e| format!("Failed to initialize data column info: {:?}", e))?,
); );
// TODO(gloas): add check that checkpoint state is Pending
let snapshot = BeaconSnapshot { let snapshot = BeaconSnapshot {
beacon_block_root: weak_subj_block_root, beacon_block_root: weak_subj_block_root,
execution_envelope: None,
beacon_block: Arc::new(weak_subj_block), beacon_block: Arc::new(weak_subj_block),
beacon_state: weak_subj_state, beacon_state: weak_subj_state,
}; };
@@ -800,6 +803,7 @@ where
let mut head_snapshot = BeaconSnapshot { let mut head_snapshot = BeaconSnapshot {
beacon_block_root: head_block_root, beacon_block_root: head_block_root,
execution_envelope: None,
beacon_block: Arc::new(head_block), beacon_block: Arc::new(head_block),
beacon_state: head_state, beacon_state: head_state,
}; };

View File

@@ -319,6 +319,7 @@ impl<T: BeaconChainTypes> CanonicalHead<T> {
let snapshot = BeaconSnapshot { let snapshot = BeaconSnapshot {
beacon_block_root, beacon_block_root,
execution_envelope: None,
beacon_block: Arc::new(beacon_block), beacon_block: Arc::new(beacon_block),
beacon_state, beacon_state,
}; };
@@ -695,6 +696,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
BeaconSnapshot { BeaconSnapshot {
beacon_block: Arc::new(beacon_block), beacon_block: Arc::new(beacon_block),
execution_envelope: None,
beacon_block_root: new_view.head_block_root, beacon_block_root: new_view.head_block_root,
beacon_state, beacon_state,
} }

View File

@@ -77,8 +77,10 @@ async fn get_chain_segment() -> (Vec<BeaconSnapshot<E>>, Vec<Option<DataSidecars
.unwrap(); .unwrap();
let block_epoch = full_block.epoch(); let block_epoch = full_block.epoch();
// TODO(gloas): probably need to update this test
segment.push(BeaconSnapshot { segment.push(BeaconSnapshot {
beacon_block_root: snapshot.beacon_block_root, beacon_block_root: snapshot.beacon_block_root,
execution_envelope: None,
beacon_block: Arc::new(full_block), beacon_block: Arc::new(full_block),
beacon_state: snapshot.beacon_state, beacon_state: snapshot.beacon_state,
}); });

View File

@@ -5599,6 +5599,7 @@ async fn test_gloas_block_and_envelope_storage_generic(
"slot = {slot}" "slot = {slot}"
); );
} }
check_db_invariants(&harness);
} }
/// Test that Pending and Full states have the correct payload status through round-trip /// Test that Pending and Full states have the correct payload status through round-trip
@@ -5666,6 +5667,7 @@ async fn test_gloas_state_payload_status() {
state = full_state; state = full_state;
} }
check_db_invariants(&harness);
} }
/// Test block replay with and without envelopes. /// Test block replay with and without envelopes.
@@ -5805,6 +5807,7 @@ async fn test_gloas_block_replay_with_envelopes() {
replayed_full, expected_full, replayed_full, expected_full,
"replayed full state should match stored full state" "replayed full state should match stored full state"
); );
check_db_invariants(&harness);
} }
/// Test the hot state hierarchy with Full states stored as ReplayFrom. /// Test the hot state hierarchy with Full states stored as ReplayFrom.
@@ -5822,7 +5825,7 @@ async fn test_gloas_hot_state_hierarchy() {
// 40 slots covers 5 epochs. // 40 slots covers 5 epochs.
let num_blocks = E::slots_per_epoch() * 5; let num_blocks = E::slots_per_epoch() * 5;
// TODO(gloas): enable finalisation by increasing this threshold // TODO(gloas): enable finalisation by increasing this threshold
let some_validators = (0..LOW_VALIDATOR_COUNT / 2).collect::<Vec<_>>(); let some_validators = (0..LOW_VALIDATOR_COUNT).collect::<Vec<_>>();
let (genesis_state, _genesis_state_root) = harness.get_current_state_and_root(); let (genesis_state, _genesis_state_root) = harness.get_current_state_and_root();
@@ -5886,6 +5889,7 @@ async fn test_gloas_hot_state_hierarchy() {
// Verify chain dump and iterators work with Gloas states. // Verify chain dump and iterators work with Gloas states.
check_chain_dump(&harness, num_blocks + 1); check_chain_dump(&harness, num_blocks + 1);
check_iterators(&harness); check_iterators(&harness);
check_db_invariants(&harness);
} }
/// Check that the HotColdDB's split_slot is equal to the start slot of the last finalized epoch. /// Check that the HotColdDB's split_slot is equal to the start slot of the last finalized epoch.

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> { fn load_hot_hdiff_buffer(&self, state_root: Hash256) -> Result<HDiffBuffer, Error> {
if let Some(buffer) = self if let Some(buffer) = self
.state_cache .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, || { self.forwards_state_roots_iterator_until(base_state.slot(), slot, || {
Err(Error::StateShouldNotBeRequired(slot)) Err(Error::StateShouldNotBeRequired(slot))
})?; })?;
// TODO(gloas): calculate correct payload status for cold states let payload_status = self.get_cold_state_payload_status(slot)?;
let payload_status = StatePayloadStatus::Pending;
let state = self.replay_blocks( let state = self.replay_blocks(
base_state, base_state,
blocks, blocks,
@@ -2591,9 +2635,10 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
{ {
return Ok((blocks, vec![])); return Ok((blocks, vec![]));
} }
// TODO(gloas): wire this up let end_block_root = self
let end_block_root = Hash256::ZERO; .get_cold_block_root(end_slot)?
let desired_payload_status = StatePayloadStatus::Pending; .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( let envelopes = self.load_payload_envelopes_for_blocks(
&blocks, &blocks,
end_block_root, end_block_root,

View File

@@ -319,6 +319,10 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
.spec .spec
.fulu_fork_epoch .fulu_fork_epoch
.map(|epoch| epoch.start_slot(E::slots_per_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_blob_slot = self.get_blob_info().oldest_blob_slot;
let oldest_data_column_slot = self.get_data_column_info().oldest_data_column_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. // Invariant 5: execution payload consistency.
// TODO(gloas): reconsider this invariant
if check_payloads if check_payloads
&& let Some(bellatrix_slot) = bellatrix_fork_slot && let Some(bellatrix_slot) = bellatrix_fork_slot
&& slot >= bellatrix_slot && slot >= bellatrix_slot
&& !self.execution_payload_exists(&block_root)?
&& !self.payload_envelope_exists(&block_root)?
{ {
result.add_violation(InvariantViolation::ExecutionPayloadMissing { if let Some(gloas_slot) = gloas_fork_slot
block_root, && slot >= gloas_slot
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. // Invariant 6: blob sidecar consistency.

View File

@@ -313,6 +313,7 @@ where
// indicates that the parent is full (and it hasn't already been applied). // indicates that the parent is full (and it hasn't already been applied).
state_root = if block.fork_name_unchecked().gloas_enabled() state_root = if block.fork_name_unchecked().gloas_enabled()
&& self.state.slot() == self.state.latest_block_header().slot && self.state.slot() == self.state.latest_block_header().slot
&& self.state.payload_status() == StatePayloadStatus::Pending
{ {
let latest_bid_block_hash = self let latest_bid_block_hash = self
.state .state