From fdd2fd0d965b9bfca09097dccb824830ec3fbf2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:19:17 -0300 Subject: [PATCH 01/10] refactor: split blocks into headers and bodies --- crates/blockchain/src/store.rs | 60 +++++++-------- .../blockchain/tests/forkchoice_spectests.rs | 8 +- crates/common/types/src/block.rs | 32 ++++++++ crates/net/p2p/src/req_resp/handlers.rs | 2 +- crates/storage/src/api/tables.rs | 11 ++- crates/storage/src/backend/rocksdb.rs | 7 +- crates/storage/src/backend/tests.rs | 30 ++++---- crates/storage/src/store.rs | 77 ++++++++++++++----- 8 files changed, 151 insertions(+), 76 deletions(-) diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index c41af16..7e758d8 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -46,8 +46,8 @@ fn update_head(store: &mut Store) { store.update_checkpoints(ForkCheckpoints::head_only(new_head)); if old_head != new_head { - let old_slot = store.get_block(&old_head).map(|b| b.slot).unwrap_or(0); - let new_slot = store.get_block(&new_head).map(|b| b.slot).unwrap_or(0); + let old_slot = store.get_block_header(&old_head).map(|h| h.slot).unwrap_or(0); + let new_slot = store.get_block_header(&new_head).map(|h| h.slot).unwrap_or(0); let justified_slot = store.latest_justified().slot; let finalized_slot = store.latest_finalized().slot; info!( @@ -91,11 +91,11 @@ fn validate_attestation(store: &Store, attestation: &Attestation) -> Result<(), let data = &attestation.data; // Availability Check - We cannot count a vote if we haven't seen the blocks involved. - let source_block = store - .get_block(&data.source.root) + let source_header = store + .get_block_header(&data.source.root) .ok_or(StoreError::UnknownSourceBlock(data.source.root))?; - let target_block = store - .get_block(&data.target.root) + let target_header = store + .get_block_header(&data.target.root) .ok_or(StoreError::UnknownTargetBlock(data.target.root))?; if !store.contains_block(&data.head.root) { @@ -108,16 +108,16 @@ fn validate_attestation(store: &Store, attestation: &Attestation) -> Result<(), } // Consistency Check - Validate checkpoint slots match block slots. - if source_block.slot != data.source.slot { + if source_header.slot != data.source.slot { return Err(StoreError::SourceSlotMismatch { checkpoint_slot: data.source.slot, - block_slot: source_block.slot, + block_slot: source_header.slot, }); } - if target_block.slot != data.target.slot { + if target_header.slot != data.target.slot { return Err(StoreError::TargetSlotMismatch { checkpoint_slot: data.target.slot, - block_slot: target_block.slot, + block_slot: target_header.slot, }); } @@ -441,12 +441,12 @@ pub fn on_block( pub fn get_attestation_target(store: &Store) -> Checkpoint { // Start from current head let mut target_block_root = store.head(); - let mut target_block = store - .get_block(&target_block_root) + let mut target_header = store + .get_block_header(&target_block_root) .expect("head block exists"); let safe_target_block_slot = store - .get_block(&store.safe_target()) + .get_block_header(&store.safe_target()) .expect("safe target exists") .slot; @@ -455,10 +455,10 @@ pub fn get_attestation_target(store: &Store) -> Checkpoint { // This ensures the target doesn't advance too far ahead of safe target, // providing a balance between liveness and safety. for _ in 0..JUSTIFICATION_LOOKBACK_SLOTS { - if target_block.slot > safe_target_block_slot { - target_block_root = target_block.parent_root; - target_block = store - .get_block(&target_block_root) + if target_header.slot > safe_target_block_slot { + target_block_root = target_header.parent_root; + target_header = store + .get_block_header(&target_block_root) .expect("parent block exists"); } else { break; @@ -469,15 +469,15 @@ pub fn get_attestation_target(store: &Store) -> Checkpoint { // // Walk back until we find a slot that satisfies justifiability rules // relative to the latest finalized checkpoint. - while !slot_is_justifiable_after(target_block.slot, store.latest_finalized().slot) { - target_block_root = target_block.parent_root; - target_block = store - .get_block(&target_block_root) + while !slot_is_justifiable_after(target_header.slot, store.latest_finalized().slot) { + target_block_root = target_header.parent_root; + target_header = store + .get_block_header(&target_block_root) .expect("parent block exists"); } Checkpoint { root: target_block_root, - slot: target_block.slot, + slot: target_header.slot, } } @@ -487,7 +487,7 @@ pub fn produce_attestation_data(store: &Store, slot: u64) -> AttestationData { let head_checkpoint = Checkpoint { root: store.head(), slot: store - .get_block(&store.head()) + .get_block_header(&store.head()) .expect("head block exists") .slot, }; @@ -1055,16 +1055,16 @@ fn is_reorg(old_head: H256, new_head: H256, store: &Store) -> bool { return false; } - let Some(old_head_block) = store.get_block(&old_head) else { + let Some(old_head_header) = store.get_block_header(&old_head) else { return false; }; - let Some(new_head_block) = store.get_block(&new_head) else { + let Some(new_head_header) = store.get_block_header(&new_head) else { return false; }; - let old_slot = old_head_block.slot; - let new_slot = new_head_block.slot; + let old_slot = old_head_header.slot; + let new_slot = new_head_header.slot; // Determine which head has the higher slot and walk back from it let (mut current_root, target_slot, target_root) = if new_slot >= old_slot { @@ -1074,12 +1074,12 @@ fn is_reorg(old_head: H256, new_head: H256, store: &Store) -> bool { }; // Walk back through the chain until we reach the target slot - while let Some(current_block) = store.get_block(¤t_root) { - if current_block.slot <= target_slot { + while let Some(current_header) = store.get_block_header(¤t_root) { + if current_header.slot <= target_slot { // We've reached the target slot - check if we're at the target block return current_root != target_root; } - current_root = current_block.parent_root; + current_root = current_header.parent_root; } // Couldn't walk back far enough (missing blocks in chain) diff --git a/crates/blockchain/tests/forkchoice_spectests.rs b/crates/blockchain/tests/forkchoice_spectests.rs index b35385c..e1c69e1 100644 --- a/crates/blockchain/tests/forkchoice_spectests.rs +++ b/crates/blockchain/tests/forkchoice_spectests.rs @@ -187,13 +187,13 @@ fn validate_checks( // Validate headSlot if let Some(expected_slot) = checks.head_slot { let head_root = st.head(); - let head_block = st - .get_block(&head_root) + let head_header = st + .get_block_header(&head_root) .ok_or_else(|| format!("Step {}: head block not found", step_idx))?; - if head_block.slot != expected_slot { + if head_header.slot != expected_slot { return Err(format!( "Step {}: headSlot mismatch: expected {}, got {}", - step_idx, expected_slot, head_block.slot + step_idx, expected_slot, head_header.slot ) .into()); } diff --git a/crates/common/types/src/block.rs b/crates/common/types/src/block.rs index d54e050..4cbf2f1 100644 --- a/crates/common/types/src/block.rs +++ b/crates/common/types/src/block.rs @@ -205,6 +205,38 @@ pub struct Block { pub body: BlockBody, } +impl Block { + /// Extract the block header, computing the body root. + pub fn header(&self) -> BlockHeader { + BlockHeader { + slot: self.slot, + proposer_index: self.proposer_index, + parent_root: self.parent_root, + state_root: self.state_root, + body_root: self.body.tree_hash_root(), + } + } + + /// Reconstruct a block from header and body. + /// + /// # Panics + /// Panics if the body root doesn't match the header's body_root. + pub fn from_header_and_body(header: BlockHeader, body: BlockBody) -> Self { + debug_assert_eq!( + header.body_root, + body.tree_hash_root(), + "body root mismatch" + ); + Self { + slot: header.slot, + proposer_index: header.proposer_index, + parent_root: header.parent_root, + state_root: header.state_root, + body, + } + } +} + /// The body of a block, containing payload data. /// /// Currently, the main operation is voting. Validators submit attestations which are diff --git a/crates/net/p2p/src/req_resp/handlers.rs b/crates/net/p2p/src/req_resp/handlers.rs index 962a9fe..07c5e74 100644 --- a/crates/net/p2p/src/req_resp/handlers.rs +++ b/crates/net/p2p/src/req_resp/handlers.rs @@ -169,7 +169,7 @@ async fn handle_blocks_by_root_response( pub fn build_status(store: &Store) -> Status { let finalized = store.latest_finalized(); let head_root = store.head(); - let head_slot = store.get_block(&head_root).expect("head block exists").slot; + let head_slot = store.get_block_header(&head_root).expect("head block exists").slot; Status { finalized, head: ethlambda_types::state::Checkpoint { diff --git a/crates/storage/src/api/tables.rs b/crates/storage/src/api/tables.rs index 36ed953..deecdad 100644 --- a/crates/storage/src/api/tables.rs +++ b/crates/storage/src/api/tables.rs @@ -1,8 +1,10 @@ /// Tables in the storage layer. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Table { - /// Block storage: H256 -> Block - Blocks, + /// Block header storage: H256 -> BlockHeader + BlockHeaders, + /// Block body storage: H256 -> BlockBody + BlockBodies, /// Block signatures storage: H256 -> BlockSignaturesWithAttestation /// /// Stored separately from blocks because the genesis block has no signatures. @@ -29,8 +31,9 @@ pub enum Table { } /// All table variants. -pub const ALL_TABLES: [Table; 9] = [ - Table::Blocks, +pub const ALL_TABLES: [Table; 10] = [ + Table::BlockHeaders, + Table::BlockBodies, Table::BlockSignatures, Table::States, Table::LatestKnownAttestations, diff --git a/crates/storage/src/backend/rocksdb.rs b/crates/storage/src/backend/rocksdb.rs index b42053d..c91b2ac 100644 --- a/crates/storage/src/backend/rocksdb.rs +++ b/crates/storage/src/backend/rocksdb.rs @@ -12,7 +12,8 @@ use std::sync::Arc; /// Returns the column family name for a table. fn cf_name(table: Table) -> &'static str { match table { - Table::Blocks => "blocks", + Table::BlockHeaders => "block_headers", + Table::BlockBodies => "block_bodies", Table::BlockSignatures => "block_signatures", Table::States => "states", Table::LatestKnownAttestations => "latest_known_attestations", @@ -166,7 +167,7 @@ mod tests { let backend = RocksDBBackend::open(dir.path()).unwrap(); let mut batch = backend.begin_write().unwrap(); batch - .put_batch(Table::Blocks, vec![(b"key1".to_vec(), b"value1".to_vec())]) + .put_batch(Table::BlockHeaders, vec![(b"key1".to_vec(), b"value1".to_vec())]) .unwrap(); batch.commit().unwrap(); } @@ -175,7 +176,7 @@ mod tests { { let backend = RocksDBBackend::open(dir.path()).unwrap(); let view = backend.begin_read().unwrap(); - let value = view.get(Table::Blocks, b"key1").unwrap(); + let value = view.get(Table::BlockHeaders, b"key1").unwrap(); assert_eq!(value, Some(b"value1".to_vec())); } } diff --git a/crates/storage/src/backend/tests.rs b/crates/storage/src/backend/tests.rs index 7401577..5a60bb4 100644 --- a/crates/storage/src/backend/tests.rs +++ b/crates/storage/src/backend/tests.rs @@ -28,7 +28,7 @@ fn test_put_and_get(backend: &dyn StorageBackend) { let mut batch = backend.begin_write().unwrap(); batch .put_batch( - Table::Blocks, + Table::BlockHeaders, vec![(b"test_put_get_key".to_vec(), b"value1".to_vec())], ) .unwrap(); @@ -38,7 +38,7 @@ fn test_put_and_get(backend: &dyn StorageBackend) { // Read data { let view = backend.begin_read().unwrap(); - let value = view.get(Table::Blocks, b"test_put_get_key").unwrap(); + let value = view.get(Table::BlockHeaders, b"test_put_get_key").unwrap(); assert_eq!(value, Some(b"value1".to_vec())); } } @@ -49,7 +49,7 @@ fn test_delete(backend: &dyn StorageBackend) { let mut batch = backend.begin_write().unwrap(); batch .put_batch( - Table::Blocks, + Table::BlockHeaders, vec![(b"test_delete_key".to_vec(), b"value1".to_vec())], ) .unwrap(); @@ -60,7 +60,7 @@ fn test_delete(backend: &dyn StorageBackend) { { let mut batch = backend.begin_write().unwrap(); batch - .delete_batch(Table::Blocks, vec![b"test_delete_key".to_vec()]) + .delete_batch(Table::BlockHeaders, vec![b"test_delete_key".to_vec()]) .unwrap(); batch.commit().unwrap(); } @@ -68,7 +68,7 @@ fn test_delete(backend: &dyn StorageBackend) { // Verify deleted { let view = backend.begin_read().unwrap(); - let value = view.get(Table::Blocks, b"test_delete_key").unwrap(); + let value = view.get(Table::BlockHeaders, b"test_delete_key").unwrap(); assert_eq!(value, None); } } @@ -109,7 +109,7 @@ fn test_prefix_iterator(backend: &dyn StorageBackend) { fn test_nonexistent_key(backend: &dyn StorageBackend) { let view = backend.begin_read().unwrap(); let value = view - .get(Table::Blocks, b"test_nonexistent_key_12345") + .get(Table::BlockHeaders, b"test_nonexistent_key_12345") .unwrap(); assert_eq!(value, None); } @@ -120,7 +120,7 @@ fn test_delete_then_put(backend: &dyn StorageBackend) { let mut batch = backend.begin_write().unwrap(); batch .put_batch( - Table::Blocks, + Table::BlockHeaders, vec![(b"test_del_put_key".to_vec(), b"old".to_vec())], ) .unwrap(); @@ -131,11 +131,11 @@ fn test_delete_then_put(backend: &dyn StorageBackend) { { let mut batch = backend.begin_write().unwrap(); batch - .delete_batch(Table::Blocks, vec![b"test_del_put_key".to_vec()]) + .delete_batch(Table::BlockHeaders, vec![b"test_del_put_key".to_vec()]) .unwrap(); batch .put_batch( - Table::Blocks, + Table::BlockHeaders, vec![(b"test_del_put_key".to_vec(), b"new".to_vec())], ) .unwrap(); @@ -144,7 +144,7 @@ fn test_delete_then_put(backend: &dyn StorageBackend) { let view = backend.begin_read().unwrap(); assert_eq!( - view.get(Table::Blocks, b"test_del_put_key").unwrap(), + view.get(Table::BlockHeaders, b"test_del_put_key").unwrap(), Some(b"new".to_vec()) ); } @@ -155,18 +155,18 @@ fn test_put_then_delete(backend: &dyn StorageBackend) { let mut batch = backend.begin_write().unwrap(); batch .put_batch( - Table::Blocks, + Table::BlockHeaders, vec![(b"test_put_del_key".to_vec(), b"value".to_vec())], ) .unwrap(); batch - .delete_batch(Table::Blocks, vec![b"test_put_del_key".to_vec()]) + .delete_batch(Table::BlockHeaders, vec![b"test_put_del_key".to_vec()]) .unwrap(); batch.commit().unwrap(); } let view = backend.begin_read().unwrap(); - assert_eq!(view.get(Table::Blocks, b"test_put_del_key").unwrap(), None); + assert_eq!(view.get(Table::BlockHeaders, b"test_put_del_key").unwrap(), None); } fn test_multiple_tables(backend: &dyn StorageBackend) { @@ -175,7 +175,7 @@ fn test_multiple_tables(backend: &dyn StorageBackend) { let mut batch = backend.begin_write().unwrap(); batch .put_batch( - Table::Blocks, + Table::BlockHeaders, vec![(b"test_multi_key".to_vec(), b"block".to_vec())], ) .unwrap(); @@ -192,7 +192,7 @@ fn test_multiple_tables(backend: &dyn StorageBackend) { { let view = backend.begin_read().unwrap(); assert_eq!( - view.get(Table::Blocks, b"test_multi_key").unwrap(), + view.get(Table::BlockHeaders, b"test_multi_key").unwrap(), Some(b"block".to_vec()) ); assert_eq!( diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index ed55223..c37e400 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -7,7 +7,7 @@ use crate::types::{StoredAggregatedPayload, StoredSignature}; use ethlambda_types::{ attestation::AttestationData, block::{ - AggregatedSignatureProof, Block, BlockBody, BlockSignaturesWithAttestation, + AggregatedSignatureProof, Block, BlockBody, BlockHeader, BlockSignaturesWithAttestation, BlockWithAttestation, SignedBlockWithAttestation, }, primitives::{ @@ -170,14 +170,22 @@ impl Store { .put_batch(Table::Metadata, metadata_entries) .expect("put metadata"); - // Block and state - let block_entries = vec![( + // Block header and body (stored separately) + let header_entries = vec![( anchor_block_root.as_ssz_bytes(), - anchor_block.as_ssz_bytes(), + anchor_block.header().as_ssz_bytes(), )]; batch - .put_batch(Table::Blocks, block_entries) - .expect("put block"); + .put_batch(Table::BlockHeaders, header_entries) + .expect("put block header"); + + let body_entries = vec![( + anchor_block_root.as_ssz_bytes(), + anchor_block.body.as_ssz_bytes(), + )]; + batch + .put_batch(Table::BlockBodies, body_entries) + .expect("put block body"); let state_entries = vec![( anchor_block_root.as_ssz_bytes(), @@ -454,26 +462,49 @@ impl Store { removed_count } + /// Get the full block by combining header and body. pub fn get_block(&self, root: &H256) -> Option { let view = self.backend.begin_read().expect("read view"); - view.get(Table::Blocks, &root.as_ssz_bytes()) + let key = root.as_ssz_bytes(); + + let header_bytes = view.get(Table::BlockHeaders, &key).expect("get")?; + let body_bytes = view.get(Table::BlockBodies, &key).expect("get")?; + + let header = BlockHeader::from_ssz_bytes(&header_bytes).expect("valid header"); + let body = BlockBody::from_ssz_bytes(&body_bytes).expect("valid body"); + + Some(Block::from_header_and_body(header, body)) + } + + /// Get only the block header without deserializing the body. + /// + /// This is more efficient than `get_block` when only header fields are needed. + pub fn get_block_header(&self, root: &H256) -> Option { + let view = self.backend.begin_read().expect("read view"); + view.get(Table::BlockHeaders, &root.as_ssz_bytes()) .expect("get") - .map(|bytes| Block::from_ssz_bytes(&bytes).expect("valid block")) + .map(|bytes| BlockHeader::from_ssz_bytes(&bytes).expect("valid header")) } pub fn contains_block(&self, root: &H256) -> bool { let view = self.backend.begin_read().expect("read view"); - view.get(Table::Blocks, &root.as_ssz_bytes()) + view.get(Table::BlockHeaders, &root.as_ssz_bytes()) .expect("get") .is_some() } pub fn insert_block(&mut self, root: H256, block: Block) { let mut batch = self.backend.begin_write().expect("write batch"); - let block_entries = vec![(root.as_ssz_bytes(), block.as_ssz_bytes())]; + + let header_entries = vec![(root.as_ssz_bytes(), block.header().as_ssz_bytes())]; batch - .put_batch(Table::Blocks, block_entries) - .expect("put block"); + .put_batch(Table::BlockHeaders, header_entries) + .expect("put block header"); + + let body_entries = vec![(root.as_ssz_bytes(), block.body.as_ssz_bytes())]; + batch + .put_batch(Table::BlockBodies, body_entries) + .expect("put block body"); let index_entries = vec![( encode_live_chain_key(block.slot, &root), @@ -513,10 +544,15 @@ impl Store { let mut batch = self.backend.begin_write().expect("write batch"); - let block_entries = vec![(root.as_ssz_bytes(), block.as_ssz_bytes())]; + let header_entries = vec![(root.as_ssz_bytes(), block.header().as_ssz_bytes())]; + batch + .put_batch(Table::BlockHeaders, header_entries) + .expect("put block header"); + + let body_entries = vec![(root.as_ssz_bytes(), block.body.as_ssz_bytes())]; batch - .put_batch(Table::Blocks, block_entries) - .expect("put block"); + .put_batch(Table::BlockBodies, body_entries) + .expect("put block body"); let sig_entries = vec![(root.as_ssz_bytes(), signatures.as_ssz_bytes())]; batch @@ -534,18 +570,21 @@ impl Store { batch.commit().expect("commit"); } - /// Get a signed block by combining block and signatures. + /// Get a signed block by combining header, body, and signatures. /// - /// Returns None if either the block or signatures are not found. + /// Returns None if any of the components are not found. /// Note: Genesis block has no entry in BlockSignatures table. pub fn get_signed_block(&self, root: &H256) -> Option { let view = self.backend.begin_read().expect("read view"); let key = root.as_ssz_bytes(); - let block_bytes = view.get(Table::Blocks, &key).expect("get")?; + let header_bytes = view.get(Table::BlockHeaders, &key).expect("get")?; + let body_bytes = view.get(Table::BlockBodies, &key).expect("get")?; let sig_bytes = view.get(Table::BlockSignatures, &key).expect("get")?; - let block = Block::from_ssz_bytes(&block_bytes).expect("valid block"); + let header = BlockHeader::from_ssz_bytes(&header_bytes).expect("valid header"); + let body = BlockBody::from_ssz_bytes(&body_bytes).expect("valid body"); + let block = Block::from_header_and_body(header, body); let signatures = BlockSignaturesWithAttestation::from_ssz_bytes(&sig_bytes).expect("valid signatures"); From 6aac3e21c7172342688e0fa26db5840d12c56117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:21:30 -0300 Subject: [PATCH 02/10] refactor: remove unused get_block --- crates/storage/src/store.rs | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index c37e400..cfc0c6a 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -462,23 +462,7 @@ impl Store { removed_count } - /// Get the full block by combining header and body. - pub fn get_block(&self, root: &H256) -> Option { - let view = self.backend.begin_read().expect("read view"); - let key = root.as_ssz_bytes(); - - let header_bytes = view.get(Table::BlockHeaders, &key).expect("get")?; - let body_bytes = view.get(Table::BlockBodies, &key).expect("get")?; - - let header = BlockHeader::from_ssz_bytes(&header_bytes).expect("valid header"); - let body = BlockBody::from_ssz_bytes(&body_bytes).expect("valid body"); - - Some(Block::from_header_and_body(header, body)) - } - - /// Get only the block header without deserializing the body. - /// - /// This is more efficient than `get_block` when only header fields are needed. + /// Get the block header by root. pub fn get_block_header(&self, root: &H256) -> Option { let view = self.backend.begin_read().expect("read view"); view.get(Table::BlockHeaders, &root.as_ssz_bytes()) @@ -860,7 +844,7 @@ impl Store { /// Returns the slot of the current safe target block. pub fn safe_target_slot(&self) -> u64 { - self.get_block(&self.safe_target()) + self.get_block_header(&self.safe_target()) .expect("safe target exists") .slot } From 715cd22d5a65ec816c130290a8a03243a1e7af7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:00:46 -0300 Subject: [PATCH 03/10] refactor: remove usages of contains_block --- crates/blockchain/src/store.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 7e758d8..d058119 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -98,9 +98,9 @@ fn validate_attestation(store: &Store, attestation: &Attestation) -> Result<(), .get_block_header(&data.target.root) .ok_or(StoreError::UnknownTargetBlock(data.target.root))?; - if !store.contains_block(&data.head.root) { - return Err(StoreError::UnknownHeadBlock(data.head.root)); - } + let _ = store + .get_block_header(&data.head.root) + .ok_or(StoreError::UnknownHeadBlock(data.head.root))?; // Topology Check - Source must be older than Target. if data.source.slot > data.target.slot { From 7d5f47b22102e571b1acdb839bfcbf278cb0258a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:03:55 -0300 Subject: [PATCH 04/10] refactor: replace contains_block with has_state and remove unused functions --- crates/blockchain/src/lib.rs | 4 +-- crates/blockchain/src/store.rs | 2 +- crates/storage/src/store.rs | 66 ++++------------------------------ 3 files changed, 9 insertions(+), 63 deletions(-) diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 8f58b39..510de5a 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -279,8 +279,8 @@ impl BlockChainServer { let parent_root = signed_block.message.block.parent_root; let proposer = signed_block.message.block.proposer_index; - // Check if parent block exists before attempting to process - if !self.store.contains_block(&parent_root) { + // Check if parent state exists before attempting to process + if !self.store.has_state(&parent_root) { info!(%slot, %parent_root, %block_root, "Block parent missing, storing as pending"); // Store block for later processing diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index d058119..75460f6 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -325,7 +325,7 @@ pub fn on_block( let slot = block.slot; // Skip duplicate blocks (idempotent operation) - if store.contains_block(&block_root) { + if store.has_state(&block_root) { return Ok(()); } diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index cfc0c6a..d0b07a0 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -470,37 +470,6 @@ impl Store { .map(|bytes| BlockHeader::from_ssz_bytes(&bytes).expect("valid header")) } - pub fn contains_block(&self, root: &H256) -> bool { - let view = self.backend.begin_read().expect("read view"); - view.get(Table::BlockHeaders, &root.as_ssz_bytes()) - .expect("get") - .is_some() - } - - pub fn insert_block(&mut self, root: H256, block: Block) { - let mut batch = self.backend.begin_write().expect("write batch"); - - let header_entries = vec![(root.as_ssz_bytes(), block.header().as_ssz_bytes())]; - batch - .put_batch(Table::BlockHeaders, header_entries) - .expect("put block header"); - - let body_entries = vec![(root.as_ssz_bytes(), block.body.as_ssz_bytes())]; - batch - .put_batch(Table::BlockBodies, body_entries) - .expect("put block body"); - - let index_entries = vec![( - encode_live_chain_key(block.slot, &root), - block.parent_root.as_ssz_bytes(), - )]; - batch - .put_batch(Table::LiveChain, index_entries) - .expect("put non-finalized chain index"); - - batch.commit().expect("commit"); - } - // ============ Signed Blocks ============ /// Insert a signed block, storing the block and signatures separately. @@ -577,27 +546,18 @@ impl Store { // ============ States ============ - /// Iterate over all (root, state) pairs. - pub fn iter_states(&self) -> impl Iterator + '_ { + pub fn get_state(&self, root: &H256) -> Option { let view = self.backend.begin_read().expect("read view"); - let entries: Vec<_> = view - .prefix_iterator(Table::States, &[]) - .expect("iterator") - .filter_map(|res| res.ok()) - .map(|(k, v)| { - let root = H256::from_ssz_bytes(&k).expect("valid root"); - let state = State::from_ssz_bytes(&v).expect("valid state"); - (root, state) - }) - .collect(); - entries.into_iter() + view.get(Table::States, &root.as_ssz_bytes()) + .expect("get") + .map(|bytes| State::from_ssz_bytes(&bytes).expect("valid state")) } - pub fn get_state(&self, root: &H256) -> Option { + pub fn has_state(&self, root: &H256) -> bool { let view = self.backend.begin_read().expect("read view"); view.get(Table::States, &root.as_ssz_bytes()) .expect("get") - .map(|bytes| State::from_ssz_bytes(&bytes).expect("valid state")) + .is_some() } pub fn insert_state(&mut self, root: H256, state: State) { @@ -738,20 +698,6 @@ impl Store { entries.into_iter() } - pub fn get_gossip_signature(&self, key: &SignatureKey) -> Option { - let view = self.backend.begin_read().expect("read view"); - view.get(Table::GossipSignatures, &encode_signature_key(key)) - .expect("get") - .and_then(|bytes| StoredSignature::from_ssz_bytes(&bytes).ok()) - } - - pub fn contains_gossip_signature(&self, key: &SignatureKey) -> bool { - let view = self.backend.begin_read().expect("read view"); - view.get(Table::GossipSignatures, &encode_signature_key(key)) - .expect("get") - .is_some() - } - pub fn insert_gossip_signature( &mut self, attestation_data: &AttestationData, From c9afc2ba8ab796a58d252b11464aa40d749ade17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:11:43 -0300 Subject: [PATCH 05/10] feat: add constructor to init Store from a State only --- crates/storage/src/store.rs | 50 ++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index d0b07a0..9ca6f5b 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -138,13 +138,32 @@ impl Store { backend: Arc, anchor_state: State, anchor_block: Block, + ) -> Self { + Self::init_store(backend, anchor_state, anchor_block.header(), Some(anchor_block.body)) + } + + /// Initialize a Store from an anchor state only. + /// + /// Uses the state's `latest_block_header` as the anchor block header. + /// No block body is stored since it's not available. + pub fn from_anchor_state(backend: Arc, anchor_state: State) -> Self { + let header = anchor_state.latest_block_header.clone(); + Self::init_store(backend, anchor_state, header, None) + } + + /// Internal helper to initialize the store with anchor data. + fn init_store( + backend: Arc, + anchor_state: State, + anchor_header: BlockHeader, + anchor_body: Option, ) -> Self { let anchor_state_root = anchor_state.tree_hash_root(); - let anchor_block_root = anchor_block.tree_hash_root(); + let anchor_block_root = anchor_header.tree_hash_root(); let anchor_checkpoint = Checkpoint { root: anchor_block_root, - slot: anchor_block.slot, + slot: anchor_header.slot, }; // Insert initial data @@ -170,23 +189,24 @@ impl Store { .put_batch(Table::Metadata, metadata_entries) .expect("put metadata"); - // Block header and body (stored separately) + // Block header let header_entries = vec![( anchor_block_root.as_ssz_bytes(), - anchor_block.header().as_ssz_bytes(), + anchor_header.as_ssz_bytes(), )]; batch .put_batch(Table::BlockHeaders, header_entries) .expect("put block header"); - let body_entries = vec![( - anchor_block_root.as_ssz_bytes(), - anchor_block.body.as_ssz_bytes(), - )]; - batch - .put_batch(Table::BlockBodies, body_entries) - .expect("put block body"); + // Block body (if provided) + if let Some(body) = anchor_body { + let body_entries = vec![(anchor_block_root.as_ssz_bytes(), body.as_ssz_bytes())]; + batch + .put_batch(Table::BlockBodies, body_entries) + .expect("put block body"); + } + // State let state_entries = vec![( anchor_block_root.as_ssz_bytes(), anchor_state.as_ssz_bytes(), @@ -195,14 +215,14 @@ impl Store { .put_batch(Table::States, state_entries) .expect("put state"); - // Non-finalized chain index + // Live chain index let index_entries = vec![( - encode_live_chain_key(anchor_block.slot, &anchor_block_root), - anchor_block.parent_root.as_ssz_bytes(), + encode_live_chain_key(anchor_header.slot, &anchor_block_root), + anchor_header.parent_root.as_ssz_bytes(), )]; batch .put_batch(Table::LiveChain, index_entries) - .expect("put non-finalized chain index"); + .expect("put live chain index"); batch.commit().expect("commit"); } From eefc463a5a343e69ce531c1ba5d9d05fb68f53da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:27:12 -0300 Subject: [PATCH 06/10] refactor: add additional checks and simplify init_store --- crates/common/types/src/block.rs | 2 +- crates/storage/src/store.rs | 70 +++++++++++++++++++++----------- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/crates/common/types/src/block.rs b/crates/common/types/src/block.rs index 4cbf2f1..f2b734d 100644 --- a/crates/common/types/src/block.rs +++ b/crates/common/types/src/block.rs @@ -176,7 +176,7 @@ impl BlockSignaturesWithAttestation { /// /// Headers are smaller than full blocks. They're useful for tracking the chain /// without storing everything. -#[derive(Debug, Clone, Serialize, Encode, Decode, TreeHash)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Encode, Decode, TreeHash)] pub struct BlockHeader { /// The slot in which the block was proposed pub slot: u64, diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 9ca6f5b..aeda818 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -118,28 +118,35 @@ pub struct Store { impl Store { /// Initialize a Store from a genesis state. - pub fn from_genesis(backend: Arc, mut genesis_state: State) -> Self { - // Ensure the header state root is zero before computing the state root - genesis_state.latest_block_header.state_root = H256::ZERO; - - let genesis_state_root = genesis_state.tree_hash_root(); - let genesis_block = Block { - slot: 0, - proposer_index: 0, - parent_root: H256::ZERO, - state_root: genesis_state_root, - body: BlockBody::default(), - }; - Self::get_forkchoice_store(backend, genesis_state, genesis_block) + pub fn from_genesis(backend: Arc, genesis_state: State) -> Self { + Self::from_anchor_state(backend, genesis_state) } /// Initialize a Store from an anchor state and block. + /// + /// The block must match the state's `latest_block_header`. + /// + /// # Panics + /// + /// Panics if the block's header doesn't match the state's `latest_block_header` + /// (comparing all fields except `state_root`, which is computed internally). pub fn get_forkchoice_store( backend: Arc, anchor_state: State, anchor_block: Block, ) -> Self { - Self::init_store(backend, anchor_state, anchor_block.header(), Some(anchor_block.body)) + // Compare headers with state_root zeroed (init_store handles state_root separately) + let mut state_header = anchor_state.latest_block_header.clone(); + let mut block_header = anchor_block.header(); + state_header.state_root = H256::ZERO; + block_header.state_root = H256::ZERO; + + assert_eq!( + state_header, block_header, + "block header doesn't match state's latest_block_header" + ); + + Self::init_store(backend, anchor_state, Some(anchor_block.body)) } /// Initialize a Store from an anchor state only. @@ -147,23 +154,40 @@ impl Store { /// Uses the state's `latest_block_header` as the anchor block header. /// No block body is stored since it's not available. pub fn from_anchor_state(backend: Arc, anchor_state: State) -> Self { - let header = anchor_state.latest_block_header.clone(); - Self::init_store(backend, anchor_state, header, None) + Self::init_store(backend, anchor_state, None) } /// Internal helper to initialize the store with anchor data. + /// + /// Header is taken from `anchor_state.latest_block_header`. fn init_store( backend: Arc, - anchor_state: State, - anchor_header: BlockHeader, + mut anchor_state: State, anchor_body: Option, ) -> Self { + // Save original state_root for validation + let original_state_root = anchor_state.latest_block_header.state_root; + + // Zero out state_root before computing (state contains header, header contains state_root) + anchor_state.latest_block_header.state_root = H256::ZERO; + + // Compute state root with zeroed header let anchor_state_root = anchor_state.tree_hash_root(); - let anchor_block_root = anchor_header.tree_hash_root(); + + // Validate: original must be zero (genesis) or match computed (checkpoint sync) + assert!( + original_state_root == H256::ZERO || original_state_root == anchor_state_root, + "anchor header state_root mismatch: expected {anchor_state_root:?}, got {original_state_root:?}" + ); + + // Populate the correct state_root + anchor_state.latest_block_header.state_root = anchor_state_root; + + let anchor_block_root = anchor_state.latest_block_header.tree_hash_root(); let anchor_checkpoint = Checkpoint { root: anchor_block_root, - slot: anchor_header.slot, + slot: anchor_state.latest_block_header.slot, }; // Insert initial data @@ -192,7 +216,7 @@ impl Store { // Block header let header_entries = vec![( anchor_block_root.as_ssz_bytes(), - anchor_header.as_ssz_bytes(), + anchor_state.latest_block_header.as_ssz_bytes(), )]; batch .put_batch(Table::BlockHeaders, header_entries) @@ -217,8 +241,8 @@ impl Store { // Live chain index let index_entries = vec![( - encode_live_chain_key(anchor_header.slot, &anchor_block_root), - anchor_header.parent_root.as_ssz_bytes(), + encode_live_chain_key(anchor_state.latest_block_header.slot, &anchor_block_root), + anchor_state.latest_block_header.parent_root.as_ssz_bytes(), )]; batch .put_batch(Table::LiveChain, index_entries) From 1ea56ef454783ba395a50814f75f0acaf3aa3e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:29:16 -0300 Subject: [PATCH 07/10] refactor: remove from_genesis --- bin/ethlambda/src/main.rs | 2 +- crates/net/rpc/src/lib.rs | 4 ++-- crates/storage/src/store.rs | 17 ++++++----------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index aa438dc..b399a74 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -95,7 +95,7 @@ async fn main() { let genesis_state = State::from_genesis(&genesis, validators); let backend = Arc::new(RocksDBBackend::open("./data").expect("Failed to open RocksDB")); - let store = Store::from_genesis(backend, genesis_state); + let store = Store::from_anchor_state(backend, genesis_state); let (p2p_tx, p2p_rx) = tokio::sync::mpsc::unbounded_channel(); let blockchain = BlockChain::spawn(store.clone(), p2p_tx, validator_keys); diff --git a/crates/net/rpc/src/lib.rs b/crates/net/rpc/src/lib.rs index fb5100c..e1f2b9e 100644 --- a/crates/net/rpc/src/lib.rs +++ b/crates/net/rpc/src/lib.rs @@ -118,7 +118,7 @@ mod tests { async fn test_get_latest_justified_checkpoint() { let state = create_test_state(); let backend = Arc::new(InMemoryBackend::new()); - let store = Store::from_genesis(backend, state); + let store = Store::from_anchor_state(backend, state); let app = build_api_router(store.clone()); @@ -154,7 +154,7 @@ mod tests { let state = create_test_state(); let backend = Arc::new(InMemoryBackend::new()); - let store = Store::from_genesis(backend, state); + let store = Store::from_anchor_state(backend, state); // Get the expected state from the store let finalized = store.latest_finalized(); diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index aeda818..9d75e09 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -117,9 +117,12 @@ pub struct Store { } impl Store { - /// Initialize a Store from a genesis state. - pub fn from_genesis(backend: Arc, genesis_state: State) -> Self { - Self::from_anchor_state(backend, genesis_state) + /// Initialize a Store from an anchor state only. + /// + /// Uses the state's `latest_block_header` as the anchor block header. + /// No block body is stored since it's not available. + pub fn from_anchor_state(backend: Arc, anchor_state: State) -> Self { + Self::init_store(backend, anchor_state, None) } /// Initialize a Store from an anchor state and block. @@ -149,14 +152,6 @@ impl Store { Self::init_store(backend, anchor_state, Some(anchor_block.body)) } - /// Initialize a Store from an anchor state only. - /// - /// Uses the state's `latest_block_header` as the anchor block header. - /// No block body is stored since it's not available. - pub fn from_anchor_state(backend: Arc, anchor_state: State) -> Self { - Self::init_store(backend, anchor_state, None) - } - /// Internal helper to initialize the store with anchor data. /// /// Header is taken from `anchor_state.latest_block_header`. From 4664a1c7234b3f53f619e539da3d772cfbaf1ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:47:14 -0300 Subject: [PATCH 08/10] docs: update Store documentation --- crates/storage/src/store.rs | 67 ++++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 9d75e09..d1a586c 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -105,14 +105,23 @@ fn decode_live_chain_key(bytes: &[u8]) -> (u64, H256) { (slot, root) } -/// Underlying storage of the node. -/// Similar to the spec's `Store`, but backed by a pluggable storage backend. +/// Fork choice store backed by a pluggable storage backend. /// -/// All data is stored in the backend. Metadata fields (time, config, head, etc.) -/// are stored in the Metadata table with their field name as the key. +/// The Store maintains all state required for fork choice and block processing: +/// +/// - **Metadata**: time, config, head, safe_target, justified/finalized checkpoints +/// - **Blocks**: headers and bodies stored separately for efficient header-only queries +/// - **States**: beacon states indexed by block root +/// - **Attestations**: latest known and pending ("new") attestations per validator +/// - **Signatures**: gossip signatures and aggregated proofs for signature verification +/// - **LiveChain**: slot index for efficient fork choice traversal (pruned on finalization) +/// +/// # Constructors +/// +/// - [`from_anchor_state`](Self::from_anchor_state): Initialize from a checkpoint state (no block body) +/// - [`get_forkchoice_store`](Self::get_forkchoice_store): Initialize from state + block (stores body) #[derive(Clone)] pub struct Store { - /// Storage backend for all store data. backend: Arc, } @@ -272,44 +281,50 @@ impl Store { // ============ Time ============ + /// Returns the current store time in seconds since genesis. pub fn time(&self) -> u64 { self.get_metadata(KEY_TIME) } + /// Sets the current store time. pub fn set_time(&mut self, time: u64) { self.set_metadata(KEY_TIME, &time); } // ============ Config ============ + /// Returns the chain configuration. pub fn config(&self) -> ChainConfig { self.get_metadata(KEY_CONFIG) } // ============ Head ============ + /// Returns the current head block root. pub fn head(&self) -> H256 { self.get_metadata(KEY_HEAD) } // ============ Safe Target ============ + /// Returns the safe target block root for attestations. pub fn safe_target(&self) -> H256 { self.get_metadata(KEY_SAFE_TARGET) } + /// Sets the safe target block root. pub fn set_safe_target(&mut self, safe_target: H256) { self.set_metadata(KEY_SAFE_TARGET, &safe_target); } - // ============ Latest Justified ============ + // ============ Checkpoints ============ + /// Returns the latest justified checkpoint. pub fn latest_justified(&self) -> Checkpoint { self.get_metadata(KEY_LATEST_JUSTIFIED) } - // ============ Latest Finalized ============ - + /// Returns the latest finalized checkpoint. pub fn latest_finalized(&self) -> Checkpoint { self.get_metadata(KEY_LATEST_FINALIZED) } @@ -585,6 +600,7 @@ impl Store { // ============ States ============ + /// Returns the state for the given block root. pub fn get_state(&self, root: &H256) -> Option { let view = self.backend.begin_read().expect("read view"); view.get(Table::States, &root.as_ssz_bytes()) @@ -592,6 +608,7 @@ impl Store { .map(|bytes| State::from_ssz_bytes(&bytes).expect("valid state")) } + /// Returns whether a state exists for the given block root. pub fn has_state(&self, root: &H256) -> bool { let view = self.backend.begin_read().expect("read view"); view.get(Table::States, &root.as_ssz_bytes()) @@ -599,6 +616,7 @@ impl Store { .is_some() } + /// Stores a state indexed by block root. pub fn insert_state(&mut self, root: H256, state: State) { let mut batch = self.backend.begin_write().expect("write batch"); let entries = vec![(root.as_ssz_bytes(), state.as_ssz_bytes())]; @@ -606,9 +624,12 @@ impl Store { batch.commit().expect("commit"); } - // ============ Latest Known Attestations ============ + // ============ Known Attestations ============ + // + // "Known" attestations are included in fork choice weight calculations. + // They're promoted from "new" attestations at specific intervals. - /// Iterate over all (validator_id, attestation_data) pairs for known attestations. + /// Iterates over all known attestations (validator_id, attestation_data). pub fn iter_known_attestations(&self) -> impl Iterator + '_ { let view = self.backend.begin_read().expect("read view"); let entries: Vec<_> = view @@ -624,6 +645,7 @@ impl Store { entries.into_iter() } + /// Returns a validator's latest known attestation. pub fn get_known_attestation(&self, validator_id: &u64) -> Option { let view = self.backend.begin_read().expect("read view"); view.get(Table::LatestKnownAttestations, &validator_id.as_ssz_bytes()) @@ -631,6 +653,7 @@ impl Store { .map(|bytes| AttestationData::from_ssz_bytes(&bytes).expect("valid attestation data")) } + /// Stores a validator's latest known attestation. pub fn insert_known_attestation(&mut self, validator_id: u64, data: AttestationData) { let mut batch = self.backend.begin_write().expect("write batch"); let entries = vec![(validator_id.as_ssz_bytes(), data.as_ssz_bytes())]; @@ -640,9 +663,12 @@ impl Store { batch.commit().expect("commit"); } - // ============ Latest New Attestations ============ + // ============ New Attestations ============ + // + // "New" attestations are pending attestations not yet included in fork choice. + // They're promoted to "known" via `promote_new_attestations`. - /// Iterate over all (validator_id, attestation_data) pairs for new attestations. + /// Iterates over all new (pending) attestations. pub fn iter_new_attestations(&self) -> impl Iterator + '_ { let view = self.backend.begin_read().expect("read view"); let entries: Vec<_> = view @@ -658,6 +684,7 @@ impl Store { entries.into_iter() } + /// Returns a validator's latest new (pending) attestation. pub fn get_new_attestation(&self, validator_id: &u64) -> Option { let view = self.backend.begin_read().expect("read view"); view.get(Table::LatestNewAttestations, &validator_id.as_ssz_bytes()) @@ -665,6 +692,7 @@ impl Store { .map(|bytes| AttestationData::from_ssz_bytes(&bytes).expect("valid attestation data")) } + /// Stores a validator's new (pending) attestation. pub fn insert_new_attestation(&mut self, validator_id: u64, data: AttestationData) { let mut batch = self.backend.begin_write().expect("write batch"); let entries = vec![(validator_id.as_ssz_bytes(), data.as_ssz_bytes())]; @@ -674,6 +702,7 @@ impl Store { batch.commit().expect("commit"); } + /// Removes a validator's new (pending) attestation. pub fn remove_new_attestation(&mut self, validator_id: &u64) { let mut batch = self.backend.begin_write().expect("write batch"); batch @@ -717,8 +746,11 @@ impl Store { } // ============ Gossip Signatures ============ + // + // Gossip signatures are individual validator signatures received via P2P. + // They're aggregated into proofs for block signature verification. - /// Iterate over all (signature_key, stored_signature) pairs. + /// Iterates over all gossip signatures. pub fn iter_gossip_signatures( &self, ) -> impl Iterator + '_ { @@ -737,6 +769,7 @@ impl Store { entries.into_iter() } + /// Stores a gossip signature for later aggregation. pub fn insert_gossip_signature( &mut self, attestation_data: &AttestationData, @@ -757,8 +790,11 @@ impl Store { } // ============ Aggregated Payloads ============ + // + // Aggregated payloads are leanVM proofs combining multiple signatures. + // Used to verify block signatures efficiently. - /// Iterate over all (signature_key, stored_payloads) pairs. + /// Iterates over all aggregated signature payloads. pub fn iter_aggregated_payloads( &self, ) -> impl Iterator)> + '_ { @@ -777,7 +813,8 @@ impl Store { entries.into_iter() } - pub fn get_aggregated_payloads( + /// Returns aggregated payloads for a signature key. + fn get_aggregated_payloads( &self, key: &SignatureKey, ) -> Option> { From d3f2e908997f2480c36747d0e0cc027a13a43178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:48:15 -0300 Subject: [PATCH 09/10] chore: fmt --- crates/blockchain/src/store.rs | 10 ++++++++-- crates/net/p2p/src/req_resp/handlers.rs | 5 ++++- crates/storage/src/backend/rocksdb.rs | 5 ++++- crates/storage/src/backend/tests.rs | 5 ++++- crates/storage/src/store.rs | 5 +---- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 75460f6..4cc787e 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -46,8 +46,14 @@ fn update_head(store: &mut Store) { store.update_checkpoints(ForkCheckpoints::head_only(new_head)); if old_head != new_head { - let old_slot = store.get_block_header(&old_head).map(|h| h.slot).unwrap_or(0); - let new_slot = store.get_block_header(&new_head).map(|h| h.slot).unwrap_or(0); + let old_slot = store + .get_block_header(&old_head) + .map(|h| h.slot) + .unwrap_or(0); + let new_slot = store + .get_block_header(&new_head) + .map(|h| h.slot) + .unwrap_or(0); let justified_slot = store.latest_justified().slot; let finalized_slot = store.latest_finalized().slot; info!( diff --git a/crates/net/p2p/src/req_resp/handlers.rs b/crates/net/p2p/src/req_resp/handlers.rs index 07c5e74..a11e06d 100644 --- a/crates/net/p2p/src/req_resp/handlers.rs +++ b/crates/net/p2p/src/req_resp/handlers.rs @@ -169,7 +169,10 @@ async fn handle_blocks_by_root_response( pub fn build_status(store: &Store) -> Status { let finalized = store.latest_finalized(); let head_root = store.head(); - let head_slot = store.get_block_header(&head_root).expect("head block exists").slot; + let head_slot = store + .get_block_header(&head_root) + .expect("head block exists") + .slot; Status { finalized, head: ethlambda_types::state::Checkpoint { diff --git a/crates/storage/src/backend/rocksdb.rs b/crates/storage/src/backend/rocksdb.rs index c91b2ac..c25b128 100644 --- a/crates/storage/src/backend/rocksdb.rs +++ b/crates/storage/src/backend/rocksdb.rs @@ -167,7 +167,10 @@ mod tests { let backend = RocksDBBackend::open(dir.path()).unwrap(); let mut batch = backend.begin_write().unwrap(); batch - .put_batch(Table::BlockHeaders, vec![(b"key1".to_vec(), b"value1".to_vec())]) + .put_batch( + Table::BlockHeaders, + vec![(b"key1".to_vec(), b"value1".to_vec())], + ) .unwrap(); batch.commit().unwrap(); } diff --git a/crates/storage/src/backend/tests.rs b/crates/storage/src/backend/tests.rs index 5a60bb4..f1a96b0 100644 --- a/crates/storage/src/backend/tests.rs +++ b/crates/storage/src/backend/tests.rs @@ -166,7 +166,10 @@ fn test_put_then_delete(backend: &dyn StorageBackend) { } let view = backend.begin_read().unwrap(); - assert_eq!(view.get(Table::BlockHeaders, b"test_put_del_key").unwrap(), None); + assert_eq!( + view.get(Table::BlockHeaders, b"test_put_del_key").unwrap(), + None + ); } fn test_multiple_tables(backend: &dyn StorageBackend) { diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index d1a586c..415e653 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -814,10 +814,7 @@ impl Store { } /// Returns aggregated payloads for a signature key. - fn get_aggregated_payloads( - &self, - key: &SignatureKey, - ) -> Option> { + fn get_aggregated_payloads(&self, key: &SignatureKey) -> Option> { let view = self.backend.begin_read().expect("read view"); view.get(Table::AggregatedPayloads, &encode_signature_key(key)) .expect("get") From 4fdf5ec3e4c109bf1d3625be295699ab19b4c3bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:12:45 -0300 Subject: [PATCH 10/10] refactor: avoid storing empty bodies --- crates/common/types/src/block.rs | 4 ++-- crates/storage/src/store.rs | 34 ++++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/crates/common/types/src/block.rs b/crates/common/types/src/block.rs index f2b734d..776dbb1 100644 --- a/crates/common/types/src/block.rs +++ b/crates/common/types/src/block.rs @@ -219,8 +219,8 @@ impl Block { /// Reconstruct a block from header and body. /// - /// # Panics - /// Panics if the body root doesn't match the header's body_root. + /// The caller should ensure that `header.body_root` matches `body.tree_hash_root()`. + /// This is verified with a debug assertion but not in release builds. pub fn from_header_and_body(header: BlockHeader, body: BlockBody) -> Self { debug_assert_eq!( header.body_root, diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 415e653..b6ef9ca 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -1,5 +1,11 @@ use std::collections::{HashMap, HashSet}; -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; + +/// The tree hash root of an empty block body. +/// +/// Used to detect genesis/anchor blocks that have no attestations, +/// allowing us to skip storing empty bodies and reconstruct them on read. +static EMPTY_BODY_ROOT: LazyLock = LazyLock::new(|| BlockBody::default().tree_hash_root()); use crate::api::{StorageBackend, Table}; use crate::types::{StoredAggregatedPayload, StoredSignature}; @@ -137,6 +143,7 @@ impl Store { /// Initialize a Store from an anchor state and block. /// /// The block must match the state's `latest_block_header`. + /// Named to mirror the spec's `get_forkchoice_store` function. /// /// # Panics /// @@ -551,15 +558,19 @@ impl Store { let mut batch = self.backend.begin_write().expect("write batch"); - let header_entries = vec![(root.as_ssz_bytes(), block.header().as_ssz_bytes())]; + let header = block.header(); + let header_entries = vec![(root.as_ssz_bytes(), header.as_ssz_bytes())]; batch .put_batch(Table::BlockHeaders, header_entries) .expect("put block header"); - let body_entries = vec![(root.as_ssz_bytes(), block.body.as_ssz_bytes())]; - batch - .put_batch(Table::BlockBodies, body_entries) - .expect("put block body"); + // Skip storing empty bodies - they can be reconstructed from the header's body_root + if header.body_root != *EMPTY_BODY_ROOT { + let body_entries = vec![(root.as_ssz_bytes(), block.body.as_ssz_bytes())]; + batch + .put_batch(Table::BlockBodies, body_entries) + .expect("put block body"); + } let sig_entries = vec![(root.as_ssz_bytes(), signatures.as_ssz_bytes())]; batch @@ -586,11 +597,18 @@ impl Store { let key = root.as_ssz_bytes(); let header_bytes = view.get(Table::BlockHeaders, &key).expect("get")?; - let body_bytes = view.get(Table::BlockBodies, &key).expect("get")?; let sig_bytes = view.get(Table::BlockSignatures, &key).expect("get")?; let header = BlockHeader::from_ssz_bytes(&header_bytes).expect("valid header"); - let body = BlockBody::from_ssz_bytes(&body_bytes).expect("valid body"); + + // Use empty body if header indicates empty, otherwise fetch from DB + let body = if header.body_root == *EMPTY_BODY_ROOT { + BlockBody::default() + } else { + let body_bytes = view.get(Table::BlockBodies, &key).expect("get")?; + BlockBody::from_ssz_bytes(&body_bytes).expect("valid body") + }; + let block = Block::from_header_and_body(header, body); let signatures = BlockSignaturesWithAttestation::from_ssz_bytes(&sig_bytes).expect("valid signatures");