Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/ethlambda/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,7 @@ async fn fetch_initial_state(
// Initialize the store from state + anchor block body, then persist the
// signatures so we can serve the anchor on BlocksByRoot. `insert_signed_block`
// overlaps with what `get_forkchoice_store` already wrote, but it's
// idempotent and the only path that also stores `BlockSignatures`.
// idempotent and the only path that also stores `BlockProof`.
let anchor_root = signed_block.message.header().hash_tree_root();
let mut store = Store::get_forkchoice_store(backend, state, signed_block.message.clone())
.inspect_err(|err| error!(%err, "Failed to initialize store from anchor state and block"))
Expand Down
2 changes: 1 addition & 1 deletion crates/net/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ mod tests {
use libssz::SszEncode;

// Genesis-anchored store: `init_store` writes the header + state but no
// `BlockSignatures` (proof) row. `get_signed_block` synthesizes an empty
// `BlockProof` (proof) row. `get_signed_block` synthesizes an empty
// proof so peers can still receive the genesis block on BlocksByRoot;
// the HTTP endpoint stays consistent and returns 200 rather than 404.
let state = create_test_state();
Expand Down
14 changes: 7 additions & 7 deletions crates/storage/src/api/tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ pub enum Table {
BlockHeaders,
/// Block body storage: H256 -> BlockBody
BlockBodies,
/// Block signatures storage: (slot || root) -> BlockSignatures
/// Block proof storage: (slot || root) -> BlockProof
///
/// Stored separately from blocks because the genesis block has no signatures.
/// Stored separately from blocks because the genesis block has no proof.
/// Keyed by slot || root so pruning can scan in slot order and stop early.
/// Non-genesis blocks have an entry until finalized: signatures below the
/// finalized boundary are pruned (`prune_old_block_signatures`), while
/// Non-genesis blocks have an entry until finalized: proofs below the
/// finalized boundary are pruned (`prune_old_block_proofs`), while
/// headers and bodies are kept forever.
BlockSignatures,
BlockProof,
/// Canonical block index: slot -> block root
BlockRoots,
/// State storage: H256 -> State
Expand Down Expand Up @@ -40,7 +40,7 @@ pub enum Table {
pub const ALL_TABLES: [Table; 8] = [
Table::BlockHeaders,
Table::BlockBodies,
Table::BlockSignatures,
Table::BlockProof,
Table::BlockRoots,
Table::States,
Table::StateDiffs,
Expand All @@ -54,7 +54,7 @@ impl Table {
match self {
Table::BlockHeaders => "block_headers",
Table::BlockBodies => "block_bodies",
Table::BlockSignatures => "block_signatures",
Table::BlockProof => "block_proof",
Table::BlockRoots => "block_roots",
Table::States => "states",
Table::StateDiffs => "state_diffs",
Expand Down
125 changes: 62 additions & 63 deletions crates/storage/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,11 @@ const SNAPSHOT_ANCHOR_INTERVAL: u64 = 1_024;
/// snapshot read or a diff-chain reconstruction.
const STATE_CACHE_CAPACITY: usize = 32;

/// Keep block signatures for at least this many slots below the tip, even once
/// finalized. Signatures older than this window are pruned only when the window
/// lies entirely within finalized history; see [`Store::prune_old_block_signatures`].
/// Keep block proofs for at least this many slots below the tip, even once
/// finalized. Proofs older than this window are pruned only when the window
/// lies entirely within finalized history; see [`Store::prune_old_block_proofs`].
/// ~1 day at 4-second slots.
const SIGNATURE_PRUNING_RANGE: u64 = 21_600;
const BLOCK_PROOF_PRUNING_RANGE: u64 = 21_600;

/// ~30 minutes of resume window at 4-second slots (1800 / 4 = 450).
pub const MAX_RESUMABLE_DB_STATE_AGE: u64 = 450;
Expand Down Expand Up @@ -522,7 +522,7 @@ fn encode_slot_root_key(slot: u64, root: &H256) -> Vec<u8> {
result
}

/// Decode a slot||root key (LiveChain / BlockSignatures) from bytes.
/// Decode a slot||root key (LiveChain / BlockProof) from bytes.
fn decode_slot_root_key(bytes: &[u8]) -> (u64, H256) {
let slot = u64::from_be_bytes(bytes[..8].try_into().expect("valid slot bytes"));
let root = H256::from_slice(&bytes[8..]);
Expand Down Expand Up @@ -916,10 +916,10 @@ impl Store {
Ok(())
}

/// Prune finalized block signatures to keep signature storage bounded.
/// Prune finalized block proofs to keep proof storage bounded.
///
/// State diffs, block headers, block bodies, and full-state snapshots are
/// all retained for the full history and are never pruned. Only signatures
/// all retained for the full history and are never pruned. Only proofs
/// of finalized blocks older than the pruning window are removed.
///
/// This is separated from `update_checkpoints` so callers can defer heavy
Expand All @@ -935,10 +935,10 @@ impl Store {
header.expect("Failed to get block header").slot
});
let pruned_below_slot = self
.prune_old_block_signatures(finalized_slot, tip_slot)
.expect("prune old block signatures");
.prune_old_block_proofs(finalized_slot, tip_slot)
.expect("prune old block proofs");
if pruned_below_slot > 0 {
info!(pruned_below_slot, "Pruned old finalized block signatures");
info!(pruned_below_slot, "Pruned old finalized block proofs");
}
Ok(())
}
Expand Down Expand Up @@ -1088,32 +1088,32 @@ impl Store {
pruned_new + pruned_known
}

/// Prune signatures of old finalized blocks, keeping a recent window.
/// Prune proofs of old finalized blocks, keeping a recent window.
///
/// Signatures within [`SIGNATURE_PRUNING_RANGE`] slots of `tip_slot` are
/// always kept, as are all signatures of non-finalized blocks. Concretely,
/// with `cutoff = tip_slot - SIGNATURE_PRUNING_RANGE`:
/// Proofs within [`BLOCK_PROOF_PRUNING_RANGE`] slots of `tip_slot` are
/// always kept, as are all proofs of non-finalized blocks. Concretely,
/// with `cutoff = tip_slot - BLOCK_PROOF_PRUNING_RANGE`:
///
/// - if `cutoff <= finalized_slot` (healthy finality): delete signatures for
/// - if `cutoff <= finalized_slot` (healthy finality): delete proofs for
/// `slot < cutoff` (entirely within finalized history);
/// - otherwise (the non-finalized range exceeds the window): prune nothing,
/// since pruning up to `cutoff` would touch non-finalized blocks.
///
/// Headers and bodies are always retained. Finalized blocks can never be
/// reverted, so their signatures are not needed for fork choice, re-org
/// reverted, so their proofs are not needed for fork choice, re-org
/// safety, or re-aggregation once outside the window.
///
/// Returns the exclusive slot below which signatures were dropped, or 0 when
/// Returns the exclusive slot below which proofs were dropped, or 0 when
/// nothing was pruned. This is a range delete, so the count of removed keys
/// is not known without reading the table back.
pub fn prune_old_block_signatures(
pub fn prune_old_block_proofs(
&mut self,
finalized_slot: u64,
tip_slot: u64,
) -> Result<u64, Error> {
let cutoff = tip_slot.saturating_sub(SIGNATURE_PRUNING_RANGE);
let cutoff = tip_slot.saturating_sub(BLOCK_PROOF_PRUNING_RANGE);
// Only prune when the whole window is finalized; never touch
// non-finalized signatures. A zero cutoff covers nothing.
// non-finalized proofs. A zero cutoff covers nothing.
if cutoff > finalized_slot || cutoff == 0 {
return Ok(0);
}
Expand All @@ -1126,11 +1126,11 @@ impl Store {
let mut batch = self.backend.begin_write().expect("write batch");
batch
.delete_range(
Table::BlockSignatures,
Table::BlockProof,
&0u64.to_be_bytes(),
&cutoff.to_be_bytes(),
)
.expect("delete finalized block signatures");
.expect("delete finalized block proofs");
batch.commit().expect("commit");

Ok(cutoff)
Expand All @@ -1149,8 +1149,8 @@ impl Store {

/// Insert a block as pending (parent state not yet available).
///
/// Stores block data in `BlockHeaders`/`BlockBodies`/`BlockSignatures`
/// **without** writing to `LiveChain`. This persists the heavy signature
/// Stores block data in `BlockHeaders`/`BlockBodies`/`BlockProof`
/// **without** writing to `LiveChain`. This persists the heavy proof
/// data (~3KB+ per block) to disk while keeping the block invisible to
/// fork choice.
///
Expand Down Expand Up @@ -1222,15 +1222,15 @@ impl Store {
/// Get a signed block by combining header, body, and the merged proof.
///
/// Returns None if the header or body (for non-empty bodies) is missing,
/// or if the signature row is missing for any block other than the
/// or if the proof row is missing for any block other than the
/// slot-0 anchor.
///
/// Signatures are absent in two cases: genesis-style anchor blocks (no
/// proposer ever signed them), and finalized blocks whose signatures were
/// pruned by [`prune_old_block_signatures`](Self::prune_old_block_signatures).
/// Proofs are absent in two cases: genesis-style anchor blocks (no
/// proposer ever signed them), and finalized blocks whose proofs were
/// pruned by [`prune_old_block_proofs`](Self::prune_old_block_proofs).
/// To keep BlocksByRoot symmetric with the fork-choice view for peers,
/// synthesize an empty proof for the slot-0 anchor only; for any other slot
/// a missing signature surfaces as `None` (a pruned finalized block can no
/// a missing proof surfaces as `None` (a pruned finalized block can no
/// longer be served with its proof) rather than as a fabricated block.
pub fn get_signed_block(&self, root: &H256) -> Result<Option<SignedBlock>, Error> {
let view = self.backend.begin_read().expect("read view");
Expand All @@ -1252,7 +1252,7 @@ impl Store {
};

let sig_key = encode_slot_root_key(header.slot, root);
let proof = match view.get(Table::BlockSignatures, &sig_key).expect("get") {
let proof = match view.get(Table::BlockProof, &sig_key).expect("get") {
Some(proof_bytes) => {
MultiMessageAggregate::from_ssz_bytes(&proof_bytes).expect("valid block proof")
}
Expand Down Expand Up @@ -1716,12 +1716,11 @@ fn write_signed_block(
.expect("put block body");
}

// Store the merged multi-message aggregate proof blob, keyed by slot||root so signature
// pruning can scan in slot order and stop early. Table name kept for the
// column-family migration cost; renaming to `BlockProof` is a follow-up.
// Store the merged multi-message aggregate proof blob, keyed by slot||root
// so proof pruning can scan in slot order and stop early.
let proof_entries = vec![(encode_slot_root_key(header.slot, root), proof.to_ssz())];
batch
.put_batch(Table::BlockSignatures, proof_entries)
.put_batch(Table::BlockProof, proof_entries)
.expect("put block proof");

block
Expand Down Expand Up @@ -1758,7 +1757,7 @@ mod tests {
}
}

/// Insert a block header (and dummy body + signature) for a given root, slot,
/// Insert a block header (and dummy body + proof) for a given root, slot,
/// and parent. The stored header equals `header_at(slot, parent_root)`, so a
/// state built from the same `(slot, parent_root)` reconstructs byte-identically.
fn insert_header(backend: &dyn StorageBackend, root: H256, slot: u64, parent_root: H256) {
Expand All @@ -1773,10 +1772,10 @@ mod tests {
.expect("put body");
batch
.put_batch(
Table::BlockSignatures,
Table::BlockProof,
vec![(encode_slot_root_key(slot, &root), vec![0u8; 4])],
)
.expect("put sigs");
.expect("put proof");
batch
.put_batch(
Table::BlockRoots,
Expand Down Expand Up @@ -1810,10 +1809,10 @@ mod tests {
view.get(table, &root.to_ssz()).expect("get").is_some()
}

/// Check whether a block signature exists for a (slot, root) pair.
fn has_signature(backend: &dyn StorageBackend, slot: u64, root: &H256) -> bool {
/// Check whether a block proof exists for a (slot, root) pair.
fn has_block_proof(backend: &dyn StorageBackend, slot: u64, root: &H256) -> bool {
let view = backend.begin_read().expect("read view");
view.get(Table::BlockSignatures, &encode_slot_root_key(slot, root))
view.get(Table::BlockProof, &encode_slot_root_key(slot, root))
.expect("get")
.is_some()
}
Expand Down Expand Up @@ -1964,33 +1963,33 @@ mod tests {
}

#[test]
fn prune_old_blocks_within_retention() {
fn prune_old_block_proofs_within_retention() {
let backend = Arc::new(InMemoryBackend::new());
let mut store = Store::test_store_with_backend(backend.clone());

// Blocks at slots 0..12, each with header + body + signature.
// Blocks at slots 0..12, each with header + body + proof.
for i in 0..13u64 {
insert_header(backend.as_ref(), root(i), i, H256::ZERO);
}

// Healthy finality: non-finalized gap (5) < SIGNATURE_PRUNING_RANGE.
// Healthy finality: non-finalized gap (5) < BLOCK_PROOF_PRUNING_RANGE.
// tip = range + 10, finalized = range + 5, so cutoff = tip - range = 10.
let tip_slot = SIGNATURE_PRUNING_RANGE + 10;
let finalized_slot = SIGNATURE_PRUNING_RANGE + 5;
let tip_slot = BLOCK_PROOF_PRUNING_RANGE + 10;
let finalized_slot = BLOCK_PROOF_PRUNING_RANGE + 5;
let pruned_below_slot = store
.prune_old_block_signatures(finalized_slot, tip_slot)
.prune_old_block_proofs(finalized_slot, tip_slot)
.expect("prune");

// cutoff = 10: slots 0..9 pruned, slots 10..12 kept (within the window).
assert_eq!(pruned_below_slot, 10);
assert_eq!(count_entries(backend.as_ref(), Table::BlockSignatures), 3);
assert_eq!(count_entries(backend.as_ref(), Table::BlockProof), 3);

// Oldest signatures are gone, but headers, bodies, and roots stay queryable.
// Oldest proofs are gone, but headers, bodies, and roots stay queryable.
for i in 0..10u64 {
assert!(!has_signature(backend.as_ref(), i, &root(i)));
assert!(!has_block_proof(backend.as_ref(), i, &root(i)));
}
for i in 10..13u64 {
assert!(has_signature(backend.as_ref(), i, &root(i)));
assert!(has_block_proof(backend.as_ref(), i, &root(i)));
}

// Headers and bodies are always retained for the whole history.
Expand All @@ -2000,39 +1999,39 @@ mod tests {
}

#[test]
fn prune_signatures_noop_when_non_finalized_range_exceeds_window() {
fn prune_block_proofs_noop_when_non_finalized_range_exceeds_window() {
let backend = Arc::new(InMemoryBackend::new());
let mut store = Store::test_store_with_backend(backend.clone());

for i in 0..10u64 {
insert_header(backend.as_ref(), root(i), i, H256::ZERO);
}

// Deep non-finality: gap (tip - finalized) > SIGNATURE_PRUNING_RANGE, so
// Deep non-finality: gap (tip - finalized) > BLOCK_PROOF_PRUNING_RANGE, so
// cutoff = tip - range > finalized → prune nothing.
let tip_slot = SIGNATURE_PRUNING_RANGE + 100;
let tip_slot = BLOCK_PROOF_PRUNING_RANGE + 100;
let finalized_slot = 5;
let pruned_below_slot = store
.prune_old_block_signatures(finalized_slot, tip_slot)
.prune_old_block_proofs(finalized_slot, tip_slot)
.expect("prune");
assert_eq!(pruned_below_slot, 0);
assert_eq!(count_entries(backend.as_ref(), Table::BlockSignatures), 10);
assert_eq!(count_entries(backend.as_ref(), Table::BlockProof), 10);
}

#[test]
fn prune_signatures_noop_when_tip_within_window() {
fn prune_block_proofs_noop_when_tip_within_window() {
let backend = Arc::new(InMemoryBackend::new());
let mut store = Store::test_store_with_backend(backend.clone());

for i in 0..10u64 {
insert_header(backend.as_ref(), root(i), i, H256::ZERO);
}

// Early chain: tip < SIGNATURE_PRUNING_RANGE → cutoff saturates to 0,
// Early chain: tip < BLOCK_PROOF_PRUNING_RANGE → cutoff saturates to 0,
// so nothing is old enough to prune even though slots are finalized.
let pruned_below_slot = store.prune_old_block_signatures(9, 9).expect("prune");
let pruned_below_slot = store.prune_old_block_proofs(9, 9).expect("prune");
assert_eq!(pruned_below_slot, 0);
assert_eq!(count_entries(backend.as_ref(), Table::BlockSignatures), 10);
assert_eq!(count_entries(backend.as_ref(), Table::BlockProof), 10);
}

// ============ State Diff Reconstruction Tests ============
Expand Down Expand Up @@ -2920,7 +2919,7 @@ mod tests {
assert_eq!(buf.len(), 2);
}

/// `Store::from_anchor_state` writes the header but no `BlockSignatures`
/// `Store::from_anchor_state` writes the header but no `BlockProof`
/// row for the slot-0 anchor. `get_signed_block` must synthesize an empty
/// proof so the genesis block can still be served on BlocksByRoot /
/// `/lean/v0/blocks/finalized`.
Expand All @@ -2940,14 +2939,14 @@ mod tests {
}

/// The synthesis branch must be confined to the slot-0 anchor: a
/// non-genesis block whose `BlockSignatures` row is missing is treated
/// non-genesis block whose `BlockProof` row is missing is treated
/// as storage corruption and surfaces as `None`, not a fabricated block.
#[test]
fn get_signed_block_returns_none_for_non_genesis_with_missing_signatures() {
fn get_signed_block_returns_none_for_non_genesis_with_missing_proof() {
let backend: Arc<dyn StorageBackend> = Arc::new(InMemoryBackend::new());

// Hand-insert a slot-1 header (and empty body, via `EMPTY_BODY_ROOT`)
// but skip the `BlockSignatures` row. This mimics the corruption case
// but skip the `BlockProof` row. This mimics the corruption case
// the guard is meant to catch, without going through the normal
// `insert_signed_block` write path which always writes all three rows.
let header = BlockHeader {
Expand Down
Loading
Loading