Skip to content

Commit 62e20ec

Browse files
authored
fix: prune finalized blocks and attestations (#83)
Implements automatic pruning of finalized data to prevent unbounded storage growth. Adds a LiveChain index table to optimize fork choice operations while enabling efficient cleanup of finalized blocks, attestations, and signatures. ## Summary When slots become finalized, they no longer participate in fork choice. This PR: 1. Adds a LiveChain index for fast fork choice (avoids Block deserialization) 2. Automatically prunes finalized data when checkpoints advance 3. Moves signature-related types to storage module for proper encapsulation ## Key Changes ### LiveChain Index - New `LiveChain` table: `(slot, root) -> parent_root` mappings (40 bytes per block) - Big-endian slot encoding enables efficient range scans and early termination - Refactored `compute_lmd_ghost_head` to use `HashMap<H256, (u64, H256)>` instead of full `Block` - Automatic pruning removes finalized entries while preserving Blocks table for historical queries ### Automatic Pruning - `update_checkpoints()` triggers pruning when finalization advances - **LiveChain**: Prunes index entries for slots < finalized_slot (keeps finalized block itself) - **GossipSignatures**: Removes signatures for finalized attestations (no longer needed for block building) - **AggregatedPayloads**: Removes aggregated proofs for finalized attestations ## Implementation Details **Why pruning on finalization?** - Finalized attestations can't contribute to future fork choice decisions - Gossip signatures are only needed for building new blocks on live forks - Aggregated payloads from finalized blocks won't be reused ## Testing Ran a validator node for 1 hour with these changes. Storage reached ~1GB with a downward slope (actively pruning) compared to ~5GB before pruning was implemented. Also tested via existing spec tests: - `forkchoice_spectests.rs`: Fork choice with LiveChain index - `signature_spectests.rs`: Block building with new signature types - `stf_spectests.rs`: State transition with pruned storage
1 parent 99b989b commit 62e20ec

24 files changed

Lines changed: 400 additions & 118 deletions

File tree

crates/blockchain/fork_choice/src/lib.rs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::collections::HashMap;
22

3-
use ethlambda_types::{attestation::AttestationData, block::Block, primitives::H256};
3+
use ethlambda_types::{attestation::AttestationData, primitives::H256};
44

55
/// Compute the LMD GHOST head of the chain, given a starting root, a set of blocks,
66
/// a set of attestations, and a minimum score threshold.
@@ -9,7 +9,7 @@ use ethlambda_types::{attestation::AttestationData, block::Block, primitives::H2
99
// TODO: add proto-array implementation
1010
pub fn compute_lmd_ghost_head(
1111
mut start_root: H256,
12-
blocks: &HashMap<H256, Block>,
12+
blocks: &HashMap<H256, (u64, H256)>,
1313
attestations: &HashMap<u64, AttestationData>,
1414
min_score: u64,
1515
) -> H256 {
@@ -19,36 +19,33 @@ pub fn compute_lmd_ghost_head(
1919
if start_root.is_zero() {
2020
start_root = *blocks
2121
.iter()
22-
.min_by_key(|(_, block)| block.slot)
22+
.min_by_key(|(_, (slot, _))| slot)
2323
.map(|(root, _)| root)
2424
.expect("we already checked blocks is non-empty");
2525
}
26-
let start_slot = blocks[&start_root].slot;
26+
let start_slot = blocks[&start_root].0;
2727
let mut weights: HashMap<H256, u64> = HashMap::new();
2828

2929
for attestation_data in attestations.values() {
3030
let mut current_root = attestation_data.head.root;
31-
while let Some(block) = blocks.get(&current_root)
32-
&& block.slot > start_slot
31+
while let Some(&(slot, parent_root)) = blocks.get(&current_root)
32+
&& slot > start_slot
3333
{
3434
*weights.entry(current_root).or_default() += 1;
35-
current_root = block.parent_root;
35+
current_root = parent_root;
3636
}
3737
}
3838

3939
let mut children_map: HashMap<H256, Vec<H256>> = HashMap::new();
4040

41-
for (root, block) in blocks {
42-
if block.parent_root.is_zero() {
41+
for (root, &(_, parent_root)) in blocks {
42+
if parent_root.is_zero() {
4343
continue;
4444
}
4545
if min_score > 0 && *weights.get(root).unwrap_or(&0) < min_score {
4646
continue;
4747
}
48-
children_map
49-
.entry(block.parent_root)
50-
.or_default()
51-
.push(*root);
48+
children_map.entry(parent_root).or_default().push(*root);
5249
}
5350

5451
let mut head = start_root;

crates/blockchain/src/key_manager.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::HashMap;
22

33
use ethlambda_types::{
44
attestation::{AttestationData, XmssSignature},
5-
primitives::{H256, TreeHash},
5+
primitives::{H256, ssz::TreeHash},
66
signature::{ValidatorSecretKey, ValidatorSignature},
77
};
88

crates/blockchain/src/lib.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ use std::time::{Duration, SystemTime};
33

44
use ethlambda_state_transition::is_proposer;
55
use ethlambda_storage::Store;
6-
use ethlambda_types::primitives::H256;
76
use ethlambda_types::{
87
ShortRoot,
98
attestation::{Attestation, AttestationData, SignedAttestation},
109
block::{BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation},
11-
primitives::TreeHash,
10+
primitives::{H256, ssz::TreeHash},
1211
signature::ValidatorSecretKey,
1312
state::Checkpoint,
1413
};

crates/blockchain/src/store.rs

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use ethlambda_types::{
1212
AggregatedAttestations, AggregatedSignatureProof, AggregationBits, Block, BlockBody,
1313
SignedBlockWithAttestation,
1414
},
15-
primitives::{H256, TreeHash},
15+
primitives::{H256, ssz::TreeHash},
1616
signature::ValidatorSignature,
1717
state::{Checkpoint, State, Validator},
1818
};
@@ -30,7 +30,7 @@ fn accept_new_attestations(store: &mut Store) {
3030

3131
/// Update the head based on the fork choice rule.
3232
fn update_head(store: &mut Store) {
33-
let blocks: HashMap<H256, Block> = store.iter_blocks().collect();
33+
let blocks = store.get_live_chain();
3434
let attestations: HashMap<u64, AttestationData> = store.iter_known_attestations().collect();
3535
let old_head = store.head();
3636
let new_head = ethlambda_fork_choice::compute_lmd_ghost_head(
@@ -69,7 +69,7 @@ fn update_safe_target(store: &mut Store) {
6969

7070
let min_target_score = (num_validators * 2).div_ceil(3);
7171

72-
let blocks: HashMap<H256, Block> = store.iter_blocks().collect();
72+
let blocks = store.get_live_chain();
7373
let attestations: HashMap<u64, AttestationData> = store.iter_new_attestations().collect();
7474
let safe_target = ethlambda_fork_choice::compute_lmd_ghost_head(
7575
store.latest_justified().root,
@@ -217,10 +217,9 @@ pub fn on_gossip_attestation(
217217

218218
if cfg!(not(feature = "skip-signature-verification")) {
219219
// Store signature for later lookup during block building
220-
let signature_key = (validator_id, message);
221220
let signature = ValidatorSignature::from_bytes(&signed_attestation.signature)
222221
.map_err(|_| StoreError::SignatureDecodingFailed)?;
223-
store.insert_gossip_signature(signature_key, signature);
222+
store.insert_gossip_signature(&attestation.data, validator_id, signature);
224223
}
225224
metrics::inc_attestations_valid("gossip");
226225

@@ -382,12 +381,10 @@ pub fn on_block(
382381
.zip(attestation_signatures.iter())
383382
{
384383
let validator_ids = aggregation_bits_to_validator_indices(&att.aggregation_bits);
385-
let data_root = att.data.tree_hash_root();
386384

387385
for validator_id in validator_ids {
388386
// Update Proof Map - Store the proof so future block builders can reuse this aggregation
389-
let key: SignatureKey = (validator_id, data_root);
390-
store.push_aggregated_payload(key, proof.clone());
387+
store.insert_aggregated_payload(&att.data, validator_id, proof.clone());
391388

392389
// Update Fork Choice - Register the vote immediately (historical/on-chain)
393390
let attestation = Attestation {
@@ -415,14 +412,14 @@ pub fn on_block(
415412

416413
if cfg!(not(feature = "skip-signature-verification")) {
417414
// Store the proposer's signature for potential future block building
418-
let proposer_sig_key: SignatureKey = (
419-
proposer_attestation.validator_id,
420-
proposer_attestation.data.tree_hash_root(),
421-
);
422415
let proposer_sig =
423416
ValidatorSignature::from_bytes(&signed_block.signature.proposer_signature)
424417
.map_err(|_| StoreError::SignatureDecodingFailed)?;
425-
store.insert_gossip_signature(proposer_sig_key, proposer_sig);
418+
store.insert_gossip_signature(
419+
&proposer_attestation.data,
420+
proposer_attestation.validator_id,
421+
proposer_sig,
422+
);
426423
}
427424

428425
// Process proposer attestation (enters "new" stage, not "known")
@@ -559,13 +556,20 @@ pub fn produce_block_with_signatures(
559556
.collect();
560557

561558
// Get known block roots for attestation validation
562-
let known_block_roots: HashSet<H256> = store.iter_blocks().map(|(root, _)| root).collect();
559+
let known_block_roots = store.get_block_roots();
563560

564561
// Collect signature data for block building
565-
let gossip_signatures: HashMap<SignatureKey, ValidatorSignature> =
566-
store.iter_gossip_signatures().collect();
567-
let aggregated_payloads: HashMap<SignatureKey, Vec<AggregatedSignatureProof>> =
568-
store.iter_aggregated_payloads().collect();
562+
let gossip_signatures: HashMap<SignatureKey, ValidatorSignature> = store
563+
.iter_gossip_signatures()
564+
.filter_map(|(key, stored)| stored.to_validator_signature().ok().map(|sig| (key, sig)))
565+
.collect();
566+
let aggregated_payloads: HashMap<SignatureKey, Vec<AggregatedSignatureProof>> = store
567+
.iter_aggregated_payloads()
568+
.map(|(key, stored_payloads)| {
569+
let proofs = stored_payloads.into_iter().map(|sp| sp.proof).collect();
570+
(key, proofs)
571+
})
572+
.collect();
569573

570574
// Build the block using fixed-point attestation collection
571575
let (block, _post_state, signatures) = build_block(

crates/blockchain/state_transition/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::collections::HashMap;
33
use ethlambda_types::{
44
ShortRoot,
55
block::{AggregatedAttestations, Block, BlockHeader},
6-
primitives::{H256, TreeHash},
6+
primitives::{H256, ssz::TreeHash},
77
state::{Checkpoint, JustificationValidators, State},
88
};
99
use tracing::info;

crates/blockchain/tests/forkchoice_spectests.rs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use ethlambda_storage::{Store, backend::InMemoryBackend};
99
use ethlambda_types::{
1010
attestation::Attestation,
1111
block::{Block, BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation},
12-
primitives::{H256, TreeHash, VariableList},
12+
primitives::{H256, VariableList, ssz::TreeHash},
1313
state::State,
1414
};
1515

@@ -165,15 +165,15 @@ fn validate_checks(
165165
}
166166

167167
// Also validate the root matches a block at this slot
168-
let blocks: HashMap<H256, Block> = st.iter_blocks().collect();
168+
let blocks = st.get_live_chain();
169169
let block_found = blocks
170170
.iter()
171-
.any(|(root, block)| block.slot == expected_slot && *root == target.root);
171+
.any(|(root, (slot, _))| *slot == expected_slot && *root == target.root);
172172

173173
if !block_found {
174174
let available: Vec<_> = blocks
175175
.iter()
176-
.filter(|(_, block)| block.slot == expected_slot)
176+
.filter(|(_, (slot, _))| *slot == expected_slot)
177177
.map(|(root, _)| format!("{:?}", root))
178178
.collect();
179179
return Err(format!(
@@ -365,7 +365,7 @@ fn validate_lexicographic_head_among(
365365
.into());
366366
}
367367

368-
let blocks: HashMap<H256, Block> = st.iter_blocks().collect();
368+
let blocks = st.get_live_chain();
369369
let known_attestations: HashMap<u64, AttestationData> = st.iter_known_attestations().collect();
370370

371371
// Resolve all fork labels to roots and compute their weights
@@ -380,13 +380,12 @@ fn validate_lexicographic_head_among(
380380
)
381381
})?;
382382

383-
let block = blocks.get(root).ok_or_else(|| {
383+
let (slot, _parent_root) = blocks.get(root).ok_or_else(|| {
384384
format!(
385385
"Step {}: block for label '{}' not found in store",
386386
step_idx, label
387387
)
388388
})?;
389-
let slot = block.slot;
390389

391390
// Calculate attestation weight: count attestations voting for this fork
392391
// An attestation votes for this fork if its head is this block or a descendant
@@ -396,26 +395,26 @@ fn validate_lexicographic_head_among(
396395
// Check if attestation head is this block or a descendant
397396
if att_head_root == *root {
398397
weight += 1;
399-
} else if let Some(att_block) = blocks.get(&att_head_root) {
398+
} else if let Some(&(att_slot, _)) = blocks.get(&att_head_root) {
400399
// Walk back from attestation head to see if we reach this block
401400
let mut current = att_head_root;
402-
let mut current_slot = att_block.slot;
403-
while current_slot > slot {
404-
if let Some(blk) = blocks.get(&current) {
405-
if blk.parent_root == *root {
401+
let mut current_slot = att_slot;
402+
while current_slot > *slot {
403+
if let Some(&(_, parent_root)) = blocks.get(&current) {
404+
if parent_root == *root {
406405
weight += 1;
407406
break;
408407
}
409-
current = blk.parent_root;
410-
current_slot = blocks.get(&current).map(|b| b.slot).unwrap_or(0);
408+
current = parent_root;
409+
current_slot = blocks.get(&current).map(|(s, _)| *s).unwrap_or(0);
411410
} else {
412411
break;
413412
}
414413
}
415414
}
416415
}
417416

418-
fork_data.insert(label.as_str(), (*root, slot, weight));
417+
fork_data.insert(label.as_str(), (*root, *slot, weight));
419418
}
420419

421420
// Verify all forks are at the same slot

crates/blockchain/tests/signature_spectests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use ethlambda_blockchain::{SECONDS_PER_SLOT, store};
55
use ethlambda_storage::{Store, backend::InMemoryBackend};
66
use ethlambda_types::{
77
block::{Block, SignedBlockWithAttestation},
8-
primitives::TreeHash,
8+
primitives::ssz::TreeHash,
99
state::State,
1010
};
1111

crates/blockchain/tests/signature_types.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ use ethlambda_types::block::{
77
AttestationSignatures, Block as EthBlock, BlockBody as EthBlockBody, BlockSignatures,
88
BlockWithAttestation, SignedBlockWithAttestation,
99
};
10-
use ethlambda_types::primitives::{BitList, Encode, H256, VariableList};
10+
use ethlambda_types::primitives::{
11+
BitList, H256, VariableList,
12+
ssz::{Decode as SszDecode, Encode as SszEncode},
13+
};
1114
use ethlambda_types::state::{Checkpoint as EthCheckpoint, State, ValidatorPubkeyBytes};
1215
use serde::Deserialize;
13-
use ssz_derive::{Decode as SszDecode, Encode as SszEncode};
1416
use ssz_types::FixedVector;
1517
use ssz_types::typenum::{U28, U32};
1618
use std::collections::HashMap;

crates/common/crypto/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::sync::Once;
22

3-
use ethlambda_types::primitives::{Decode, Encode};
43
use ethlambda_types::{
54
block::ByteListMiB,
6-
primitives::H256,
5+
primitives::{
6+
H256,
7+
ssz::{Decode, Encode},
8+
},
79
signature::{ValidatorPublicKey, ValidatorSignature},
810
};
911
use lean_multisig::{

crates/common/types/src/attestation.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
use ssz_derive::{Decode, Encode};
2-
use tree_hash_derive::TreeHash;
3-
41
use crate::{
2+
primitives::ssz::{Decode, Encode, TreeHash},
53
signature::SignatureSize,
64
state::{Checkpoint, ValidatorRegistryLimit},
75
};

0 commit comments

Comments
 (0)