Merge conlficts

This commit is contained in:
Eitan Seri- Levi
2026-03-26 21:50:30 -07:00
158 changed files with 4877 additions and 3908 deletions

View File

@@ -186,10 +186,8 @@ impl<E: EthSpec> LevelDB<E> {
)
};
for (start_key, end_key) in [
endpoints(DBColumn::BeaconState),
endpoints(DBColumn::BeaconStateSummary),
] {
{
let (start_key, end_key) = endpoints(DBColumn::BeaconStateHotSummary);
self.db.compact(&start_key, &end_key);
}

View File

@@ -12,9 +12,7 @@ use std::str::FromStr;
use std::sync::LazyLock;
use superstruct::superstruct;
use types::state::HistoricalSummary;
use types::{
BeaconState, ChainSpec, Epoch, EthSpec, Hash256, Slot, Validator, execution::StatePayloadStatus,
};
use types::{BeaconState, ChainSpec, Epoch, EthSpec, Hash256, Slot, Validator};
static EMPTY_PUBKEY: LazyLock<PublicKeyBytes> = LazyLock::new(PublicKeyBytes::empty);
@@ -655,19 +653,11 @@ impl HierarchyModuli {
/// exponents [5,13,21], to reconstruct state at slot 3,000,003: if start = 3,000,002
/// layer 2 diff will point to the start snapshot instead of the layer 1 diff at
/// 2998272.
/// * `payload_status` - whether the state is `Full` (came from processing a payload), or
/// `Pending` (came from processing a block). Prior to Gloas all states are `Pending`.
/// Skipped slots post-Gloas should also use a `Pending` status.
pub fn storage_strategy(
&self,
slot: Slot,
start_slot: Slot,
_payload_status: StatePayloadStatus,
) -> Result<StorageStrategy, Error> {
// FIXME(sproul): Reverted the idea of using different storage strategies for full and
// pending states, this has the consequence of storing double diffs and double snapshots
// at full slots. The complexity of managing skipped slots was the main impetus for
// reverting the payload-status sensitive design: a Full skipped slot has no same-slot
pub fn storage_strategy(&self, slot: Slot, start_slot: Slot) -> Result<StorageStrategy, Error> {
// Initially had the idea of using different storage strategies for full and pending states,
// but it was very complex. However without this concept we end up storing two diffs/two
// snapshots at full slots. The complexity of managing skipped slots was the main impetus
// for reverting the payload-status sensitive design: a Full skipped slot has no same-slot
// Pending state to replay from, so has to be handled differently from Full non-skipped
// slots.
match slot.cmp(&start_slot) {

View File

@@ -452,26 +452,15 @@ impl<E: EthSpec> HotColdDB<E, BeaconNodeBackend<E>, BeaconNodeBackend<E>> {
}
impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> {
fn cold_storage_strategy(
&self,
slot: Slot,
// payload_status: StatePayloadStatus,
) -> Result<StorageStrategy, Error> {
fn cold_storage_strategy(&self, slot: Slot) -> Result<StorageStrategy, Error> {
// The start slot for the freezer HDiff is always 0
// TODO(gloas): wire up payload_status
Ok(self
.hierarchy
.storage_strategy(slot, Slot::new(0), StatePayloadStatus::Pending)?)
Ok(self.hierarchy.storage_strategy(slot, Slot::new(0))?)
}
pub fn hot_storage_strategy(
&self,
slot: Slot,
payload_status: StatePayloadStatus,
) -> Result<StorageStrategy, Error> {
pub fn hot_storage_strategy(&self, slot: Slot) -> Result<StorageStrategy, Error> {
Ok(self
.hierarchy
.storage_strategy(slot, self.hot_hdiff_start_slot()?, payload_status)?)
.storage_strategy(slot, self.hot_hdiff_start_slot()?)?)
}
pub fn hot_hdiff_start_slot(&self) -> Result<Slot, Error> {
@@ -1402,8 +1391,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
// Use `Pending` status here because snapshots and diffs are only stored for
// `Pending` states.
if let Some(slot) = slot
&& let Ok(strategy) =
self.hot_storage_strategy(slot, StatePayloadStatus::Pending)
&& let Ok(strategy) = self.hot_storage_strategy(slot)
{
match strategy {
StorageStrategy::Snapshot => {
@@ -1675,8 +1663,6 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
state: &BeaconState<E>,
ops: &mut Vec<KeyValueStoreOp>,
) -> Result<(), Error> {
let payload_status = state.payload_status();
match self.state_cache.lock().put_state(
*state_root,
state.get_latest_block_root(*state_root),
@@ -1722,7 +1708,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
debug!(
?state_root,
slot = %state.slot(),
storage_strategy = ?self.hot_storage_strategy(state.slot(), payload_status)?,
storage_strategy = ?self.hot_storage_strategy(state.slot())?,
diff_base_state = %summary.diff_base_state,
previous_state_root = ?summary.previous_state_root,
"Storing hot state summary and diffs"
@@ -1745,7 +1731,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
self,
*state_root,
state,
self.hot_storage_strategy(state.slot(), state.payload_status())?,
self.hot_storage_strategy(state.slot())?,
)?;
ops.push(hot_state_summary.as_kv_store_op(*state_root));
Ok(hot_state_summary)
@@ -1758,7 +1744,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
ops: &mut Vec<KeyValueStoreOp>,
) -> Result<(), Error> {
let slot = state.slot();
let storage_strategy = self.hot_storage_strategy(slot, state.payload_status())?;
let storage_strategy = self.hot_storage_strategy(slot)?;
match storage_strategy {
StorageStrategy::ReplayFrom(_) => {
// Already have persisted the state summary, don't persist anything else
@@ -1896,7 +1882,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
// Load the hot state summary for the previous state.
//
// If it has the same slot as this summary then we know this summary is for a `Full` block
// If it has the same slot as this summary then we know this summary is for a `Full` state
// (payload state), because they are always diffed against their same-slot `Pending` state.
//
// If the previous summary has a different slot AND the latest block is from `summary.slot`,
@@ -1920,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
@@ -1929,20 +1960,16 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
return Ok(buffer);
}
let Some(
summary @ HotStateSummary {
slot,
diff_base_state,
..
},
) = self.load_hot_state_summary(&state_root)?
let Some(HotStateSummary {
slot,
diff_base_state,
..
}) = self.load_hot_state_summary(&state_root)?
else {
return Err(Error::MissingHotStateSummary(state_root));
};
let payload_status = self.get_hot_state_summary_payload_status(&summary)?;
let buffer = match self.hot_storage_strategy(slot, payload_status)? {
let buffer = match self.hot_storage_strategy(slot)? {
StorageStrategy::Snapshot => {
let Some(state) = self.load_hot_state_as_snapshot(state_root)? else {
let existing_snapshots = self.load_hot_state_snapshot_roots()?;
@@ -2035,7 +2062,7 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
?payload_status,
"Loading hot state"
);
let mut state = match self.hot_storage_strategy(slot, payload_status)? {
let mut state = match self.hot_storage_strategy(slot)? {
strat @ StorageStrategy::Snapshot | strat @ StorageStrategy::DiffFrom(_) => {
let buffer_timer = metrics::start_timer_vec(
&metrics::BEACON_HDIFF_BUFFER_LOAD_TIME,
@@ -2472,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,
@@ -2609,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,
@@ -2630,7 +2657,6 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
/// Payloads are also returned in slot-ascending order, but only payloads forming part of
/// the chain are loaded (payloads for EMPTY slots are omitted). Prior to Gloas, an empty
/// vec of payloads will be returned.
// TODO(gloas): handle last payload
#[allow(clippy::type_complexity)]
pub fn load_blocks_to_replay(
&self,
@@ -2716,7 +2742,6 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
}
// Load the payload for the last block if desired.
// TODO(gloas): check that we don't load a duplicate in the case of a skipped slot
if let StatePayloadStatus::Full = desired_payload_status {
let envelope = self.get_payload_envelope(&end_block_root)?.ok_or(
HotColdDBError::MissingExecutionPayloadEnvelope(end_block_root),
@@ -3245,12 +3270,10 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
Some(mut split) => {
debug!(?split, "Loaded split partial");
// Load the hot state summary to get the block root.
let latest_block_root = self
.load_block_root_from_summary_any_version(&split.state_root)
.ok_or(HotColdDBError::MissingSplitState(
split.state_root,
split.slot,
))?;
let latest_block_root =
self.load_block_root_from_summary(&split.state_root).ok_or(
HotColdDBError::MissingSplitState(split.state_root, split.slot),
)?;
split.block_root = latest_block_root;
Ok(Some(split))
}
@@ -3281,29 +3304,11 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
.map_err(|e| Error::LoadHotStateSummary(*state_root, e.into()))
}
/// Load a hot state's summary in V22 format, given its root.
pub fn load_hot_state_summary_v22(
&self,
state_root: &Hash256,
) -> Result<Option<HotStateSummaryV22>, Error> {
self.hot_db
.get(state_root)
.map_err(|e| Error::LoadHotStateSummary(*state_root, e.into()))
}
/// Load the latest block root for a hot state summary either in modern form, or V22 form.
///
/// This function is required to open a V22 database for migration to V24, or vice versa.
pub fn load_block_root_from_summary_any_version(
&self,
state_root: &Hash256,
) -> Option<Hash256> {
/// Load the latest block root for a hot state summary.
pub fn load_block_root_from_summary(&self, state_root: &Hash256) -> Option<Hash256> {
if let Ok(Some(summary)) = self.load_hot_state_summary(state_root) {
return Some(summary.latest_block_root);
}
if let Ok(Some(summary)) = self.load_hot_state_summary_v22(state_root) {
return Some(summary.latest_block_root);
}
None
}
@@ -4262,30 +4267,6 @@ impl HotStateSummary {
}
}
/// Legacy hot state summary used in schema V22 and before.
///
/// This can be deleted when we remove V22 support.
#[derive(Debug, Clone, Copy, Encode, Decode)]
pub struct HotStateSummaryV22 {
pub slot: Slot,
pub latest_block_root: Hash256,
pub epoch_boundary_state_root: Hash256,
}
impl StoreItem for HotStateSummaryV22 {
fn db_column() -> DBColumn {
DBColumn::BeaconStateSummary
}
fn as_store_bytes(&self) -> Vec<u8> {
self.as_ssz_bytes()
}
fn from_store_bytes(bytes: &[u8]) -> Result<Self, Error> {
Ok(Self::from_ssz_bytes(bytes)?)
}
}
/// Struct for summarising a state in the freezer database.
#[derive(Debug, Clone, Copy, Default, Encode, Decode)]
pub(crate) struct ColdStateSummary {

View File

@@ -0,0 +1,796 @@
//! Database invariant checks for the hot and cold databases.
//!
//! These checks verify the consistency of data stored in the database. They are designed to be
//! called from the HTTP API and from tests to detect data corruption or bugs in the store logic.
//!
//! See the `check_invariants` and `check_database_invariants` methods for the full list.
use crate::hdiff::StorageStrategy;
use crate::hot_cold_store::{ColdStateSummary, HotStateSummary};
use crate::{DBColumn, Error, ItemStore};
use crate::{HotColdDB, Split};
use serde::Serialize;
use ssz::Decode;
use std::cmp;
use std::collections::HashSet;
use types::*;
/// Result of running invariant checks on the database.
#[derive(Debug, Clone, Serialize)]
pub struct InvariantCheckResult {
/// List of invariant violations found.
pub violations: Vec<InvariantViolation>,
}
impl InvariantCheckResult {
pub fn new() -> Self {
Self {
violations: Vec::new(),
}
}
pub fn is_ok(&self) -> bool {
self.violations.is_empty()
}
pub fn add_violation(&mut self, violation: InvariantViolation) {
self.violations.push(violation);
}
pub fn merge(&mut self, other: InvariantCheckResult) {
self.violations.extend(other.violations);
}
}
impl Default for InvariantCheckResult {
fn default() -> Self {
Self::new()
}
}
/// Context data from the beacon chain needed for invariant checks.
///
/// This allows all invariant checks to live in the store crate while still checking
/// invariants that depend on fork choice, state cache, and custody context.
pub struct InvariantContext {
/// Block roots tracked by fork choice (invariant 1).
pub fork_choice_blocks: Vec<(Hash256, Slot)>,
/// State roots held in the in-memory state cache (invariant 8).
pub state_cache_roots: Vec<Hash256>,
/// Custody columns for the current epoch (invariant 7).
pub custody_columns: Vec<ColumnIndex>,
/// Compressed pubkey bytes from the in-memory validator pubkey cache, indexed by validator index
/// (invariant 9).
pub pubkey_cache_pubkeys: Vec<Vec<u8>>,
}
/// A single invariant violation.
#[derive(Debug, Clone, Serialize)]
pub enum InvariantViolation {
/// Invariant 1: fork choice block consistency.
///
/// ```text
/// block in fork_choice && descends_from_finalized -> block in hot_db
/// ```
ForkChoiceBlockMissing { block_root: Hash256, slot: Slot },
/// Invariant 2: block and state consistency.
///
/// ```text
/// block in hot_db && block.slot >= split.slot
/// -> state_summary for block.state_root() in hot_db
/// ```
HotBlockMissingStateSummary {
block_root: Hash256,
slot: Slot,
state_root: Hash256,
},
/// Invariant 3: state summary diff consistency.
///
/// ```text
/// state_summary in hot_db
/// -> state diff/snapshot/nothing in hot_db according to hierarchy rules
/// ```
HotStateMissingSnapshot { state_root: Hash256, slot: Slot },
/// Invariant 3: state summary diff consistency (missing diff).
///
/// ```text
/// state_summary in hot_db
/// -> state diff/snapshot/nothing in hot_db according to hierarchy rules
/// ```
HotStateMissingDiff { state_root: Hash256, slot: Slot },
/// Invariant 3: DiffFrom/ReplayFrom base slot must reference an existing summary.
///
/// ```text
/// state_summary in hot_db
/// -> state diff/snapshot/nothing in hot_db according to hierarchy rules
/// ```
HotStateBaseSummaryMissing {
slot: Slot,
base_state_root: Hash256,
},
/// Invariant 4: state summary chain consistency.
///
/// ```text
/// state_summary in hot_db && state_summary.slot > split.slot
/// -> state_summary for previous_state_root in hot_db
/// ```
HotStateMissingPreviousSummary {
slot: Slot,
previous_state_root: Hash256,
},
/// Invariant 5: block and execution payload consistency.
///
/// ```text
/// block in hot_db && !prune_payloads -> payload for block.root in hot_db
/// ```
ExecutionPayloadMissing { block_root: Hash256, slot: Slot },
/// Invariant 6: block and blobs consistency.
///
/// ```text
/// block in hot_db && num_blob_commitments > 0
/// -> blob_list for block.root in hot_db
/// ```
BlobSidecarMissing { block_root: Hash256, slot: Slot },
/// Invariant 7: block and data columns consistency.
///
/// ```text
/// block in hot_db && num_blob_commitments > 0
/// && block.slot >= earliest_available_slot
/// && data_column_idx in custody_columns
/// -> (block_root, data_column_idx) in hot_db
/// ```
DataColumnMissing {
block_root: Hash256,
slot: Slot,
column_index: ColumnIndex,
},
/// Invariant 8: state cache and disk consistency.
///
/// ```text
/// state in state_cache -> state_summary in hot_db
/// ```
StateCacheMissingSummary { state_root: Hash256 },
/// Invariant 9: pubkey cache consistency.
///
/// ```text
/// state_summary in hot_db
/// -> all validator pubkeys from state.validators are in the hot_db
/// ```
PubkeyCacheMissing { validator_index: usize },
/// Invariant 9b: pubkey cache value mismatch.
///
/// ```text
/// pubkey_cache[i] == hot_db(PubkeyCache)[i]
/// ```
PubkeyCacheMismatch { validator_index: usize },
/// Invariant 10: block root indices mapping.
///
/// ```text
/// oldest_block_slot <= i < split.slot
/// -> block_root for slot i in cold_db
/// && block for block_root in hot_db
/// ```
ColdBlockRootMissing {
slot: Slot,
oldest_block_slot: Slot,
split_slot: Slot,
},
/// Invariant 10: block root index references a block that must exist.
///
/// ```text
/// oldest_block_slot <= i < split.slot
/// -> block_root for slot i in cold_db
/// && block for block_root in hot_db
/// ```
ColdBlockRootOrphan { slot: Slot, block_root: Hash256 },
/// Invariant 11: state root indices mapping.
///
/// ```text
/// (i <= state_lower_limit || i >= min(split.slot, state_upper_limit)) && i < split.slot
/// -> i |-> state_root in cold_db(BeaconStateRoots)
/// && state_root |-> cold_state_summary in cold_db(BeaconColdStateSummary)
/// && cold_state_summary.slot == i
/// ```
ColdStateRootMissing {
slot: Slot,
state_lower_limit: Slot,
state_upper_limit: Slot,
split_slot: Slot,
},
/// Invariant 11: state root index must have a cold state summary.
///
/// ```text
/// (i <= state_lower_limit || i >= min(split.slot, state_upper_limit)) && i < split.slot
/// -> i |-> state_root in cold_db(BeaconStateRoots)
/// && state_root |-> cold_state_summary in cold_db(BeaconColdStateSummary)
/// && cold_state_summary.slot == i
/// ```
ColdStateRootMissingSummary { slot: Slot, state_root: Hash256 },
/// Invariant 11: cold state summary slot must match index slot.
///
/// ```text
/// (i <= state_lower_limit || i >= min(split.slot, state_upper_limit)) && i < split.slot
/// -> i |-> state_root in cold_db(BeaconStateRoots)
/// && state_root |-> cold_state_summary in cold_db(BeaconColdStateSummary)
/// && cold_state_summary.slot == i
/// ```
ColdStateRootSlotMismatch {
slot: Slot,
state_root: Hash256,
summary_slot: Slot,
},
/// Invariant 12: cold state diff consistency.
///
/// ```text
/// cold_state_summary in cold_db
/// -> slot |-> state diff/snapshot/nothing in cold_db according to diff hierarchy
/// ```
ColdStateMissingSnapshot { state_root: Hash256, slot: Slot },
/// Invariant 12: cold state diff consistency (missing diff).
///
/// ```text
/// cold_state_summary in cold_db
/// -> slot |-> state diff/snapshot/nothing in cold_db according to diff hierarchy
/// ```
ColdStateMissingDiff { state_root: Hash256, slot: Slot },
/// Invariant 12: DiffFrom/ReplayFrom base slot must reference an existing summary.
///
/// ```text
/// cold_state_summary in cold_db
/// -> slot |-> state diff/snapshot/nothing in cold_db according to diff hierarchy
/// ```
ColdStateBaseSummaryMissing { slot: Slot, base_slot: Slot },
}
impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold> {
/// Run all database invariant checks.
///
/// The `ctx` parameter provides data from the beacon chain layer (fork choice, state cache,
/// custody columns, pubkey cache) so that all invariant checks can live in this single file.
pub fn check_invariants(&self, ctx: &InvariantContext) -> Result<InvariantCheckResult, Error> {
let mut result = InvariantCheckResult::new();
let split = self.get_split_info();
result.merge(self.check_fork_choice_block_consistency(ctx)?);
result.merge(self.check_hot_block_invariants(&split, ctx)?);
result.merge(self.check_hot_state_summary_diff_consistency()?);
result.merge(self.check_hot_state_summary_chain_consistency(&split)?);
result.merge(self.check_state_cache_consistency(ctx)?);
result.merge(self.check_cold_block_root_indices(&split)?);
result.merge(self.check_cold_state_root_indices(&split)?);
result.merge(self.check_cold_state_diff_consistency()?);
result.merge(self.check_pubkey_cache_consistency(ctx)?);
Ok(result)
}
/// Invariant 1 (Hot DB): Fork choice block consistency.
///
/// ```text
/// block in fork_choice && descends_from_finalized -> block in hot_db
/// ```
///
/// Every canonical fork choice block (descending from finalized) must exist in the hot
/// database. Pruned non-canonical fork blocks may linger in the proto-array and are
/// excluded from this check.
fn check_fork_choice_block_consistency(
&self,
ctx: &InvariantContext,
) -> Result<InvariantCheckResult, Error> {
let mut result = InvariantCheckResult::new();
for &(block_root, slot) in &ctx.fork_choice_blocks {
let exists = self
.hot_db
.key_exists(DBColumn::BeaconBlock, block_root.as_slice())?;
if !exists {
result
.add_violation(InvariantViolation::ForkChoiceBlockMissing { block_root, slot });
}
}
Ok(result)
}
/// Invariants 2, 5, 6, 7 (Hot DB): Block-related consistency checks.
///
/// Iterates hot DB blocks once and checks:
/// - Invariant 2: block-state summary consistency
/// - Invariant 5: execution payload consistency (when prune_payloads=false)
/// - Invariant 6: blob sidecar consistency (Deneb to Fulu)
/// - Invariant 7: data column consistency (post-Fulu, when custody_columns provided)
fn check_hot_block_invariants(
&self,
split: &Split,
ctx: &InvariantContext,
) -> Result<InvariantCheckResult, Error> {
let mut result = InvariantCheckResult::new();
let check_payloads = !self.get_config().prune_payloads;
let bellatrix_fork_slot = self
.spec
.bellatrix_fork_epoch
.map(|epoch| epoch.start_slot(E::slots_per_epoch()));
let deneb_fork_slot = self
.spec
.deneb_fork_epoch
.map(|epoch| epoch.start_slot(E::slots_per_epoch()));
let fulu_fork_slot = self
.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;
for res in self.hot_db.iter_column::<Hash256>(DBColumn::BeaconBlock) {
let (block_root, block_bytes) = res?;
let block = SignedBlindedBeaconBlock::<E>::from_ssz_bytes(&block_bytes, &self.spec)?;
let slot = block.slot();
// Invariant 2: block-state consistency.
if slot >= split.slot {
let state_root = block.state_root();
let has_summary = self
.hot_db
.key_exists(DBColumn::BeaconStateHotSummary, state_root.as_slice())?;
if !has_summary {
result.add_violation(InvariantViolation::HotBlockMissingStateSummary {
block_root,
slot,
state_root,
});
}
}
// Invariant 5: execution payload consistency.
if check_payloads
&& let Some(bellatrix_slot) = bellatrix_fork_slot
&& slot >= bellatrix_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.
// Only check blocks that actually have blob KZG commitments — blocks with 0
// commitments legitimately have no blob sidecars stored.
if let Some(deneb_slot) = deneb_fork_slot
&& let Some(oldest_blob) = oldest_blob_slot
&& slot >= deneb_slot
&& slot >= oldest_blob
&& fulu_fork_slot.is_none_or(|fulu_slot| slot < fulu_slot)
&& block.num_expected_blobs() > 0
{
let has_blob = self
.blobs_db
.key_exists(DBColumn::BeaconBlob, block_root.as_slice())?;
if !has_blob {
result
.add_violation(InvariantViolation::BlobSidecarMissing { block_root, slot });
}
}
// Invariant 7: data column consistency.
// Only check blocks that actually have blob KZG commitments.
// TODO(gloas): reconsider this invariant — non-canonical payloads won't have
// their data column sidecars stored.
if !ctx.custody_columns.is_empty()
&& let Some(fulu_slot) = fulu_fork_slot
&& let Some(oldest_dc) = oldest_data_column_slot
&& slot >= fulu_slot
&& slot >= oldest_dc
&& block.num_expected_blobs() > 0
{
let stored_columns = self.get_data_column_keys(block_root)?;
for col_idx in &ctx.custody_columns {
if !stored_columns.contains(col_idx) {
result.add_violation(InvariantViolation::DataColumnMissing {
block_root,
slot,
column_index: *col_idx,
});
}
}
}
}
Ok(result)
}
/// Invariant 3 (Hot DB): State summary diff/snapshot consistency.
///
/// ```text
/// state_summary in hot_db
/// -> state diff/snapshot/nothing in hot_db per HDiff hierarchy rules
/// ```
///
/// Each hot state summary should have the correct storage artifact (snapshot, diff, or
/// nothing) according to the HDiff hierarchy configuration. The hierarchy uses the
/// anchor_slot as its start point for the hot DB.
fn check_hot_state_summary_diff_consistency(&self) -> Result<InvariantCheckResult, Error> {
let mut result = InvariantCheckResult::new();
let anchor_slot = self.get_anchor_info().anchor_slot;
// Collect all summary slots and their strategies in a first pass.
let mut known_state_roots = HashSet::new();
let mut base_state_refs: Vec<(Slot, Hash256)> = Vec::new();
for res in self
.hot_db
.iter_column::<Hash256>(DBColumn::BeaconStateHotSummary)
{
let (state_root, value) = res?;
let summary = HotStateSummary::from_ssz_bytes(&value)?;
known_state_roots.insert(state_root);
match self.hierarchy.storage_strategy(summary.slot, anchor_slot)? {
StorageStrategy::Snapshot => {
let has_snapshot = self
.hot_db
.key_exists(DBColumn::BeaconStateHotSnapshot, state_root.as_slice())?;
if !has_snapshot {
result.add_violation(InvariantViolation::HotStateMissingSnapshot {
state_root,
slot: summary.slot,
});
}
}
StorageStrategy::DiffFrom(base_slot) => {
let has_diff = self
.hot_db
.key_exists(DBColumn::BeaconStateHotDiff, state_root.as_slice())?;
if !has_diff {
result.add_violation(InvariantViolation::HotStateMissingDiff {
state_root,
slot: summary.slot,
});
}
if let Ok(base_root) = summary.diff_base_state.get_root(base_slot) {
base_state_refs.push((summary.slot, base_root));
}
}
StorageStrategy::ReplayFrom(base_slot) => {
if let Ok(base_root) = summary.diff_base_state.get_root(base_slot) {
base_state_refs.push((summary.slot, base_root));
}
}
}
}
// Verify that all diff base state roots reference existing summaries.
for (slot, base_state_root) in base_state_refs {
if !known_state_roots.contains(&base_state_root) {
result.add_violation(InvariantViolation::HotStateBaseSummaryMissing {
slot,
base_state_root,
});
}
}
Ok(result)
}
/// Invariant 4 (Hot DB): State summary chain consistency.
///
/// ```text
/// state_summary in hot_db && state_summary.slot > split.slot
/// -> state_summary for previous_state_root in hot_db
/// ```
///
/// The chain of `previous_state_root` links must be continuous back to the split state.
/// The split state itself is the boundary and does not need a predecessor in the hot DB.
fn check_hot_state_summary_chain_consistency(
&self,
split: &Split,
) -> Result<InvariantCheckResult, Error> {
let mut result = InvariantCheckResult::new();
for res in self
.hot_db
.iter_column::<Hash256>(DBColumn::BeaconStateHotSummary)
{
let (_state_root, value) = res?;
let summary = HotStateSummary::from_ssz_bytes(&value)?;
if summary.slot > split.slot {
let prev_root = summary.previous_state_root;
let has_prev = self
.hot_db
.key_exists(DBColumn::BeaconStateHotSummary, prev_root.as_slice())?;
if !has_prev {
result.add_violation(InvariantViolation::HotStateMissingPreviousSummary {
slot: summary.slot,
previous_state_root: prev_root,
});
}
}
}
Ok(result)
}
/// Invariant 8 (Hot DB): State cache and disk consistency.
///
/// ```text
/// state in state_cache -> state_summary in hot_db
/// ```
///
/// Every state held in the in-memory state cache (including the finalized state) should
/// have a corresponding hot state summary on disk.
fn check_state_cache_consistency(
&self,
ctx: &InvariantContext,
) -> Result<InvariantCheckResult, Error> {
let mut result = InvariantCheckResult::new();
for &state_root in &ctx.state_cache_roots {
let has_summary = self
.hot_db
.key_exists(DBColumn::BeaconStateHotSummary, state_root.as_slice())?;
if !has_summary {
result.add_violation(InvariantViolation::StateCacheMissingSummary { state_root });
}
}
Ok(result)
}
/// Invariant 10 (Cold DB): Block root indices.
///
/// ```text
/// oldest_block_slot <= i < split.slot
/// -> block_root for slot i in cold_db
/// && block for block_root in hot_db
/// ```
///
/// Every slot in the cold range (from `oldest_block_slot` to `split.slot`) should have a
/// block root index entry, and the referenced block should exist in the hot DB. Note that
/// skip slots store the most recent non-skipped block's root, so `block.slot()` may differ
/// from the index slot.
fn check_cold_block_root_indices(&self, split: &Split) -> Result<InvariantCheckResult, Error> {
let mut result = InvariantCheckResult::new();
let anchor_info = self.get_anchor_info();
if anchor_info.oldest_block_slot >= split.slot {
return Ok(result);
}
for slot_val in anchor_info.oldest_block_slot.as_u64()..split.slot.as_u64() {
let slot = Slot::new(slot_val);
let slot_bytes = slot_val.to_be_bytes();
let block_root_bytes = self
.cold_db
.get_bytes(DBColumn::BeaconBlockRoots, &slot_bytes)?;
let Some(root_bytes) = block_root_bytes else {
result.add_violation(InvariantViolation::ColdBlockRootMissing {
slot,
oldest_block_slot: anchor_info.oldest_block_slot,
split_slot: split.slot,
});
continue;
};
if root_bytes.len() != 32 {
return Err(Error::InvalidKey(format!(
"cold block root at slot {slot} has invalid length {}",
root_bytes.len()
)));
}
let block_root = Hash256::from_slice(&root_bytes);
let block_exists = self
.hot_db
.key_exists(DBColumn::BeaconBlock, block_root.as_slice())?;
if !block_exists {
result.add_violation(InvariantViolation::ColdBlockRootOrphan { slot, block_root });
}
}
Ok(result)
}
/// Invariant 11 (Cold DB): State root indices.
///
/// ```text
/// (i <= state_lower_limit || i >= min(split.slot, state_upper_limit)) && i < split.slot
/// -> i |-> state_root in cold_db(BeaconStateRoots)
/// && state_root |-> cold_state_summary in cold_db(BeaconColdStateSummary)
/// && cold_state_summary.slot == i
/// ```
fn check_cold_state_root_indices(&self, split: &Split) -> Result<InvariantCheckResult, Error> {
let mut result = InvariantCheckResult::new();
let anchor_info = self.get_anchor_info();
// Expected slots are: (i <= state_lower_limit || i >= effective_upper) && i < split.slot
// where effective_upper = min(split.slot, state_upper_limit).
for slot_val in 0..split.slot.as_u64() {
let slot = Slot::new(slot_val);
if slot <= anchor_info.state_lower_limit
|| slot >= cmp::min(split.slot, anchor_info.state_upper_limit)
{
let slot_bytes = slot_val.to_be_bytes();
let Some(root_bytes) = self
.cold_db
.get_bytes(DBColumn::BeaconStateRoots, &slot_bytes)?
else {
result.add_violation(InvariantViolation::ColdStateRootMissing {
slot,
state_lower_limit: anchor_info.state_lower_limit,
state_upper_limit: anchor_info.state_upper_limit,
split_slot: split.slot,
});
continue;
};
if root_bytes.len() != 32 {
return Err(Error::InvalidKey(format!(
"cold state root at slot {slot} has invalid length {}",
root_bytes.len()
)));
}
let state_root = Hash256::from_slice(&root_bytes);
match self
.cold_db
.get_bytes(DBColumn::BeaconColdStateSummary, state_root.as_slice())?
{
None => {
result.add_violation(InvariantViolation::ColdStateRootMissingSummary {
slot,
state_root,
});
}
Some(summary_bytes) => {
let summary = ColdStateSummary::from_ssz_bytes(&summary_bytes)?;
if summary.slot != slot {
result.add_violation(InvariantViolation::ColdStateRootSlotMismatch {
slot,
state_root,
summary_slot: summary.slot,
});
}
}
}
}
}
Ok(result)
}
/// Invariant 12 (Cold DB): Cold state diff/snapshot consistency.
///
/// ```text
/// cold_state_summary in cold_db
/// -> state diff/snapshot/nothing in cold_db per HDiff hierarchy rules
/// ```
///
/// Each cold state summary should have the correct storage artifact according to the
/// HDiff hierarchy. Cold states always use genesis (slot 0) as the hierarchy start since
/// they are finalized and have no anchor_slot dependency.
fn check_cold_state_diff_consistency(&self) -> Result<InvariantCheckResult, Error> {
let mut result = InvariantCheckResult::new();
let mut summary_slots = HashSet::new();
let mut base_slot_refs = Vec::new();
for res in self
.cold_db
.iter_column::<Hash256>(DBColumn::BeaconColdStateSummary)
{
let (state_root, value) = res?;
let summary = ColdStateSummary::from_ssz_bytes(&value)?;
summary_slots.insert(summary.slot);
let slot_bytes = summary.slot.as_u64().to_be_bytes();
match self
.hierarchy
.storage_strategy(summary.slot, Slot::new(0))?
{
StorageStrategy::Snapshot => {
let has_snapshot = self
.cold_db
.key_exists(DBColumn::BeaconStateSnapshot, &slot_bytes)?;
if !has_snapshot {
result.add_violation(InvariantViolation::ColdStateMissingSnapshot {
state_root,
slot: summary.slot,
});
}
}
StorageStrategy::DiffFrom(base_slot) => {
let has_diff = self
.cold_db
.key_exists(DBColumn::BeaconStateDiff, &slot_bytes)?;
if !has_diff {
result.add_violation(InvariantViolation::ColdStateMissingDiff {
state_root,
slot: summary.slot,
});
}
base_slot_refs.push((summary.slot, base_slot));
}
StorageStrategy::ReplayFrom(base_slot) => {
base_slot_refs.push((summary.slot, base_slot));
}
}
}
// Verify that all DiffFrom/ReplayFrom base slots reference existing summaries.
for (slot, base_slot) in base_slot_refs {
if !summary_slots.contains(&base_slot) {
result.add_violation(InvariantViolation::ColdStateBaseSummaryMissing {
slot,
base_slot,
});
}
}
Ok(result)
}
/// Invariant 9 (Hot DB): Pubkey cache consistency.
///
/// ```text
/// all validator pubkeys from states are in hot_db(PubkeyCache)
/// ```
///
/// Checks that the in-memory pubkey cache and the on-disk PubkeyCache column have the same
/// number of entries AND that each pubkey matches at every validator index.
fn check_pubkey_cache_consistency(
&self,
ctx: &InvariantContext,
) -> Result<InvariantCheckResult, Error> {
let mut result = InvariantCheckResult::new();
// Read on-disk pubkeys by sequential validator index (matching how they are stored
// with Hash256::from_low_u64_be(index) as key).
// Iterate in-memory pubkeys and verify each matches on disk.
for (validator_index, in_memory_bytes) in ctx.pubkey_cache_pubkeys.iter().enumerate() {
let mut key = [0u8; 32];
key[24..].copy_from_slice(&(validator_index as u64).to_be_bytes());
match self.hot_db.get_bytes(DBColumn::PubkeyCache, &key)? {
Some(on_disk_bytes) if in_memory_bytes != &on_disk_bytes => {
result
.add_violation(InvariantViolation::PubkeyCacheMismatch { validator_index });
}
None => {
result
.add_violation(InvariantViolation::PubkeyCacheMissing { validator_index });
}
_ => {}
}
}
Ok(result)
}
}

View File

@@ -15,6 +15,7 @@ pub mod hdiff;
pub mod historic_state_cache;
pub mod hot_cold_store;
mod impls;
pub mod invariants;
mod memory_store;
pub mod metadata;
pub mod metrics;
@@ -76,11 +77,7 @@ pub trait KeyValueStore<E: EthSpec>: Sync + Send + Sized + 'static {
fn compact(&self) -> Result<(), Error> {
// Compact state and block related columns as they are likely to have the most churn,
// i.e. entries being created and deleted.
for column in [
DBColumn::BeaconState,
DBColumn::BeaconStateHotSummary,
DBColumn::BeaconBlock,
] {
for column in [DBColumn::BeaconStateHotSummary, DBColumn::BeaconBlock] {
self.compact_column(column)?;
}
Ok(())

View File

@@ -111,6 +111,19 @@ impl<E: EthSpec> StateCache<E> {
self.hdiff_buffers.mem_usage()
}
/// Return all state roots currently held in the cache, including the finalized state.
pub fn state_roots(&self) -> Vec<Hash256> {
let mut roots: Vec<Hash256> = self
.states
.iter()
.map(|(&state_root, _)| state_root)
.collect();
if let Some(ref finalized) = self.finalized_state {
roots.push(finalized.state_root);
}
roots
}
pub fn update_finalized_state(
&mut self,
state_root: Hash256,
@@ -332,7 +345,12 @@ impl<E: EthSpec> StateCache<E> {
}
pub fn delete_block_states(&mut self, block_root: &Hash256) {
if let Some(slot_map) = self.block_map.delete_block_states(block_root) {
let (pending_state_roots, full_state_roots) =
self.block_map.delete_block_states(block_root);
for slot_map in [pending_state_roots, full_state_roots]
.into_iter()
.flatten()
{
for state_root in slot_map.slots.values() {
self.states.pop(state_root);
}
@@ -443,11 +461,12 @@ impl BlockMap {
});
}
fn delete_block_states(&mut self, block_root: &Hash256) -> Option<SlotMap> {
// TODO(gloas): update return type
self.blocks
fn delete_block_states(&mut self, block_root: &Hash256) -> (Option<SlotMap>, Option<SlotMap>) {
let pending_state_roots = self
.blocks
.remove(&(*block_root, StatePayloadStatus::Pending));
self.blocks.remove(&(*block_root, StatePayloadStatus::Full))
let full_state_roots = self.blocks.remove(&(*block_root, StatePayloadStatus::Full));
(pending_state_roots, full_state_roots)
}
}