From 71d2d19575736c51c0029efd54cd16cc74702fa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:19:42 -0300 Subject: [PATCH 01/15] refactor: move Store to ethlambda-storage --- crates/blockchain/src/lib.rs | 28 +- crates/blockchain/src/store.rs | 1106 ++++++++--------- .../blockchain/tests/forkchoice_spectests.rs | 51 +- .../blockchain/tests/signature_spectests.rs | 8 +- crates/storage/src/lib.rs | 306 +++++ 5 files changed, 840 insertions(+), 659 deletions(-) diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index f120e18..4ca5a2a 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::time::{Duration, SystemTime}; use ethlambda_state_transition::is_proposer; +use ethlambda_storage::Store; use ethlambda_types::{ attestation::{Attestation, AttestationData, SignedAttestation}, block::{BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation}, @@ -12,7 +13,6 @@ use ethlambda_types::{ use spawned_concurrency::tasks::{ CallResponse, CastResponse, GenServer, GenServerHandle, send_after, }; -use store::Store; use tokio::sync::mpsc; use tracing::{error, info, warn}; @@ -45,7 +45,7 @@ impl BlockChain { validator_keys: HashMap, ) -> BlockChain { let genesis_time = genesis_state.config.genesis_time; - let store = Store::from_genesis(genesis_state); + let store = store::from_genesis(genesis_state); let key_manager = key_manager::KeyManager::new(validator_keys); let handle = BlockChainServer { store, @@ -109,8 +109,7 @@ impl BlockChainServer { .flatten(); // Tick the store first - this accepts attestations at interval 0 if we have a proposal - self.store - .on_tick(timestamp, proposer_validator_id.is_some()); + store::on_tick(&mut self.store, timestamp, proposer_validator_id.is_some()); // Now build and publish the block (after attestations have been accepted) if let Some(validator_id) = proposer_validator_id { @@ -123,12 +122,12 @@ impl BlockChainServer { } // Update safe target slot metric (updated by store.on_tick at interval 2) - metrics::update_safe_target_slot(self.store.safe_target_slot()); + metrics::update_safe_target_slot(store::safe_target_slot(&self.store)); } /// Returns the validator ID if any of our validators is the proposer for this slot. fn get_our_proposer(&self, slot: u64) -> Option { - let head_state = self.store.head_state(); + let head_state = store::head_state(&self.store); let num_validators = head_state.validators.len() as u64; self.key_manager @@ -139,12 +138,12 @@ impl BlockChainServer { fn produce_attestations(&mut self, slot: u64) { // Get the head state to determine number of validators - let head_state = self.store.head_state(); + let head_state = store::head_state(&self.store); let num_validators = head_state.validators.len() as u64; // Produce attestation data once for all validators - let attestation_data = self.store.produce_attestation_data(slot); + let attestation_data = store::produce_attestation_data(&self.store, slot); // For each registered validator, produce and publish attestation for validator_id in self.key_manager.validator_ids() { @@ -191,10 +190,9 @@ impl BlockChainServer { info!(%slot, %validator_id, "We are the proposer for this slot"); // Build the block with attestation signatures - let Ok((block, attestation_signatures)) = self - .store - .produce_block_with_signatures(slot, validator_id) - .inspect_err(|err| error!(%slot, %validator_id, %err, "Failed to build block")) + let Ok((block, attestation_signatures)) = + store::produce_block_with_signatures(&mut self.store, slot, validator_id) + .inspect_err(|err| error!(%slot, %validator_id, %err, "Failed to build block")) else { return; }; @@ -208,7 +206,7 @@ impl BlockChainServer { root: block.tree_hash_root(), slot: block.slot, }, - target: self.store.get_attestation_target(), + target: store::get_attestation_target(&self.store), source: *self.store.latest_justified(), }, }; @@ -261,7 +259,7 @@ impl BlockChainServer { signed_block: SignedBlockWithAttestation, ) -> Result<(), StoreError> { let slot = signed_block.message.block.slot; - self.store.on_block(signed_block)?; + store::on_block(&mut self.store, signed_block)?; metrics::update_head_slot(slot); metrics::update_latest_justified_slot(self.store.latest_justified().slot); metrics::update_latest_finalized_slot(self.store.latest_finalized().slot); @@ -277,7 +275,7 @@ impl BlockChainServer { } fn on_gossip_attestation(&mut self, attestation: SignedAttestation) { - if let Err(err) = self.store.on_gossip_attestation(attestation) { + if let Err(err) = store::on_gossip_attestation(&mut self.store, attestation) { warn!(%err, "Failed to process gossiped attestation"); } } diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index d921d5f..9a8a914 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -4,6 +4,7 @@ use ethlambda_crypto::aggregate_signatures; use ethlambda_state_transition::{ is_proposer, process_block, process_slots, slot_is_justifiable_after, }; +use ethlambda_storage::{SignatureKey, Store}; use ethlambda_types::{ attestation::{AggregatedAttestation, Attestation, AttestationData, SignedAttestation}, block::{ @@ -12,7 +13,7 @@ use ethlambda_types::{ }, primitives::{H256, TreeHash}, signature::ValidatorSignature, - state::{ChainConfig, Checkpoint, State, Validator}, + state::{Checkpoint, State, Validator}, }; use tracing::{info, trace, warn}; @@ -20,700 +21,575 @@ use crate::SECONDS_PER_SLOT; const JUSTIFICATION_LOOKBACK_SLOTS: u64 = 3; -/// Key for looking up individual validator signatures. -/// Used to index signature caches by (validator, message) pairs. -/// -/// Values are (validator_index, attestation_data_root). -type SignatureKey = (u64, H256); - -/// Forkchoice store tracking chain state and validator attestations. -/// -/// This is the "local view" that a node uses to run LMD GHOST. It contains: -/// -/// - which blocks and states are known, -/// - which checkpoints are justified and finalized, -/// - which block is currently considered the head, -/// - and, for each validator, their latest attestation that should influence fork choice. -/// -/// The `Store` is updated whenever: -/// - a new block is processed, -/// - an attestation is received (via a block or gossip), -/// - an interval tick occurs (activating new attestations), -/// - or when the head is recomputed. -#[derive(Clone)] -pub struct Store { - /// Current time in intervals since genesis. - time: u64, - - /// Chain configuration parameters. - config: ChainConfig, - - /// Root of the current canonical chain head block. - /// - /// This is the result of running the fork choice algorithm on the current contents of the `Store`. - head: H256, - - /// Root of the current safe target for attestation. - /// - /// This can be used by higher-level logic to restrict which blocks are - /// considered safe to attest to, based on additional safety conditions. - /// - safe_target: H256, - - /// Highest slot justified checkpoint known to the store. - /// - /// LMD GHOST starts from this checkpoint when computing the head. - /// - /// Only descendants of this checkpoint are considered viable. - latest_justified: Checkpoint, - - /// Highest slot finalized checkpoint known to the store. - /// - /// Everything strictly before this checkpoint can be considered immutable. - /// - /// Fork choice will never revert finalized history. - latest_finalized: Checkpoint, - - /// Mapping from block root to Block objects. - /// - /// This is the set of blocks that the node currently knows about. - /// - /// Every block that might participate in fork choice must appear here. - blocks: HashMap, - - /// Mapping from block root to State objects. - /// - /// For each known block, we keep its post-state. - /// - /// These states carry justified and finalized checkpoints that we use to update the - /// `Store`'s latest justified and latest finalized checkpoints. - states: HashMap, - - /// Latest signed attestations by validator that have been processed. - /// - /// - These attestations are "known" and contribute to fork choice weights. - /// - Keyed by validator index to enforce one attestation per validator. - latest_known_attestations: HashMap, - - /// Latest signed attestations by validator that are pending processing. - /// - /// - These attestations are "new" and do not yet contribute to fork choice. - /// - They migrate to `latest_known_attestations` via interval ticks. - /// - Keyed by validator index to enforce one attestation per validator. - latest_new_attestations: HashMap, - - /// Per-validator XMSS signatures learned from gossip. - /// - /// Keyed by SignatureKey(validator_id, attestation_data_root). - gossip_signatures: HashMap, - - /// Aggregated signature proofs learned from blocks. - /// - Keyed by SignatureKey(validator_id, attestation_data_root). - /// - Values are lists of AggregatedSignatureProof, each containing the participants - /// bitfield indicating which validators signed. - /// - Used for recursive signature aggregation when building blocks. - /// - Populated by on_block. - aggregated_payloads: HashMap>, +/// Initialize a Store from a genesis state. +pub fn from_genesis(mut genesis_state: State) -> Store { + // 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: Default::default(), + }; + get_forkchoice_store(genesis_state, genesis_block) } -impl Store { - pub fn from_genesis(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: Default::default(), - }; - Self::get_forkchoice_store(genesis_state, genesis_block) - } +/// Initialize a Store from an anchor state and block. +pub fn get_forkchoice_store(anchor_state: State, anchor_block: Block) -> Store { + let anchor_state_root = anchor_state.tree_hash_root(); + let anchor_block_root = anchor_block.tree_hash_root(); - pub fn get_forkchoice_store(anchor_state: State, anchor_block: Block) -> Self { - let anchor_state_root = anchor_state.tree_hash_root(); - let anchor_block_root = anchor_block.tree_hash_root(); + let mut blocks = HashMap::new(); + blocks.insert(anchor_block_root, anchor_block.clone()); - let mut blocks = HashMap::new(); - blocks.insert(anchor_block_root, anchor_block.clone()); + let mut states = HashMap::new(); + states.insert(anchor_block_root, anchor_state.clone()); - let mut states = HashMap::new(); - states.insert(anchor_block_root, anchor_state.clone()); + let anchor_checkpoint = Checkpoint { + root: anchor_block_root, + slot: 0, + }; - let anchor_checkpoint = Checkpoint { - root: anchor_block_root, - slot: 0, - }; + info!(%anchor_state_root, %anchor_block_root, "Initialized store"); + + Store::new( + 0, + anchor_state.config.clone(), + anchor_block_root, + anchor_block_root, + anchor_checkpoint, + anchor_checkpoint, + blocks, + states, + ) +} - info!(%anchor_state_root, %anchor_block_root, "Initialized store"); - - Self { - time: 0, - config: anchor_state.config.clone(), - head: anchor_block_root, - safe_target: anchor_block_root, - latest_justified: anchor_checkpoint, - latest_finalized: anchor_checkpoint, - blocks, - states, - latest_known_attestations: HashMap::new(), - latest_new_attestations: HashMap::new(), - gossip_signatures: HashMap::new(), - aggregated_payloads: HashMap::new(), - } - } +/// Accept new attestations, moving them from pending to known. +pub fn accept_new_attestations(store: &mut Store) { + let mut latest_new_attestations = store.take_new_attestations(); + store.extend_known_attestations(latest_new_attestations.drain()); + store.restore_new_attestations(latest_new_attestations); - pub fn accept_new_attestations(&mut self) { - let mut latest_new_attestations = std::mem::take(&mut self.latest_new_attestations); - self.latest_known_attestations - .extend(latest_new_attestations.drain()); - self.latest_new_attestations = latest_new_attestations; + update_head(store); +} - self.update_head(); - } +/// Update the head based on the fork choice rule. +pub fn update_head(store: &mut Store) { + let head = ethlambda_fork_choice::compute_lmd_ghost_head( + store.latest_justified().root, + store.blocks(), + store.latest_known_attestations(), + 0, + ); + store.set_head(head); +} - pub fn update_head(&mut self) { - let head = ethlambda_fork_choice::compute_lmd_ghost_head( - self.latest_justified.root, - &self.blocks, - &self.latest_known_attestations, - 0, - ); - self.head = head; - } +/// Update the safe target for attestation. +pub fn update_safe_target(store: &mut Store) { + let head_state = store.get_state(&store.head()).expect("head state exists"); + let num_validators = head_state.validators.len() as u64; - pub fn update_safe_target(&mut self) { - let head_state = &self.states[&self.head]; - let num_validators = head_state.validators.len() as u64; + let min_target_score = (num_validators * 2).div_ceil(3); - let min_target_score = (num_validators * 2).div_ceil(3); + let safe_target = ethlambda_fork_choice::compute_lmd_ghost_head( + store.latest_justified().root, + store.blocks(), + store.latest_new_attestations(), + min_target_score, + ); + store.set_safe_target(safe_target); +} - let safe_target = ethlambda_fork_choice::compute_lmd_ghost_head( - self.latest_justified.root, - &self.blocks, - &self.latest_new_attestations, - min_target_score, - ); - self.safe_target = safe_target; +/// Validate incoming attestation before processing. +/// +/// Ensures the vote respects the basic laws of time and topology: +/// 1. The blocks voted for must exist in our store. +/// 2. A vote cannot span backwards in time (source > target). +/// 3. A vote cannot be for a future slot. +pub fn validate_attestation(store: &Store, attestation: &Attestation) -> Result<(), StoreError> { + 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) + .ok_or(StoreError::UnknownSourceBlock(data.source.root))?; + let target_block = store + .get_block(&data.target.root) + .ok_or(StoreError::UnknownTargetBlock(data.target.root))?; + + if !store.contains_block(&data.head.root) { + return Err(StoreError::UnknownHeadBlock(data.head.root)); } - /// Validate incoming attestation before processing. - /// - /// Ensures the vote respects the basic laws of time and topology: - /// 1. The blocks voted for must exist in our store. - /// 2. A vote cannot span backwards in time (source > target). - /// 3. A vote cannot be for a future slot. - pub fn validate_attestation(&self, attestation: &Attestation) -> Result<(), StoreError> { - let data = &attestation.data; - - // Availability Check - We cannot count a vote if we haven't seen the blocks involved. - let source_block = self - .blocks - .get(&data.source.root) - .ok_or(StoreError::UnknownSourceBlock(data.source.root))?; - let target_block = self - .blocks - .get(&data.target.root) - .ok_or(StoreError::UnknownTargetBlock(data.target.root))?; - - if !self.blocks.contains_key(&data.head.root) { - return Err(StoreError::UnknownHeadBlock(data.head.root)); - } - - // Topology Check - Source must be older than Target. - if data.source.slot > data.target.slot { - return Err(StoreError::SourceExceedsTarget); - } - - // Consistency Check - Validate checkpoint slots match block slots. - if source_block.slot != data.source.slot { - return Err(StoreError::SourceSlotMismatch { - checkpoint_slot: data.source.slot, - block_slot: source_block.slot, - }); - } - if target_block.slot != data.target.slot { - return Err(StoreError::TargetSlotMismatch { - checkpoint_slot: data.target.slot, - block_slot: target_block.slot, - }); - } + // Topology Check - Source must be older than Target. + if data.source.slot > data.target.slot { + return Err(StoreError::SourceExceedsTarget); + } - // Time Check - Validate attestation is not too far in the future. - // We allow a small margin for clock disparity (1 slot), but no further. - let current_slot = self.time / SECONDS_PER_SLOT; - if data.slot > current_slot + 1 { - return Err(StoreError::AttestationTooFarInFuture { - attestation_slot: data.slot, - current_slot, - }); - } + // Consistency Check - Validate checkpoint slots match block slots. + if source_block.slot != data.source.slot { + return Err(StoreError::SourceSlotMismatch { + checkpoint_slot: data.source.slot, + block_slot: source_block.slot, + }); + } + if target_block.slot != data.target.slot { + return Err(StoreError::TargetSlotMismatch { + checkpoint_slot: data.target.slot, + block_slot: target_block.slot, + }); + } - Ok(()) + // Time Check - Validate attestation is not too far in the future. + // We allow a small margin for clock disparity (1 slot), but no further. + let current_slot = store.time() / SECONDS_PER_SLOT; + if data.slot > current_slot + 1 { + return Err(StoreError::AttestationTooFarInFuture { + attestation_slot: data.slot, + current_slot, + }); } - pub fn on_tick(&mut self, timestamp: u64, has_proposal: bool) { - let time = timestamp - self.config.genesis_time; + Ok(()) +} - // If we're more than a slot behind, fast-forward to a slot before. - // Operations are idempotent, so this should be fine. - if time.saturating_sub(self.time) > SECONDS_PER_SLOT { - self.time = time - SECONDS_PER_SLOT; - } +/// Process a tick event. +pub fn on_tick(store: &mut Store, timestamp: u64, has_proposal: bool) { + let time = timestamp - store.config().genesis_time; + + // If we're more than a slot behind, fast-forward to a slot before. + // Operations are idempotent, so this should be fine. + if time.saturating_sub(store.time()) > SECONDS_PER_SLOT { + store.set_time(time - SECONDS_PER_SLOT); + } - while self.time < time { - self.time += 1; + while store.time() < time { + store.set_time(store.time() + 1); - let slot = self.time / SECONDS_PER_SLOT; - let interval = self.time % SECONDS_PER_SLOT; + let slot = store.time() / SECONDS_PER_SLOT; + let interval = store.time() % SECONDS_PER_SLOT; - trace!(%slot, %interval, "processing tick"); + trace!(%slot, %interval, "processing tick"); - // has_proposal is only signaled for the final tick (matching Python spec behavior) - let is_final_tick = self.time == time; - let should_signal_proposal = has_proposal && is_final_tick; + // has_proposal is only signaled for the final tick (matching Python spec behavior) + let is_final_tick = store.time() == time; + let should_signal_proposal = has_proposal && is_final_tick; - // NOTE: here we assume on_tick never skips intervals - match interval { - 0 => { - // Start of slot - process attestations if proposal exists - if should_signal_proposal { - self.accept_new_attestations(); - } - } - 1 => { - // Second interval - no action - } - 2 => { - // Mid-slot - update safe target for validators - self.update_safe_target(); + // NOTE: here we assume on_tick never skips intervals + match interval { + 0 => { + // Start of slot - process attestations if proposal exists + if should_signal_proposal { + accept_new_attestations(store); } - 3 => { - // End of slot - accept accumulated attestations - self.accept_new_attestations(); - } - _ => unreachable!("slots only have 4 intervals"), } - } - } - - pub fn on_gossip_attestation( - &mut self, - signed_attestation: SignedAttestation, - ) -> Result<(), StoreError> { - let validator_id = signed_attestation.validator_id; - let attestation = Attestation { - validator_id, - data: signed_attestation.message, - }; - self.validate_attestation(&attestation)?; - let target = attestation.data.target; - let target_state = self - .states - .get(&target.root) - .ok_or(StoreError::MissingTargetState(target.root))?; - if validator_id >= target_state.validators.len() as u64 { - return Err(StoreError::InvalidValidatorIndex); - } - let validator_pubkey = target_state.validators[validator_id as usize] - .get_pubkey() - .map_err(|_| StoreError::PubkeyDecodingFailed(validator_id))?; - let message = attestation.data.tree_hash_root(); - if cfg!(not(feature = "skip-signature-verification")) { - use ethlambda_types::signature::ValidatorSignature; - // Use attestation.data.slot as epoch (matching what Zeam and ethlambda use for signing) - let epoch: u32 = attestation.data.slot.try_into().expect("slot exceeds u32"); - let signature = ValidatorSignature::from_bytes(&signed_attestation.signature) - .map_err(|_| StoreError::SignatureDecodingFailed)?; - if !signature.is_valid(&validator_pubkey, epoch, &message) { - return Err(StoreError::SignatureVerificationFailed); + 1 => { + // Second interval - no action + } + 2 => { + // Mid-slot - update safe target for validators + update_safe_target(store); } + 3 => { + // End of slot - accept accumulated attestations + accept_new_attestations(store); + } + _ => unreachable!("slots only have 4 intervals"), } - self.on_attestation(attestation, false)?; + } +} - if cfg!(not(feature = "skip-signature-verification")) { - // Store signature for later lookup during block building - let signature_key = (validator_id, message); - let signature = ValidatorSignature::from_bytes(&signed_attestation.signature) - .map_err(|_| StoreError::SignatureDecodingFailed)?; - self.gossip_signatures.insert(signature_key, signature); +/// Process a gossiped attestation. +pub fn on_gossip_attestation( + store: &mut Store, + signed_attestation: SignedAttestation, +) -> Result<(), StoreError> { + let validator_id = signed_attestation.validator_id; + let attestation = Attestation { + validator_id, + data: signed_attestation.message, + }; + validate_attestation(store, &attestation)?; + let target = attestation.data.target; + let target_state = store + .get_state(&target.root) + .ok_or(StoreError::MissingTargetState(target.root))?; + if validator_id >= target_state.validators.len() as u64 { + return Err(StoreError::InvalidValidatorIndex); + } + let validator_pubkey = target_state.validators[validator_id as usize] + .get_pubkey() + .map_err(|_| StoreError::PubkeyDecodingFailed(validator_id))?; + let message = attestation.data.tree_hash_root(); + if cfg!(not(feature = "skip-signature-verification")) { + use ethlambda_types::signature::ValidatorSignature; + // Use attestation.data.slot as epoch (matching what Zeam and ethlambda use for signing) + let epoch: u32 = attestation.data.slot.try_into().expect("slot exceeds u32"); + let signature = ValidatorSignature::from_bytes(&signed_attestation.signature) + .map_err(|_| StoreError::SignatureDecodingFailed)?; + if !signature.is_valid(&validator_pubkey, epoch, &message) { + return Err(StoreError::SignatureVerificationFailed); } - Ok(()) } + on_attestation(store, attestation, false)?; + + if cfg!(not(feature = "skip-signature-verification")) { + // Store signature for later lookup during block building + let signature_key = (validator_id, message); + let signature = ValidatorSignature::from_bytes(&signed_attestation.signature) + .map_err(|_| StoreError::SignatureDecodingFailed)?; + store.insert_gossip_signature(signature_key, signature); + } + Ok(()) +} - /// Process a new attestation and place it into the correct attestation stage. - /// - /// Attestations can come from: - /// - a block body (on-chain, `is_from_block=true`), or - /// - the gossip network (off-chain, `is_from_block=false`). - /// - /// The Attestation Pipeline: - /// - Stage 1 (latest_new_attestations): Pending attestations not yet counted in fork choice. - /// - Stage 2 (latest_known_attestations): Active attestations used by LMD-GHOST. - fn on_attestation( - &mut self, - attestation: Attestation, - is_from_block: bool, - ) -> Result<(), StoreError> { - // First, ensure the attestation is structurally and temporally valid. - self.validate_attestation(&attestation)?; - - let validator_id = attestation.validator_id; - let attestation_data = attestation.data; - let attestation_slot = attestation_data.slot; - - if is_from_block { - // On-chain attestation processing - // These are historical attestations from other validators included by the proposer. - // They are processed immediately as "known" attestations. - - let should_update = self - .latest_known_attestations - .get(&validator_id) - .is_none_or(|latest| latest.slot < attestation_slot); - - if should_update { - self.latest_known_attestations - .insert(validator_id, attestation_data.clone()); - } - - // Remove pending attestation if superseded by on-chain attestation - if let Some(existing_new) = self.latest_new_attestations.get(&validator_id) - && existing_new.slot <= attestation_slot - { - self.latest_new_attestations.remove(&validator_id); - } - } else { - // Network gossip attestation processing - // These enter the "new" stage and must wait for interval tick acceptance. - - // Reject attestations from future slots - let current_slot = self.time / SECONDS_PER_SLOT; - if attestation_slot > current_slot { - return Err(StoreError::AttestationTooFarInFuture { - attestation_slot, - current_slot, - }); - } +/// Process a new attestation and place it into the correct attestation stage. +/// +/// Attestations can come from: +/// - a block body (on-chain, `is_from_block=true`), or +/// - the gossip network (off-chain, `is_from_block=false`). +/// +/// The Attestation Pipeline: +/// - Stage 1 (latest_new_attestations): Pending attestations not yet counted in fork choice. +/// - Stage 2 (latest_known_attestations): Active attestations used by LMD-GHOST. +fn on_attestation( + store: &mut Store, + attestation: Attestation, + is_from_block: bool, +) -> Result<(), StoreError> { + // First, ensure the attestation is structurally and temporally valid. + validate_attestation(store, &attestation)?; - let should_update = self - .latest_new_attestations - .get(&validator_id) - .is_none_or(|latest| latest.slot < attestation_slot); + let validator_id = attestation.validator_id; + let attestation_data = attestation.data; + let attestation_slot = attestation_data.slot; - if should_update { - self.latest_new_attestations - .insert(validator_id, attestation_data); - } - } + if is_from_block { + // On-chain attestation processing + // These are historical attestations from other validators included by the proposer. + // They are processed immediately as "known" attestations. - Ok(()) - } + let should_update = store + .get_known_attestation(&validator_id) + .is_none_or(|latest| latest.slot < attestation_slot); - /// Process a new block and update the forkchoice state. - /// - /// This method integrates a block into the forkchoice store by: - /// 1. Validating the block's parent exists - /// 2. Computing the post-state via the state transition function - /// 3. Processing attestations included in the block body (on-chain) - /// 4. Updating the forkchoice head - /// 5. Processing the proposer's attestation (as if gossiped) - pub fn on_block(&mut self, signed_block: SignedBlockWithAttestation) -> Result<(), StoreError> { - // Unpack block components - let block = signed_block.message.block.clone(); - let proposer_attestation = signed_block.message.proposer_attestation.clone(); - let block_root = block.tree_hash_root(); - let slot = block.slot; - - // Skip duplicate blocks (idempotent operation) - if self.blocks.contains_key(&block_root) { - return Ok(()); + if should_update { + store.insert_known_attestation(validator_id, attestation_data.clone()); } - // Verify parent chain is available - // TODO: sync parent chain if parent is missing - let parent_state = - self.states - .get(&block.parent_root) - .ok_or(StoreError::MissingParentState { - parent_root: block.parent_root, - slot, - })?; - - // Validate cryptographic signatures - // TODO: extract signature verification to a pre-checks function - // to avoid the need for this - if cfg!(not(feature = "skip-signature-verification")) { - verify_signatures(parent_state, &signed_block)?; + // Remove pending attestation if superseded by on-chain attestation + if let Some(existing_new) = store.get_new_attestation(&validator_id) + && existing_new.slot <= attestation_slot + { + store.remove_new_attestation(&validator_id); } + } else { + // Network gossip attestation processing + // These enter the "new" stage and must wait for interval tick acceptance. - // Execute state transition function to compute post-block state - let mut post_state = parent_state.clone(); - ethlambda_state_transition::state_transition(&mut post_state, &block)?; + // Reject attestations from future slots + let current_slot = store.time() / SECONDS_PER_SLOT; + if attestation_slot > current_slot { + return Err(StoreError::AttestationTooFarInFuture { + attestation_slot, + current_slot, + }); + } - // Cache the state root in the latest block header - let state_root = block.state_root; - post_state.latest_block_header.state_root = state_root; + let should_update = store + .get_new_attestation(&validator_id) + .is_none_or(|latest| latest.slot < attestation_slot); - // If post-state has a higher justified checkpoint, update the store - if post_state.latest_justified.slot > self.latest_justified.slot { - self.latest_justified = post_state.latest_justified; + if should_update { + store.insert_new_attestation(validator_id, attestation_data); } + } - // If post-state has a higher finalized checkpoint, update the store - if post_state.latest_finalized.slot > self.latest_finalized.slot { - self.latest_finalized = post_state.latest_finalized; - } + Ok(()) +} - // Store block and state - self.blocks.insert(block_root, block.clone()); - self.states.insert(block_root, post_state); +/// Process a new block and update the forkchoice state. +/// +/// This method integrates a block into the forkchoice store by: +/// 1. Validating the block's parent exists +/// 2. Computing the post-state via the state transition function +/// 3. Processing attestations included in the block body (on-chain) +/// 4. Updating the forkchoice head +/// 5. Processing the proposer's attestation (as if gossiped) +pub fn on_block( + store: &mut Store, + signed_block: SignedBlockWithAttestation, +) -> Result<(), StoreError> { + // Unpack block components + let block = signed_block.message.block.clone(); + let proposer_attestation = signed_block.message.proposer_attestation.clone(); + let block_root = block.tree_hash_root(); + let slot = block.slot; + + // Skip duplicate blocks (idempotent operation) + if store.contains_block(&block_root) { + return Ok(()); + } - // Process block body attestations and their signatures - let aggregated_attestations = &block.body.attestations; - let attestation_signatures = &signed_block.signature.attestation_signatures; + // Verify parent chain is available + // TODO: sync parent chain if parent is missing + let parent_state = + store + .get_state(&block.parent_root) + .ok_or(StoreError::MissingParentState { + parent_root: block.parent_root, + slot, + })?; - // Process block body attestations. - // TODO: fail the block if an attestation is invalid. Right now we - // just log a warning. - for (att, proof) in aggregated_attestations - .iter() - .zip(attestation_signatures.iter()) - { - let validator_ids = aggregation_bits_to_validator_indices(&att.aggregation_bits); - let data_root = att.data.tree_hash_root(); - - for validator_id in validator_ids { - // Update Proof Map - Store the proof so future block builders can reuse this aggregation - let key: SignatureKey = (validator_id, data_root); - self.aggregated_payloads - .entry(key) - .or_default() - .push(proof.clone()); - - // Update Fork Choice - Register the vote immediately (historical/on-chain) - let attestation = Attestation { - validator_id, - data: att.data.clone(), - }; - // TODO: validate attestations before processing - if let Err(err) = self.on_attestation(attestation, true) { - warn!(%slot, %validator_id, %err, "Invalid attestation in block"); - } - } - } + // Validate cryptographic signatures + // TODO: extract signature verification to a pre-checks function + // to avoid the need for this + if cfg!(not(feature = "skip-signature-verification")) { + verify_signatures(parent_state, &signed_block)?; + } - // Update forkchoice head based on new block and attestations - // IMPORTANT: This must happen BEFORE processing proposer attestation - // to prevent the proposer from gaining circular weight advantage. - self.update_head(); - - // Process proposer attestation as if received via gossip - // The proposer's attestation should NOT affect this block's fork choice position. - // It is treated as pending until interval 3 (end of slot). - - if cfg!(not(feature = "skip-signature-verification")) { - // Store the proposer's signature for potential future block building - let proposer_sig_key: SignatureKey = ( - proposer_attestation.validator_id, - proposer_attestation.data.tree_hash_root(), - ); - let proposer_sig = - ValidatorSignature::from_bytes(&signed_block.signature.proposer_signature) - .map_err(|_| StoreError::SignatureDecodingFailed)?; - self.gossip_signatures - .insert(proposer_sig_key, proposer_sig); - } + // Execute state transition function to compute post-block state + let mut post_state = parent_state.clone(); + ethlambda_state_transition::state_transition(&mut post_state, &block)?; - // Process proposer attestation (enters "new" stage, not "known") - // TODO: validate attestations before processing - if let Err(err) = self.on_attestation(proposer_attestation, false) { - warn!(%slot, %err, "Invalid proposer attestation in block"); - } + // Cache the state root in the latest block header + let state_root = block.state_root; + post_state.latest_block_header.state_root = state_root; - info!(%slot, %block_root, %state_root, "Processed new block"); - Ok(()) + // If post-state has a higher justified checkpoint, update the store + if post_state.latest_justified.slot > store.latest_justified().slot { + store.set_latest_justified(post_state.latest_justified); } - /// Calculate target checkpoint for validator attestations. - /// - /// NOTE: this assumes that we have all the blocks from the head back to the latest finalized. - pub fn get_attestation_target(&self) -> Checkpoint { - // Start from current head - let mut target_block_root = self.head; - let mut target_block = &self.blocks[&target_block_root]; - - let safe_target_block_slot = self.blocks[&self.safe_target].slot; - - // Walk back toward safe target (up to `JUSTIFICATION_LOOKBACK_SLOTS` steps) - // - // 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 = &self.blocks[&target_block_root]; - } else { - break; - } - } - - // Ensure target is in justifiable slot range - // - // 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, self.latest_finalized.slot) { - target_block_root = target_block.parent_root; - target_block = &self.blocks[&target_block_root]; - } - Checkpoint { - root: target_block_root, - slot: target_block.slot, - } + // If post-state has a higher finalized checkpoint, update the store + if post_state.latest_finalized.slot > store.latest_finalized().slot { + store.set_latest_finalized(post_state.latest_finalized); } - /// Produce attestation data for the given slot. - pub fn produce_attestation_data(&self, slot: u64) -> AttestationData { - // Get the head block the validator sees for this slot - let head_checkpoint = Checkpoint { - root: self.head, - slot: self.blocks[&self.head].slot, - }; + // Store block and state + store.insert_block(block_root, block.clone()); + store.insert_state(block_root, post_state); - // Calculate the target checkpoint for this attestation - let target_checkpoint = self.get_attestation_target(); + // Process block body attestations and their signatures + let aggregated_attestations = &block.body.attestations; + let attestation_signatures = &signed_block.signature.attestation_signatures; - // Construct attestation data - AttestationData { - slot, - head: head_checkpoint, - target: target_checkpoint, - source: self.latest_justified, + // Process block body attestations. + // TODO: fail the block if an attestation is invalid. Right now we + // just log a warning. + for (att, proof) in aggregated_attestations + .iter() + .zip(attestation_signatures.iter()) + { + let validator_ids = aggregation_bits_to_validator_indices(&att.aggregation_bits); + let data_root = att.data.tree_hash_root(); + + for validator_id in validator_ids { + // Update Proof Map - Store the proof so future block builders can reuse this aggregation + let key: SignatureKey = (validator_id, data_root); + store.push_aggregated_payload(key, proof.clone()); + + // Update Fork Choice - Register the vote immediately (historical/on-chain) + let attestation = Attestation { + validator_id, + data: att.data.clone(), + }; + // TODO: validate attestations before processing + if let Err(err) = on_attestation(store, attestation, true) { + warn!(%slot, %validator_id, %err, "Invalid attestation in block"); + } } } - /// Get the head for block proposal at the given slot. - /// - /// Ensures store is up-to-date and processes any pending attestations - /// before returning the canonical head. - pub fn get_proposal_head(&mut self, slot: u64) -> H256 { - // Calculate time corresponding to this slot - let slot_time = self.config.genesis_time + slot * SECONDS_PER_SLOT; + // Update forkchoice head based on new block and attestations + // IMPORTANT: This must happen BEFORE processing proposer attestation + // to prevent the proposer from gaining circular weight advantage. + update_head(store); - // Advance time to current slot (ticking intervals) - self.on_tick(slot_time, true); + // Process proposer attestation as if received via gossip + // The proposer's attestation should NOT affect this block's fork choice position. + // It is treated as pending until interval 3 (end of slot). - // Process any pending attestations before proposal - self.accept_new_attestations(); + if cfg!(not(feature = "skip-signature-verification")) { + // Store the proposer's signature for potential future block building + let proposer_sig_key: SignatureKey = ( + proposer_attestation.validator_id, + proposer_attestation.data.tree_hash_root(), + ); + let proposer_sig = + ValidatorSignature::from_bytes(&signed_block.signature.proposer_signature) + .map_err(|_| StoreError::SignatureDecodingFailed)?; + store.insert_gossip_signature(proposer_sig_key, proposer_sig); + } - self.head + // Process proposer attestation (enters "new" stage, not "known") + // TODO: validate attestations before processing + if let Err(err) = on_attestation(store, proposer_attestation, false) { + warn!(%slot, %err, "Invalid proposer attestation in block"); } - /// Produce a block and per-aggregated-attestation signature payloads for the target slot. - /// - /// Returns the finalized block and attestation signature payloads aligned - /// with `block.body.attestations`. - pub fn produce_block_with_signatures( - &mut self, - slot: u64, - validator_index: u64, - ) -> Result<(Block, Vec), StoreError> { - // Get parent block and state to build upon - let head_root = self.get_proposal_head(slot); - let head_state = self - .states - .get(&head_root) - .ok_or(StoreError::MissingParentState { - parent_root: head_root, - slot, - })? - .clone(); - - // Validate proposer authorization for this slot - let num_validators = head_state.validators.len() as u64; - if !is_proposer(validator_index, slot, num_validators) { - return Err(StoreError::NotProposer { - validator_index, - slot, - }); + info!(%slot, %block_root, %state_root, "Processed new block"); + Ok(()) +} + +/// Calculate target checkpoint for validator attestations. +/// +/// NOTE: this assumes that we have all the blocks from the head back to the latest finalized. +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) + .expect("head block exists"); + + let safe_target_block_slot = store + .get_block(&store.safe_target()) + .expect("safe target exists") + .slot; + + // Walk back toward safe target (up to `JUSTIFICATION_LOOKBACK_SLOTS` steps) + // + // 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) + .expect("parent block exists"); + } else { + break; } + } - // Convert AttestationData to Attestation objects for build_block - let available_attestations: Vec = self - .latest_known_attestations - .iter() - .map(|(&validator_id, data)| Attestation { - validator_id, - data: data.clone(), - }) - .collect(); + // Ensure target is in justifiable slot range + // + // 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) + .expect("parent block exists"); + } + Checkpoint { + root: target_block_root, + slot: target_block.slot, + } +} - // Get known block roots for attestation validation - let known_block_roots: HashSet = self.blocks.keys().copied().collect(); +/// Produce attestation data for the given slot. +pub fn produce_attestation_data(store: &Store, slot: u64) -> AttestationData { + // Get the head block the validator sees for this slot + let head_checkpoint = Checkpoint { + root: store.head(), + slot: store + .get_block(&store.head()) + .expect("head block exists") + .slot, + }; - // Build the block using fixed-point attestation collection - let (block, _post_state, signatures) = build_block( - &head_state, - slot, - validator_index, - head_root, - &available_attestations, - &known_block_roots, - &self.gossip_signatures, - &self.aggregated_payloads, - )?; - - Ok((block, signatures)) - } + // Calculate the target checkpoint for this attestation + let target_checkpoint = get_attestation_target(store); - /// Returns the root of the current canonical chain head block. - pub fn head(&self) -> H256 { - self.head + // Construct attestation data + AttestationData { + slot, + head: head_checkpoint, + target: target_checkpoint, + source: *store.latest_justified(), } +} - /// Returns a reference to all known blocks. - pub fn blocks(&self) -> &HashMap { - &self.blocks - } +/// Get the head for block proposal at the given slot. +/// +/// Ensures store is up-to-date and processes any pending attestations +/// before returning the canonical head. +pub fn get_proposal_head(store: &mut Store, slot: u64) -> H256 { + // Calculate time corresponding to this slot + let slot_time = store.config().genesis_time + slot * SECONDS_PER_SLOT; - /// Returns a reference to the latest known attestations by validator. - pub fn latest_known_attestations(&self) -> &HashMap { - &self.latest_known_attestations - } + // Advance time to current slot (ticking intervals) + on_tick(store, slot_time, true); - /// Returns a reference to the latest new (pending) attestations by validator. - pub fn latest_new_attestations(&self) -> &HashMap { - &self.latest_new_attestations - } + // Process any pending attestations before proposal + accept_new_attestations(store); - /// Returns a reference to the latest justified checkpoint. - pub fn latest_justified(&self) -> &Checkpoint { - &self.latest_justified - } + store.head() +} - /// Returns a reference to the latest finalized checkpoint. - pub fn latest_finalized(&self) -> &Checkpoint { - &self.latest_finalized - } +/// Produce a block and per-aggregated-attestation signature payloads for the target slot. +/// +/// Returns the finalized block and attestation signature payloads aligned +/// with `block.body.attestations`. +pub fn produce_block_with_signatures( + store: &mut Store, + slot: u64, + validator_index: u64, +) -> Result<(Block, Vec), StoreError> { + // Get parent block and state to build upon + let head_root = get_proposal_head(store, slot); + let head_state = store + .get_state(&head_root) + .ok_or(StoreError::MissingParentState { + parent_root: head_root, + slot, + })? + .clone(); - /// Returns a reference to the chain configuration. - pub fn config(&self) -> &ChainConfig { - &self.config + // Validate proposer authorization for this slot + let num_validators = head_state.validators.len() as u64; + if !is_proposer(validator_index, slot, num_validators) { + return Err(StoreError::NotProposer { + validator_index, + slot, + }); } - /// Returns a reference to the head state if it exists. - pub fn head_state(&self) -> &State { - self.states - .get(&self.head) - .expect("head state is always available") - } + // Convert AttestationData to Attestation objects for build_block + let available_attestations: Vec = store + .latest_known_attestations() + .iter() + .map(|(&validator_id, data)| Attestation { + validator_id, + data: data.clone(), + }) + .collect(); - /// Returns the slot of the current safe target block. - pub fn safe_target_slot(&self) -> u64 { - self.blocks[&self.safe_target].slot - } + // Get known block roots for attestation validation + let known_block_roots: HashSet = store.blocks().keys().copied().collect(); + + // Build the block using fixed-point attestation collection + let (block, _post_state, signatures) = build_block( + &head_state, + slot, + validator_index, + head_root, + &available_attestations, + &known_block_roots, + store.gossip_signatures(), + store.aggregated_payloads(), + )?; + + Ok((block, signatures)) +} + +/// Returns the slot of the current safe target block. +pub fn safe_target_slot(store: &Store) -> u64 { + store + .get_block(&store.safe_target()) + .expect("safe target exists") + .slot +} + +/// Returns a reference to the head state if it exists. +pub fn head_state(store: &Store) -> &State { + store + .get_state(&store.head()) + .expect("head state is always available") } /// Errors that can occur during Store operations. diff --git a/crates/blockchain/tests/forkchoice_spectests.rs b/crates/blockchain/tests/forkchoice_spectests.rs index 718ced4..e6cfd56 100644 --- a/crates/blockchain/tests/forkchoice_spectests.rs +++ b/crates/blockchain/tests/forkchoice_spectests.rs @@ -3,7 +3,8 @@ use std::{ path::Path, }; -use ethlambda_blockchain::{SECONDS_PER_SLOT, store::Store}; +use ethlambda_blockchain::{SECONDS_PER_SLOT, store}; +use ethlambda_storage::Store; use ethlambda_types::{ attestation::Attestation, block::{Block, BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation}, @@ -34,7 +35,7 @@ fn run(path: &Path) -> datatest_stable::Result<()> { let anchor_state: State = test.anchor_state.into(); let anchor_block: Block = test.anchor_block.into(); let genesis_time = anchor_state.config.genesis_time; - let mut store = Store::get_forkchoice_store(anchor_state, anchor_block); + let mut store = store::get_forkchoice_store(anchor_state, anchor_block); // Block registry: maps block labels to their roots let mut block_registry: HashMap = HashMap::new(); @@ -58,8 +59,8 @@ fn run(path: &Path) -> datatest_stable::Result<()> { signed_block.message.block.slot * SECONDS_PER_SLOT + genesis_time; // NOTE: the has_proposal argument is set to true, following the spec - store.on_tick(block_time, true); - let result = store.on_block(signed_block); + store::on_tick(&mut store, block_time, true); + let result = store::on_block(&mut store, signed_block); match (result.is_ok(), step.valid) { (true, false) => { @@ -83,7 +84,7 @@ fn run(path: &Path) -> datatest_stable::Result<()> { "tick" => { let timestamp = step.time.expect("tick step missing time"); // NOTE: the has_proposal argument is set to false, following the spec - store.on_tick(timestamp, false); + store::on_tick(&mut store, timestamp, false); } other => { // Fail for unsupported step types for now @@ -117,7 +118,7 @@ fn build_signed_block(block_data: types::BlockStepData) -> SignedBlockWithAttest } fn validate_checks( - store: &Store, + st: &Store, checks: &StoreChecks, step_idx: usize, block_registry: &HashMap, @@ -152,7 +153,7 @@ fn validate_checks( } // Validate attestationTargetSlot if let Some(expected_slot) = checks.attestation_target_slot { - let target = store.get_attestation_target(); + let target = store::get_attestation_target(st); if target.slot != expected_slot { return Err(format!( "Step {}: attestationTargetSlot mismatch: expected {}, got {}", @@ -162,13 +163,13 @@ fn validate_checks( } // Also validate the root matches a block at this slot - let block_found = store + let block_found = st .blocks() .iter() .any(|(root, block)| block.slot == expected_slot && *root == target.root); if !block_found { - let available: Vec<_> = store + let available: Vec<_> = st .blocks() .iter() .filter(|(_, block)| block.slot == expected_slot) @@ -184,8 +185,8 @@ fn validate_checks( // Validate headSlot if let Some(expected_slot) = checks.head_slot { - let head_root = store.head(); - let head_block = store + let head_root = st.head(); + let head_block = st .blocks() .get(&head_root) .ok_or_else(|| format!("Step {}: head block not found", step_idx))?; @@ -200,7 +201,7 @@ fn validate_checks( // Validate headRoot if let Some(ref expected_root) = checks.head_root { - let head_root = store.head(); + let head_root = st.head(); if head_root != *expected_root { return Err(format!( "Step {}: headRoot mismatch: expected {:?}, got {:?}", @@ -212,7 +213,7 @@ fn validate_checks( // Validate latestJustifiedSlot if let Some(expected_slot) = checks.latest_justified_slot { - let justified = store.latest_justified(); + let justified = st.latest_justified(); if justified.slot != expected_slot { return Err(format!( "Step {}: latestJustifiedSlot mismatch: expected {}, got {}", @@ -224,7 +225,7 @@ fn validate_checks( // Validate latestJustifiedRoot if let Some(ref expected_root) = checks.latest_justified_root { - let justified = store.latest_justified(); + let justified = st.latest_justified(); if justified.root != *expected_root { return Err(format!( "Step {}: latestJustifiedRoot mismatch: expected {:?}, got {:?}", @@ -236,7 +237,7 @@ fn validate_checks( // Validate latestFinalizedSlot if let Some(expected_slot) = checks.latest_finalized_slot { - let finalized = store.latest_finalized(); + let finalized = st.latest_finalized(); if finalized.slot != expected_slot { return Err(format!( "Step {}: latestFinalizedSlot mismatch: expected {}, got {}", @@ -248,7 +249,7 @@ fn validate_checks( // Validate latestFinalizedRoot if let Some(ref expected_root) = checks.latest_finalized_root { - let finalized = store.latest_finalized(); + let finalized = st.latest_finalized(); if finalized.root != *expected_root { return Err(format!( "Step {}: latestFinalizedRoot mismatch: expected {:?}, got {:?}", @@ -261,20 +262,20 @@ fn validate_checks( // Validate attestationChecks if let Some(ref att_checks) = checks.attestation_checks { for att_check in att_checks { - validate_attestation_check(store, att_check, step_idx)?; + validate_attestation_check(st, att_check, step_idx)?; } } // Validate lexicographicHeadAmong if let Some(ref fork_labels) = checks.lexicographic_head_among { - validate_lexicographic_head_among(store, fork_labels, step_idx, block_registry)?; + validate_lexicographic_head_among(st, fork_labels, step_idx, block_registry)?; } Ok(()) } fn validate_attestation_check( - store: &Store, + st: &Store, check: &types::AttestationCheck, step_idx: usize, ) -> datatest_stable::Result<()> { @@ -282,8 +283,8 @@ fn validate_attestation_check( let location = check.location.as_str(); let attestations = match location { - "new" => store.latest_new_attestations(), - "known" => store.latest_known_attestations(), + "new" => st.latest_new_attestations(), + "known" => st.latest_known_attestations(), other => { return Err( format!("Step {}: unknown attestation location: {}", step_idx, other).into(), @@ -345,7 +346,7 @@ fn validate_attestation_check( } fn validate_lexicographic_head_among( - store: &Store, + st: &Store, fork_labels: &[String], step_idx: usize, block_registry: &HashMap, @@ -360,7 +361,7 @@ fn validate_lexicographic_head_among( .into()); } - let blocks = store.blocks(); + let blocks = st.blocks(); // Resolve all fork labels to roots and compute their weights // Map: label -> (root, slot, weight) @@ -385,7 +386,7 @@ fn validate_lexicographic_head_among( // Calculate attestation weight: count attestations voting for this fork // An attestation votes for this fork if its head is this block or a descendant let mut weight = 0; - for attestation in store.latest_known_attestations().values() { + for attestation in st.latest_known_attestations().values() { let att_head_root = attestation.head.root; // Check if attestation head is this block or a descendant if att_head_root == *root { @@ -451,7 +452,7 @@ fn validate_lexicographic_head_among( .expect("fork_data is not empty"); // Verify the current head matches the lexicographically highest root - let actual_head_root = store.head(); + let actual_head_root = st.head(); if actual_head_root != expected_head_root { let highest_label = fork_data .iter() diff --git a/crates/blockchain/tests/signature_spectests.rs b/crates/blockchain/tests/signature_spectests.rs index 0318d35..6069a4f 100644 --- a/crates/blockchain/tests/signature_spectests.rs +++ b/crates/blockchain/tests/signature_spectests.rs @@ -1,6 +1,6 @@ use std::path::Path; -use ethlambda_blockchain::{SECONDS_PER_SLOT, store::Store}; +use ethlambda_blockchain::{SECONDS_PER_SLOT, store}; use ethlambda_types::{ block::{Block, SignedBlockWithAttestation}, primitives::TreeHash, @@ -40,17 +40,17 @@ fn run(path: &Path) -> datatest_stable::Result<()> { // Initialize the store with the anchor state and block let genesis_time = anchor_state.config.genesis_time; - let mut store = Store::get_forkchoice_store(anchor_state, anchor_block); + let mut st = store::get_forkchoice_store(anchor_state, anchor_block); // Step 2: Run the state transition function with the block fixture let signed_block: SignedBlockWithAttestation = test.signed_block_with_attestation.into(); // Advance time to the block's slot let block_time = signed_block.message.block.slot * SECONDS_PER_SLOT + genesis_time; - store.on_tick(block_time, true); + store::on_tick(&mut st, block_time, true); // Process the block (this includes signature verification) - let result = store.on_block(signed_block); + let result = store::on_block(&mut st, signed_block); // Step 3: Check that it succeeded or failed as expected match (result.is_ok(), test.expect_exception.as_ref()) { diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 8b13789..d8a7da2 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -1 +1,307 @@ +use std::collections::HashMap; +use ethlambda_types::{ + attestation::AttestationData, + block::{AggregatedSignatureProof, Block}, + primitives::H256, + signature::ValidatorSignature, + state::{ChainConfig, Checkpoint, State}, +}; + +/// Key for looking up individual validator signatures. +/// Used to index signature caches by (validator, message) pairs. +/// +/// Values are (validator_index, attestation_data_root). +pub type SignatureKey = (u64, H256); + +/// Forkchoice store tracking chain state and validator attestations. +/// +/// This is the "local view" that a node uses to run LMD GHOST. It contains: +/// +/// - which blocks and states are known, +/// - which checkpoints are justified and finalized, +/// - which block is currently considered the head, +/// - and, for each validator, their latest attestation that should influence fork choice. +/// +/// The `Store` is updated whenever: +/// - a new block is processed, +/// - an attestation is received (via a block or gossip), +/// - an interval tick occurs (activating new attestations), +/// - or when the head is recomputed. +#[derive(Clone)] +pub struct Store { + /// Current time in intervals since genesis. + time: u64, + + /// Chain configuration parameters. + config: ChainConfig, + + /// Root of the current canonical chain head block. + /// + /// This is the result of running the fork choice algorithm on the current contents of the `Store`. + head: H256, + + /// Root of the current safe target for attestation. + /// + /// This can be used by higher-level logic to restrict which blocks are + /// considered safe to attest to, based on additional safety conditions. + /// + safe_target: H256, + + /// Highest slot justified checkpoint known to the store. + /// + /// LMD GHOST starts from this checkpoint when computing the head. + /// + /// Only descendants of this checkpoint are considered viable. + latest_justified: Checkpoint, + + /// Highest slot finalized checkpoint known to the store. + /// + /// Everything strictly before this checkpoint can be considered immutable. + /// + /// Fork choice will never revert finalized history. + latest_finalized: Checkpoint, + + /// Mapping from block root to Block objects. + /// + /// This is the set of blocks that the node currently knows about. + /// + /// Every block that might participate in fork choice must appear here. + blocks: HashMap, + + /// Mapping from block root to State objects. + /// + /// For each known block, we keep its post-state. + /// + /// These states carry justified and finalized checkpoints that we use to update the + /// `Store`'s latest justified and latest finalized checkpoints. + states: HashMap, + + /// Latest signed attestations by validator that have been processed. + /// + /// - These attestations are "known" and contribute to fork choice weights. + /// - Keyed by validator index to enforce one attestation per validator. + latest_known_attestations: HashMap, + + /// Latest signed attestations by validator that are pending processing. + /// + /// - These attestations are "new" and do not yet contribute to fork choice. + /// - They migrate to `latest_known_attestations` via interval ticks. + /// - Keyed by validator index to enforce one attestation per validator. + latest_new_attestations: HashMap, + + /// Per-validator XMSS signatures learned from gossip. + /// + /// Keyed by SignatureKey(validator_id, attestation_data_root). + gossip_signatures: HashMap, + + /// Aggregated signature proofs learned from blocks. + /// - Keyed by SignatureKey(validator_id, attestation_data_root). + /// - Values are lists of AggregatedSignatureProof, each containing the participants + /// bitfield indicating which validators signed. + /// - Used for recursive signature aggregation when building blocks. + /// - Populated by on_block. + aggregated_payloads: HashMap>, +} + +impl Store { + /// Creates a new Store with the given initial values. + #[expect(clippy::too_many_arguments)] + pub fn new( + time: u64, + config: ChainConfig, + head: H256, + safe_target: H256, + latest_justified: Checkpoint, + latest_finalized: Checkpoint, + blocks: HashMap, + states: HashMap, + ) -> Self { + Self { + time, + config, + head, + safe_target, + latest_justified, + latest_finalized, + blocks, + states, + latest_known_attestations: HashMap::new(), + latest_new_attestations: HashMap::new(), + gossip_signatures: HashMap::new(), + aggregated_payloads: HashMap::new(), + } + } + + // ============ Time ============ + + pub fn time(&self) -> u64 { + self.time + } + + pub fn set_time(&mut self, time: u64) { + self.time = time; + } + + // ============ Config ============ + + pub fn config(&self) -> &ChainConfig { + &self.config + } + + // ============ Head ============ + + pub fn head(&self) -> H256 { + self.head + } + + pub fn set_head(&mut self, head: H256) { + self.head = head; + } + + // ============ Safe Target ============ + + pub fn safe_target(&self) -> H256 { + self.safe_target + } + + pub fn set_safe_target(&mut self, safe_target: H256) { + self.safe_target = safe_target; + } + + // ============ Latest Justified ============ + + pub fn latest_justified(&self) -> &Checkpoint { + &self.latest_justified + } + + pub fn set_latest_justified(&mut self, checkpoint: Checkpoint) { + self.latest_justified = checkpoint; + } + + // ============ Latest Finalized ============ + + pub fn latest_finalized(&self) -> &Checkpoint { + &self.latest_finalized + } + + pub fn set_latest_finalized(&mut self, checkpoint: Checkpoint) { + self.latest_finalized = checkpoint; + } + + // ============ Blocks ============ + + pub fn blocks(&self) -> &HashMap { + &self.blocks + } + + pub fn get_block(&self, root: &H256) -> Option<&Block> { + self.blocks.get(root) + } + + pub fn contains_block(&self, root: &H256) -> bool { + self.blocks.contains_key(root) + } + + pub fn insert_block(&mut self, root: H256, block: Block) { + self.blocks.insert(root, block); + } + + // ============ States ============ + + pub fn states(&self) -> &HashMap { + &self.states + } + + pub fn get_state(&self, root: &H256) -> Option<&State> { + self.states.get(root) + } + + pub fn insert_state(&mut self, root: H256, state: State) { + self.states.insert(root, state); + } + + // ============ Latest Known Attestations ============ + + pub fn latest_known_attestations(&self) -> &HashMap { + &self.latest_known_attestations + } + + pub fn get_known_attestation(&self, validator_id: &u64) -> Option<&AttestationData> { + self.latest_known_attestations.get(validator_id) + } + + pub fn insert_known_attestation(&mut self, validator_id: u64, data: AttestationData) { + self.latest_known_attestations.insert(validator_id, data); + } + + pub fn extend_known_attestations(&mut self, iter: I) + where + I: IntoIterator, + { + self.latest_known_attestations.extend(iter); + } + + // ============ Latest New Attestations ============ + + pub fn latest_new_attestations(&self) -> &HashMap { + &self.latest_new_attestations + } + + pub fn get_new_attestation(&self, validator_id: &u64) -> Option<&AttestationData> { + self.latest_new_attestations.get(validator_id) + } + + pub fn insert_new_attestation(&mut self, validator_id: u64, data: AttestationData) { + self.latest_new_attestations.insert(validator_id, data); + } + + pub fn remove_new_attestation(&mut self, validator_id: &u64) { + self.latest_new_attestations.remove(validator_id); + } + + /// Takes all new attestations, leaving an empty map in their place. + pub fn take_new_attestations(&mut self) -> HashMap { + std::mem::take(&mut self.latest_new_attestations) + } + + /// Restores new attestations (used after take_new_attestations). + pub fn restore_new_attestations(&mut self, attestations: HashMap) { + self.latest_new_attestations = attestations; + } + + // ============ Gossip Signatures ============ + + pub fn gossip_signatures(&self) -> &HashMap { + &self.gossip_signatures + } + + pub fn get_gossip_signature(&self, key: &SignatureKey) -> Option<&ValidatorSignature> { + self.gossip_signatures.get(key) + } + + pub fn contains_gossip_signature(&self, key: &SignatureKey) -> bool { + self.gossip_signatures.contains_key(key) + } + + pub fn insert_gossip_signature(&mut self, key: SignatureKey, signature: ValidatorSignature) { + self.gossip_signatures.insert(key, signature); + } + + // ============ Aggregated Payloads ============ + + pub fn aggregated_payloads(&self) -> &HashMap> { + &self.aggregated_payloads + } + + pub fn get_aggregated_payloads( + &self, + key: &SignatureKey, + ) -> Option<&Vec> { + self.aggregated_payloads.get(key) + } + + pub fn push_aggregated_payload(&mut self, key: SignatureKey, proof: AggregatedSignatureProof) { + self.aggregated_payloads.entry(key).or_default().push(proof); + } +} From dc726b58820c7eb9e06791df35f983c97fde2be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:22:08 -0300 Subject: [PATCH 02/15] refactor: move constructors to be Store methods Also, merge attestation functions into promote_new_attestations --- crates/blockchain/src/lib.rs | 2 +- crates/blockchain/src/store.rs | 51 +----------- .../blockchain/tests/forkchoice_spectests.rs | 2 +- .../blockchain/tests/signature_spectests.rs | 3 +- crates/storage/src/lib.rs | 79 +++++++++++++++---- 5 files changed, 67 insertions(+), 70 deletions(-) diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 4ca5a2a..54a767b 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -45,7 +45,7 @@ impl BlockChain { validator_keys: HashMap, ) -> BlockChain { let genesis_time = genesis_state.config.genesis_time; - let store = store::from_genesis(genesis_state); + let store = Store::from_genesis(genesis_state); let key_manager = key_manager::KeyManager::new(validator_keys); let handle = BlockChainServer { store, diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 9a8a914..9e98f7b 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -21,58 +21,9 @@ use crate::SECONDS_PER_SLOT; const JUSTIFICATION_LOOKBACK_SLOTS: u64 = 3; -/// Initialize a Store from a genesis state. -pub fn from_genesis(mut genesis_state: State) -> Store { - // 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: Default::default(), - }; - get_forkchoice_store(genesis_state, genesis_block) -} - -/// Initialize a Store from an anchor state and block. -pub fn get_forkchoice_store(anchor_state: State, anchor_block: Block) -> Store { - let anchor_state_root = anchor_state.tree_hash_root(); - let anchor_block_root = anchor_block.tree_hash_root(); - - let mut blocks = HashMap::new(); - blocks.insert(anchor_block_root, anchor_block.clone()); - - let mut states = HashMap::new(); - states.insert(anchor_block_root, anchor_state.clone()); - - let anchor_checkpoint = Checkpoint { - root: anchor_block_root, - slot: 0, - }; - - info!(%anchor_state_root, %anchor_block_root, "Initialized store"); - - Store::new( - 0, - anchor_state.config.clone(), - anchor_block_root, - anchor_block_root, - anchor_checkpoint, - anchor_checkpoint, - blocks, - states, - ) -} - /// Accept new attestations, moving them from pending to known. pub fn accept_new_attestations(store: &mut Store) { - let mut latest_new_attestations = store.take_new_attestations(); - store.extend_known_attestations(latest_new_attestations.drain()); - store.restore_new_attestations(latest_new_attestations); - + store.promote_new_attestations(); update_head(store); } diff --git a/crates/blockchain/tests/forkchoice_spectests.rs b/crates/blockchain/tests/forkchoice_spectests.rs index e6cfd56..636260d 100644 --- a/crates/blockchain/tests/forkchoice_spectests.rs +++ b/crates/blockchain/tests/forkchoice_spectests.rs @@ -35,7 +35,7 @@ fn run(path: &Path) -> datatest_stable::Result<()> { let anchor_state: State = test.anchor_state.into(); let anchor_block: Block = test.anchor_block.into(); let genesis_time = anchor_state.config.genesis_time; - let mut store = store::get_forkchoice_store(anchor_state, anchor_block); + let mut store = Store::get_forkchoice_store(anchor_state, anchor_block); // Block registry: maps block labels to their roots let mut block_registry: HashMap = HashMap::new(); diff --git a/crates/blockchain/tests/signature_spectests.rs b/crates/blockchain/tests/signature_spectests.rs index 6069a4f..a0fb24c 100644 --- a/crates/blockchain/tests/signature_spectests.rs +++ b/crates/blockchain/tests/signature_spectests.rs @@ -1,6 +1,7 @@ use std::path::Path; use ethlambda_blockchain::{SECONDS_PER_SLOT, store}; +use ethlambda_storage::Store; use ethlambda_types::{ block::{Block, SignedBlockWithAttestation}, primitives::TreeHash, @@ -40,7 +41,7 @@ fn run(path: &Path) -> datatest_stable::Result<()> { // Initialize the store with the anchor state and block let genesis_time = anchor_state.config.genesis_time; - let mut st = store::get_forkchoice_store(anchor_state, anchor_block); + let mut st = Store::get_forkchoice_store(anchor_state, anchor_block); // Step 2: Run the state transition function with the block fixture let signed_block: SignedBlockWithAttestation = test.signed_block_with_attestation.into(); diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index d8a7da2..c88862f 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -2,11 +2,12 @@ use std::collections::HashMap; use ethlambda_types::{ attestation::AttestationData, - block::{AggregatedSignatureProof, Block}, - primitives::H256, + block::{AggregatedSignatureProof, Block, BlockBody}, + primitives::{H256, TreeHash}, signature::ValidatorSignature, state::{ChainConfig, Checkpoint, State}, }; +use tracing::info; /// Key for looking up individual validator signatures. /// Used to index signature caches by (validator, message) pairs. @@ -105,6 +106,56 @@ pub struct Store { } impl Store { + /// Initialize a Store from a genesis state. + pub fn from_genesis(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(genesis_state, genesis_block) + } + + /// Initialize a Store from an anchor state and block. + pub fn get_forkchoice_store(anchor_state: State, anchor_block: Block) -> Self { + let anchor_state_root = anchor_state.tree_hash_root(); + let anchor_block_root = anchor_block.tree_hash_root(); + + let mut blocks = HashMap::new(); + blocks.insert(anchor_block_root, anchor_block); + + let mut states = HashMap::new(); + states.insert(anchor_block_root, anchor_state.clone()); + + let anchor_checkpoint = Checkpoint { + root: anchor_block_root, + slot: 0, + }; + + info!(%anchor_state_root, %anchor_block_root, "Initialized store"); + + Self { + time: 0, + config: anchor_state.config.clone(), + head: anchor_block_root, + safe_target: anchor_block_root, + latest_justified: anchor_checkpoint, + latest_finalized: anchor_checkpoint, + blocks, + states, + latest_known_attestations: HashMap::new(), + latest_new_attestations: HashMap::new(), + gossip_signatures: HashMap::new(), + aggregated_payloads: HashMap::new(), + } + } + /// Creates a new Store with the given initial values. #[expect(clippy::too_many_arguments)] pub fn new( @@ -235,13 +286,6 @@ impl Store { self.latest_known_attestations.insert(validator_id, data); } - pub fn extend_known_attestations(&mut self, iter: I) - where - I: IntoIterator, - { - self.latest_known_attestations.extend(iter); - } - // ============ Latest New Attestations ============ pub fn latest_new_attestations(&self) -> &HashMap { @@ -260,14 +304,15 @@ impl Store { self.latest_new_attestations.remove(validator_id); } - /// Takes all new attestations, leaving an empty map in their place. - pub fn take_new_attestations(&mut self) -> HashMap { - std::mem::take(&mut self.latest_new_attestations) - } - - /// Restores new attestations (used after take_new_attestations). - pub fn restore_new_attestations(&mut self, attestations: HashMap) { - self.latest_new_attestations = attestations; + /// Promotes all new attestations to known attestations. + /// + /// Takes all attestations from `latest_new_attestations` and moves them + /// to `latest_known_attestations`, making them count for fork choice. + pub fn promote_new_attestations(&mut self) { + let mut new_attestations = std::mem::take(&mut self.latest_new_attestations); + self.latest_known_attestations + .extend(new_attestations.drain()); + self.latest_new_attestations = new_attestations; } // ============ Gossip Signatures ============ From dc8055436781011fac45d9f0d09195869fe744d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:26:25 -0300 Subject: [PATCH 03/15] refactor: remove unused pubs --- crates/blockchain/src/store.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 9e98f7b..821c836 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -22,13 +22,13 @@ use crate::SECONDS_PER_SLOT; const JUSTIFICATION_LOOKBACK_SLOTS: u64 = 3; /// Accept new attestations, moving them from pending to known. -pub fn accept_new_attestations(store: &mut Store) { +fn accept_new_attestations(store: &mut Store) { store.promote_new_attestations(); update_head(store); } /// Update the head based on the fork choice rule. -pub fn update_head(store: &mut Store) { +fn update_head(store: &mut Store) { let head = ethlambda_fork_choice::compute_lmd_ghost_head( store.latest_justified().root, store.blocks(), @@ -39,7 +39,7 @@ pub fn update_head(store: &mut Store) { } /// Update the safe target for attestation. -pub fn update_safe_target(store: &mut Store) { +fn update_safe_target(store: &mut Store) { let head_state = store.get_state(&store.head()).expect("head state exists"); let num_validators = head_state.validators.len() as u64; @@ -60,7 +60,7 @@ pub fn update_safe_target(store: &mut Store) { /// 1. The blocks voted for must exist in our store. /// 2. A vote cannot span backwards in time (source > target). /// 3. A vote cannot be for a future slot. -pub fn validate_attestation(store: &Store, attestation: &Attestation) -> Result<(), StoreError> { +fn validate_attestation(store: &Store, attestation: &Attestation) -> Result<(), StoreError> { let data = &attestation.data; // Availability Check - We cannot count a vote if we haven't seen the blocks involved. @@ -459,7 +459,7 @@ pub fn produce_attestation_data(store: &Store, slot: u64) -> AttestationData { /// /// Ensures store is up-to-date and processes any pending attestations /// before returning the canonical head. -pub fn get_proposal_head(store: &mut Store, slot: u64) -> H256 { +fn get_proposal_head(store: &mut Store, slot: u64) -> H256 { // Calculate time corresponding to this slot let slot_time = store.config().genesis_time + slot * SECONDS_PER_SLOT; From 916d54c44d8fc04ba2d92f07e5facf8f50d240c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:23:29 -0300 Subject: [PATCH 04/15] refactor: remove some unused methods --- crates/blockchain/src/lib.rs | 6 +++--- crates/blockchain/src/store.rs | 15 --------------- crates/storage/src/lib.rs | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 54a767b..8c44a0e 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -122,12 +122,12 @@ impl BlockChainServer { } // Update safe target slot metric (updated by store.on_tick at interval 2) - metrics::update_safe_target_slot(store::safe_target_slot(&self.store)); + metrics::update_safe_target_slot(self.store.safe_target_slot()); } /// Returns the validator ID if any of our validators is the proposer for this slot. fn get_our_proposer(&self, slot: u64) -> Option { - let head_state = store::head_state(&self.store); + let head_state = self.store.head_state(); let num_validators = head_state.validators.len() as u64; self.key_manager @@ -138,7 +138,7 @@ impl BlockChainServer { fn produce_attestations(&mut self, slot: u64) { // Get the head state to determine number of validators - let head_state = store::head_state(&self.store); + let head_state = self.store.head_state(); let num_validators = head_state.validators.len() as u64; diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 821c836..fa46462 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -528,21 +528,6 @@ pub fn produce_block_with_signatures( Ok((block, signatures)) } -/// Returns the slot of the current safe target block. -pub fn safe_target_slot(store: &Store) -> u64 { - store - .get_block(&store.safe_target()) - .expect("safe target exists") - .slot -} - -/// Returns a reference to the head state if it exists. -pub fn head_state(store: &Store) -> &State { - store - .get_state(&store.head()) - .expect("head state is always available") -} - /// Errors that can occur during Store operations. #[derive(Debug, thiserror::Error)] pub enum StoreError { diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index c88862f..d67d72d 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -349,4 +349,19 @@ impl Store { pub fn push_aggregated_payload(&mut self, key: SignatureKey, proof: AggregatedSignatureProof) { self.aggregated_payloads.entry(key).or_default().push(proof); } + + // ============ Derived Accessors ============ + + /// Returns the slot of the current safe target block. + pub fn safe_target_slot(&self) -> u64 { + self.get_block(&self.safe_target) + .expect("safe target exists") + .slot + } + + /// Returns a reference to the head state. + pub fn head_state(&self) -> &State { + self.get_state(&self.head) + .expect("head state is always available") + } } From b5ff952c08ad9b0ba855ee02ac27b9438a2ddb0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:43:45 -0300 Subject: [PATCH 05/15] refactor: merge head, justified, finalized setters --- crates/blockchain/src/store.rs | 18 +++++------ crates/storage/src/lib.rs | 59 ++++++++++++++++++++++++++++------ 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index fa46462..e092179 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -4,7 +4,7 @@ use ethlambda_crypto::aggregate_signatures; use ethlambda_state_transition::{ is_proposer, process_block, process_slots, slot_is_justifiable_after, }; -use ethlambda_storage::{SignatureKey, Store}; +use ethlambda_storage::{ForkCheckpoints, SignatureKey, Store}; use ethlambda_types::{ attestation::{AggregatedAttestation, Attestation, AttestationData, SignedAttestation}, block::{ @@ -35,7 +35,7 @@ fn update_head(store: &mut Store) { store.latest_known_attestations(), 0, ); - store.set_head(head); + store.update_checkpoints(ForkCheckpoints::head_only(head)); } /// Update the safe target for attestation. @@ -310,14 +310,14 @@ pub fn on_block( let state_root = block.state_root; post_state.latest_block_header.state_root = state_root; - // If post-state has a higher justified checkpoint, update the store - if post_state.latest_justified.slot > store.latest_justified().slot { - store.set_latest_justified(post_state.latest_justified); - } + // Update justified/finalized checkpoints if they have higher slots + let justified = (post_state.latest_justified.slot > store.latest_justified().slot) + .then_some(post_state.latest_justified); + let finalized = (post_state.latest_finalized.slot > store.latest_finalized().slot) + .then_some(post_state.latest_finalized); - // If post-state has a higher finalized checkpoint, update the store - if post_state.latest_finalized.slot > store.latest_finalized().slot { - store.set_latest_finalized(post_state.latest_finalized); + if justified.is_some() || finalized.is_some() { + store.update_checkpoints(ForkCheckpoints::new(store.head(), justified, finalized)); } // Store block and state diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index d67d72d..f2b83fa 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -15,6 +15,38 @@ use tracing::info; /// Values are (validator_index, attestation_data_root). pub type SignatureKey = (u64, H256); +/// Checkpoints to update in the forkchoice store. +/// +/// Used with `Store::update_checkpoints` to update head and optionally +/// update justified/finalized checkpoints (only if higher slot). +pub struct ForkCheckpoints { + head: H256, + justified: Option, + finalized: Option, +} + +impl ForkCheckpoints { + /// Create checkpoints update with only the head. + pub fn head_only(head: H256) -> Self { + Self { + head, + justified: None, + finalized: None, + } + } + + /// Create checkpoints update with optional justified and finalized. + /// + /// The head is passed through unchanged. + pub fn new(head: H256, justified: Option, finalized: Option) -> Self { + Self { + head, + justified, + finalized, + } + } +} + /// Forkchoice store tracking chain state and validator attestations. /// /// This is the "local view" that a node uses to run LMD GHOST. It contains: @@ -206,10 +238,6 @@ impl Store { self.head } - pub fn set_head(&mut self, head: H256) { - self.head = head; - } - // ============ Safe Target ============ pub fn safe_target(&self) -> H256 { @@ -226,18 +254,29 @@ impl Store { &self.latest_justified } - pub fn set_latest_justified(&mut self, checkpoint: Checkpoint) { - self.latest_justified = checkpoint; - } - // ============ Latest Finalized ============ pub fn latest_finalized(&self) -> &Checkpoint { &self.latest_finalized } - pub fn set_latest_finalized(&mut self, checkpoint: Checkpoint) { - self.latest_finalized = checkpoint; + // ============ Checkpoint Updates ============ + + /// Updates head, justified, and finalized checkpoints. + /// + /// - Head is always updated to the new value. + /// - Justified is updated if provided. + /// - Finalized is updated if provided. + pub fn update_checkpoints(&mut self, checkpoints: ForkCheckpoints) { + self.head = checkpoints.head; + + if let Some(justified) = checkpoints.justified { + self.latest_justified = justified; + } + + if let Some(finalized) = checkpoints.finalized { + self.latest_finalized = finalized; + } } // ============ Blocks ============ From bffd47ed9f739b8e1fff5d00898eb0bb68c97d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:37:42 -0300 Subject: [PATCH 06/15] feat: add StorageBackend API --- crates/storage/src/api/mod.rs | 7 + crates/storage/src/api/tables.rs | 29 +++ crates/storage/src/api/traits.rs | 41 ++++ crates/storage/src/backend/in_memory.rs | 242 ++++++++++++++++++++++++ crates/storage/src/backend/mod.rs | 5 + crates/storage/src/lib.rs | 3 + 6 files changed, 327 insertions(+) create mode 100644 crates/storage/src/api/mod.rs create mode 100644 crates/storage/src/api/tables.rs create mode 100644 crates/storage/src/api/traits.rs create mode 100644 crates/storage/src/backend/in_memory.rs create mode 100644 crates/storage/src/backend/mod.rs diff --git a/crates/storage/src/api/mod.rs b/crates/storage/src/api/mod.rs new file mode 100644 index 0000000..f005aa8 --- /dev/null +++ b/crates/storage/src/api/mod.rs @@ -0,0 +1,7 @@ +#![allow(dead_code, unused_imports)] // Infrastructure not yet integrated with Store + +mod tables; +mod traits; + +pub use tables::{Table, ALL_TABLES}; +pub use traits::{Error, PrefixResult, StorageBackend, StorageReadView, StorageWriteBatch}; diff --git a/crates/storage/src/api/tables.rs b/crates/storage/src/api/tables.rs new file mode 100644 index 0000000..0b99940 --- /dev/null +++ b/crates/storage/src/api/tables.rs @@ -0,0 +1,29 @@ +/// Tables in the storage layer. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Table { + /// Block storage: H256 -> Block + Blocks, + /// State storage: H256 -> State + States, + /// Known attestations: u64 -> AttestationData + LatestKnownAttestations, + /// Pending attestations: u64 -> AttestationData + LatestNewAttestations, + /// Gossip signatures: SignatureKey -> ValidatorSignature + GossipSignatures, + /// Aggregated proofs: SignatureKey -> Vec + AggregatedPayloads, + /// Metadata: string keys -> various scalar values + Metadata, +} + +/// All table variants. +pub const ALL_TABLES: [Table; 7] = [ + Table::Blocks, + Table::States, + Table::LatestKnownAttestations, + Table::LatestNewAttestations, + Table::GossipSignatures, + Table::AggregatedPayloads, + Table::Metadata, +]; diff --git a/crates/storage/src/api/traits.rs b/crates/storage/src/api/traits.rs new file mode 100644 index 0000000..1c30028 --- /dev/null +++ b/crates/storage/src/api/traits.rs @@ -0,0 +1,41 @@ +use super::Table; + +/// Storage error type. +pub type Error = Box; + +/// Result type for prefix iterator operations. +pub type PrefixResult = Result<(Box<[u8]>, Box<[u8]>), Error>; + +/// A storage backend that can create read views and write batches. +pub trait StorageBackend { + /// Begin a read-only transaction. + fn begin_read(&self) -> Result, Error>; + + /// Begin a write batch. + fn begin_write(&self) -> Result, Error>; +} + +/// A read-only view of the storage. +pub trait StorageReadView { + /// Get a value by key from a table. + fn get(&self, table: Table, key: &[u8]) -> Result>, Error>; + + /// Iterate over all entries with a given key prefix. + fn prefix_iterator( + &self, + table: Table, + prefix: &[u8], + ) -> Result + '_>, Error>; +} + +/// A write batch that can be committed atomically. +pub trait StorageWriteBatch: Send { + /// Put multiple key-value pairs into a table. + fn put_batch(&mut self, table: Table, batch: Vec<(Vec, Vec)>) -> Result<(), Error>; + + /// Delete multiple keys from a table. + fn delete_batch(&mut self, table: Table, keys: Vec>) -> Result<(), Error>; + + /// Commit the batch, consuming it. + fn commit(self: Box) -> Result<(), Error>; +} diff --git a/crates/storage/src/backend/in_memory.rs b/crates/storage/src/backend/in_memory.rs new file mode 100644 index 0000000..8bd1863 --- /dev/null +++ b/crates/storage/src/backend/in_memory.rs @@ -0,0 +1,242 @@ +#![allow(dead_code)] // Infrastructure not yet integrated with Store + +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; + +use crate::api::{Error, PrefixResult, StorageBackend, StorageReadView, StorageWriteBatch, Table}; + +type TableData = HashMap, Vec>; +type StorageData = HashMap; +type PendingEntries = HashMap, Vec)>>; +type PendingDeletes = HashMap>>; + +/// In-memory storage backend using HashMaps. +#[derive(Clone, Default)] +pub struct InMemoryBackend { + data: Arc>, +} + +impl InMemoryBackend { + /// Create a new empty in-memory backend. + pub fn new() -> Self { + Self::default() + } +} + +impl StorageBackend for InMemoryBackend { + fn begin_read(&self) -> Result, Error> { + let guard = self.data.read().map_err(|e| e.to_string())?; + Ok(Box::new(InMemoryReadView { guard })) + } + + fn begin_write(&self) -> Result, Error> { + Ok(Box::new(InMemoryWriteBatch { + data: Arc::clone(&self.data), + pending: HashMap::new(), + deletes: HashMap::new(), + })) + } +} + +/// Read view holding a read lock on the storage data. +struct InMemoryReadView<'a> { + guard: std::sync::RwLockReadGuard<'a, StorageData>, +} + +impl StorageReadView for InMemoryReadView<'_> { + fn get(&self, table: Table, key: &[u8]) -> Result>, Error> { + Ok(self.guard.get(&table).and_then(|t| t.get(key)).cloned()) + } + + fn prefix_iterator( + &self, + table: Table, + prefix: &[u8], + ) -> Result + '_>, Error> { + let table_data = self.guard.get(&table); + let prefix_owned = prefix.to_vec(); + + let iter: Box + '_> = match table_data { + Some(data) => Box::new( + data.iter() + .filter(move |(k, _)| k.starts_with(&prefix_owned)) + .map(|(k, v)| Ok((k.clone().into_boxed_slice(), v.clone().into_boxed_slice()))), + ), + None => Box::new(std::iter::empty()), + }; + + Ok(iter) + } +} + +/// Write batch that accumulates changes before committing. +struct InMemoryWriteBatch { + data: Arc>, + pending: PendingEntries, + deletes: PendingDeletes, +} + +impl StorageWriteBatch for InMemoryWriteBatch { + fn put_batch(&mut self, table: Table, batch: Vec<(Vec, Vec)>) -> Result<(), Error> { + self.pending.entry(table).or_default().extend(batch); + Ok(()) + } + + fn delete_batch(&mut self, table: Table, keys: Vec>) -> Result<(), Error> { + self.deletes.entry(table).or_default().extend(keys); + Ok(()) + } + + fn commit(self: Box) -> Result<(), Error> { + let mut guard = self.data.write().map_err(|e| e.to_string())?; + + // Apply puts + for (table, entries) in self.pending { + let table_data = guard.entry(table).or_default(); + for (key, value) in entries { + table_data.insert(key, value); + } + } + + // Apply deletes + for (table, keys) in self.deletes { + if let Some(table_data) = guard.get_mut(&table) { + for key in keys { + table_data.remove(&key); + } + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_put_and_get() { + let backend = InMemoryBackend::new(); + + // Write data + { + let mut batch = backend.begin_write().unwrap(); + batch + .put_batch(Table::Blocks, vec![(b"key1".to_vec(), b"value1".to_vec())]) + .unwrap(); + batch.commit().unwrap(); + } + + // Read data + { + let view = backend.begin_read().unwrap(); + let value = view.get(Table::Blocks, b"key1").unwrap(); + assert_eq!(value, Some(b"value1".to_vec())); + } + } + + #[test] + fn test_delete() { + let backend = InMemoryBackend::new(); + + // Write data + { + let mut batch = backend.begin_write().unwrap(); + batch + .put_batch(Table::Blocks, vec![(b"key1".to_vec(), b"value1".to_vec())]) + .unwrap(); + batch.commit().unwrap(); + } + + // Delete data + { + let mut batch = backend.begin_write().unwrap(); + batch + .delete_batch(Table::Blocks, vec![b"key1".to_vec()]) + .unwrap(); + batch.commit().unwrap(); + } + + // Verify deleted + { + let view = backend.begin_read().unwrap(); + let value = view.get(Table::Blocks, b"key1").unwrap(); + assert_eq!(value, None); + } + } + + #[test] + fn test_prefix_iterator() { + let backend = InMemoryBackend::new(); + + // Write data with common prefix + { + let mut batch = backend.begin_write().unwrap(); + batch + .put_batch( + Table::Metadata, + vec![ + (b"config:a".to_vec(), b"1".to_vec()), + (b"config:b".to_vec(), b"2".to_vec()), + (b"other:x".to_vec(), b"3".to_vec()), + ], + ) + .unwrap(); + batch.commit().unwrap(); + } + + // Query by prefix + { + let view = backend.begin_read().unwrap(); + let mut results: Vec<_> = view + .prefix_iterator(Table::Metadata, b"config:") + .unwrap() + .collect::, _>>() + .unwrap(); + + results.sort_by(|a, b| a.0.cmp(&b.0)); + assert_eq!(results.len(), 2); + assert_eq!(&*results[0].0, b"config:a"); + assert_eq!(&*results[1].0, b"config:b"); + } + } + + #[test] + fn test_nonexistent_key() { + let backend = InMemoryBackend::new(); + let view = backend.begin_read().unwrap(); + let value = view.get(Table::Blocks, b"nonexistent").unwrap(); + assert_eq!(value, None); + } + + #[test] + fn test_multiple_tables() { + let backend = InMemoryBackend::new(); + + // Write to different tables + { + let mut batch = backend.begin_write().unwrap(); + batch + .put_batch(Table::Blocks, vec![(b"key".to_vec(), b"block".to_vec())]) + .unwrap(); + batch + .put_batch(Table::States, vec![(b"key".to_vec(), b"state".to_vec())]) + .unwrap(); + batch.commit().unwrap(); + } + + // Verify isolation + { + let view = backend.begin_read().unwrap(); + assert_eq!( + view.get(Table::Blocks, b"key").unwrap(), + Some(b"block".to_vec()) + ); + assert_eq!( + view.get(Table::States, b"key").unwrap(), + Some(b"state".to_vec()) + ); + } + } +} diff --git a/crates/storage/src/backend/mod.rs b/crates/storage/src/backend/mod.rs new file mode 100644 index 0000000..288360c --- /dev/null +++ b/crates/storage/src/backend/mod.rs @@ -0,0 +1,5 @@ +#![allow(dead_code, unused_imports)] // Infrastructure not yet integrated with Store + +mod in_memory; + +pub use in_memory::InMemoryBackend; diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index f2b83fa..fe5a7ac 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -1,3 +1,6 @@ +mod api; +mod backend; + use std::collections::HashMap; use ethlambda_types::{ From 98f2be5adc736fbe090c93a236b59fe7a5dcfd6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:47:01 -0300 Subject: [PATCH 07/15] fix: track deletes and puts on same key correctly --- crates/storage/src/backend/in_memory.rs | 99 ++++++++++++++++++++----- 1 file changed, 79 insertions(+), 20 deletions(-) diff --git a/crates/storage/src/backend/in_memory.rs b/crates/storage/src/backend/in_memory.rs index 8bd1863..adc5799 100644 --- a/crates/storage/src/backend/in_memory.rs +++ b/crates/storage/src/backend/in_memory.rs @@ -7,8 +7,14 @@ use crate::api::{Error, PrefixResult, StorageBackend, StorageReadView, StorageWr type TableData = HashMap, Vec>; type StorageData = HashMap; -type PendingEntries = HashMap, Vec)>>; -type PendingDeletes = HashMap>>; + +/// Pending operation for a key - last operation wins. +enum PendingOp { + Put(Vec), + Delete, +} + +type PendingOps = HashMap, PendingOp>>; /// In-memory storage backend using HashMaps. #[derive(Clone, Default)] @@ -32,8 +38,7 @@ impl StorageBackend for InMemoryBackend { fn begin_write(&self) -> Result, Error> { Ok(Box::new(InMemoryWriteBatch { data: Arc::clone(&self.data), - pending: HashMap::new(), - deletes: HashMap::new(), + ops: HashMap::new(), })) } } @@ -72,37 +77,39 @@ impl StorageReadView for InMemoryReadView<'_> { /// Write batch that accumulates changes before committing. struct InMemoryWriteBatch { data: Arc>, - pending: PendingEntries, - deletes: PendingDeletes, + ops: PendingOps, } impl StorageWriteBatch for InMemoryWriteBatch { fn put_batch(&mut self, table: Table, batch: Vec<(Vec, Vec)>) -> Result<(), Error> { - self.pending.entry(table).or_default().extend(batch); + let table_ops = self.ops.entry(table).or_default(); + for (key, value) in batch { + table_ops.insert(key, PendingOp::Put(value)); + } Ok(()) } fn delete_batch(&mut self, table: Table, keys: Vec>) -> Result<(), Error> { - self.deletes.entry(table).or_default().extend(keys); + let table_ops = self.ops.entry(table).or_default(); + for key in keys { + table_ops.insert(key, PendingOp::Delete); + } Ok(()) } fn commit(self: Box) -> Result<(), Error> { let mut guard = self.data.write().map_err(|e| e.to_string())?; - // Apply puts - for (table, entries) in self.pending { + for (table, ops) in self.ops { let table_data = guard.entry(table).or_default(); - for (key, value) in entries { - table_data.insert(key, value); - } - } - - // Apply deletes - for (table, keys) in self.deletes { - if let Some(table_data) = guard.get_mut(&table) { - for key in keys { - table_data.remove(&key); + for (key, op) in ops { + match op { + PendingOp::Put(value) => { + table_data.insert(key, value); + } + PendingOp::Delete => { + table_data.remove(&key); + } } } } @@ -210,6 +217,58 @@ mod tests { assert_eq!(value, None); } + #[test] + fn test_delete_then_put() { + let backend = InMemoryBackend::new(); + + // Initial value + { + let mut batch = backend.begin_write().unwrap(); + batch + .put_batch(Table::Blocks, vec![(b"key".to_vec(), b"old".to_vec())]) + .unwrap(); + batch.commit().unwrap(); + } + + // Delete then put in same batch - put should win + { + let mut batch = backend.begin_write().unwrap(); + batch + .delete_batch(Table::Blocks, vec![b"key".to_vec()]) + .unwrap(); + batch + .put_batch(Table::Blocks, vec![(b"key".to_vec(), b"new".to_vec())]) + .unwrap(); + batch.commit().unwrap(); + } + + let view = backend.begin_read().unwrap(); + assert_eq!( + view.get(Table::Blocks, b"key").unwrap(), + Some(b"new".to_vec()) + ); + } + + #[test] + fn test_put_then_delete() { + let backend = InMemoryBackend::new(); + + // Put then delete in same batch - delete should win + { + let mut batch = backend.begin_write().unwrap(); + batch + .put_batch(Table::Blocks, vec![(b"key".to_vec(), b"value".to_vec())]) + .unwrap(); + batch + .delete_batch(Table::Blocks, vec![b"key".to_vec()]) + .unwrap(); + batch.commit().unwrap(); + } + + let view = backend.begin_read().unwrap(); + assert_eq!(view.get(Table::Blocks, b"key").unwrap(), None); + } + #[test] fn test_multiple_tables() { let backend = InMemoryBackend::new(); From 3e8f01ef1a9515102ab575e7b9c71ed620a39aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:06:16 -0300 Subject: [PATCH 08/15] refactor: initialize all tables as empty --- crates/storage/src/backend/in_memory.rs | 47 +++++++++++++++++-------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/crates/storage/src/backend/in_memory.rs b/crates/storage/src/backend/in_memory.rs index adc5799..9aca763 100644 --- a/crates/storage/src/backend/in_memory.rs +++ b/crates/storage/src/backend/in_memory.rs @@ -3,7 +3,9 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; -use crate::api::{Error, PrefixResult, StorageBackend, StorageReadView, StorageWriteBatch, Table}; +use crate::api::{ + Error, PrefixResult, StorageBackend, StorageReadView, StorageWriteBatch, Table, ALL_TABLES, +}; type TableData = HashMap, Vec>; type StorageData = HashMap; @@ -17,13 +19,27 @@ enum PendingOp { type PendingOps = HashMap, PendingOp>>; /// In-memory storage backend using HashMaps. -#[derive(Clone, Default)] +/// +/// All tables are created (empty) on initialization. +#[derive(Clone)] pub struct InMemoryBackend { data: Arc>, } +impl Default for InMemoryBackend { + fn default() -> Self { + let mut data = StorageData::new(); + for table in ALL_TABLES { + data.insert(table, TableData::new()); + } + Self { + data: Arc::new(RwLock::new(data)), + } + } +} + impl InMemoryBackend { - /// Create a new empty in-memory backend. + /// Create a new in-memory backend with all tables initialized empty. pub fn new() -> Self { Self::default() } @@ -50,7 +66,12 @@ struct InMemoryReadView<'a> { impl StorageReadView for InMemoryReadView<'_> { fn get(&self, table: Table, key: &[u8]) -> Result>, Error> { - Ok(self.guard.get(&table).and_then(|t| t.get(key)).cloned()) + Ok(self + .guard + .get(&table) + .expect("table exists") + .get(key) + .cloned()) } fn prefix_iterator( @@ -58,19 +79,15 @@ impl StorageReadView for InMemoryReadView<'_> { table: Table, prefix: &[u8], ) -> Result + '_>, Error> { - let table_data = self.guard.get(&table); + let table_data = self.guard.get(&table).expect("table exists"); let prefix_owned = prefix.to_vec(); - let iter: Box + '_> = match table_data { - Some(data) => Box::new( - data.iter() - .filter(move |(k, _)| k.starts_with(&prefix_owned)) - .map(|(k, v)| Ok((k.clone().into_boxed_slice(), v.clone().into_boxed_slice()))), - ), - None => Box::new(std::iter::empty()), - }; + let iter = table_data + .iter() + .filter(move |(k, _)| k.starts_with(&prefix_owned)) + .map(|(k, v)| Ok((k.clone().into_boxed_slice(), v.clone().into_boxed_slice()))); - Ok(iter) + Ok(Box::new(iter)) } } @@ -101,7 +118,7 @@ impl StorageWriteBatch for InMemoryWriteBatch { let mut guard = self.data.write().map_err(|e| e.to_string())?; for (table, ops) in self.ops { - let table_data = guard.entry(table).or_default(); + let table_data = guard.get_mut(&table).expect("table exists"); for (key, op) in ops { match op { PendingOp::Put(value) => { From f5f73a93ee2574114f244e0b60e9b26c95bf4774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:06:25 -0300 Subject: [PATCH 09/15] refactor: connect Store to InMemoryBackend --- crates/blockchain/src/store.rs | 34 +- .../blockchain/tests/forkchoice_spectests.rs | 25 +- crates/storage/src/api/mod.rs | 4 +- crates/storage/src/backend/in_memory.rs | 4 +- crates/storage/src/backend/mod.rs | 2 - crates/storage/src/lib.rs | 413 +++++++++++++----- 6 files changed, 339 insertions(+), 143 deletions(-) diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index e092179..5cbd880 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -29,10 +29,12 @@ fn accept_new_attestations(store: &mut Store) { /// Update the head based on the fork choice rule. fn update_head(store: &mut Store) { + let blocks: HashMap = store.iter_blocks().collect(); + let attestations: HashMap = store.iter_known_attestations().collect(); let head = ethlambda_fork_choice::compute_lmd_ghost_head( store.latest_justified().root, - store.blocks(), - store.latest_known_attestations(), + &blocks, + &attestations, 0, ); store.update_checkpoints(ForkCheckpoints::head_only(head)); @@ -45,10 +47,12 @@ fn update_safe_target(store: &mut Store) { let min_target_score = (num_validators * 2).div_ceil(3); + let blocks: HashMap = store.iter_blocks().collect(); + let attestations: HashMap = store.iter_new_attestations().collect(); let safe_target = ethlambda_fork_choice::compute_lmd_ghost_head( store.latest_justified().root, - store.blocks(), - store.latest_new_attestations(), + &blocks, + &attestations, min_target_score, ); store.set_safe_target(safe_target); @@ -299,7 +303,7 @@ pub fn on_block( // TODO: extract signature verification to a pre-checks function // to avoid the need for this if cfg!(not(feature = "skip-signature-verification")) { - verify_signatures(parent_state, &signed_block)?; + verify_signatures(&parent_state, &signed_block)?; } // Execute state transition function to compute post-block state @@ -502,16 +506,18 @@ pub fn produce_block_with_signatures( // Convert AttestationData to Attestation objects for build_block let available_attestations: Vec = store - .latest_known_attestations() - .iter() - .map(|(&validator_id, data)| Attestation { - validator_id, - data: data.clone(), - }) + .iter_known_attestations() + .map(|(validator_id, data)| Attestation { validator_id, data }) .collect(); // Get known block roots for attestation validation - let known_block_roots: HashSet = store.blocks().keys().copied().collect(); + let known_block_roots: HashSet = store.iter_blocks().map(|(root, _)| root).collect(); + + // Collect signature data for block building + let gossip_signatures: HashMap = + store.iter_gossip_signatures().collect(); + let aggregated_payloads: HashMap> = + store.iter_aggregated_payloads().collect(); // Build the block using fixed-point attestation collection let (block, _post_state, signatures) = build_block( @@ -521,8 +527,8 @@ pub fn produce_block_with_signatures( head_root, &available_attestations, &known_block_roots, - store.gossip_signatures(), - store.aggregated_payloads(), + &gossip_signatures, + &aggregated_payloads, )?; Ok((block, signatures)) diff --git a/crates/blockchain/tests/forkchoice_spectests.rs b/crates/blockchain/tests/forkchoice_spectests.rs index 636260d..a0ee5d8 100644 --- a/crates/blockchain/tests/forkchoice_spectests.rs +++ b/crates/blockchain/tests/forkchoice_spectests.rs @@ -163,14 +163,13 @@ fn validate_checks( } // Also validate the root matches a block at this slot - let block_found = st - .blocks() + let blocks: HashMap = st.iter_blocks().collect(); + let block_found = blocks .iter() .any(|(root, block)| block.slot == expected_slot && *root == target.root); if !block_found { - let available: Vec<_> = st - .blocks() + let available: Vec<_> = blocks .iter() .filter(|(_, block)| block.slot == expected_slot) .map(|(root, _)| format!("{:?}", root)) @@ -187,8 +186,7 @@ fn validate_checks( if let Some(expected_slot) = checks.head_slot { let head_root = st.head(); let head_block = st - .blocks() - .get(&head_root) + .get_block(&head_root) .ok_or_else(|| format!("Step {}: head block not found", step_idx))?; if head_block.slot != expected_slot { return Err(format!( @@ -279,12 +277,14 @@ fn validate_attestation_check( check: &types::AttestationCheck, step_idx: usize, ) -> datatest_stable::Result<()> { + use ethlambda_types::attestation::AttestationData; + let validator_id = check.validator; let location = check.location.as_str(); - let attestations = match location { - "new" => st.latest_new_attestations(), - "known" => st.latest_known_attestations(), + let attestations: HashMap = match location { + "new" => st.iter_new_attestations().collect(), + "known" => st.iter_known_attestations().collect(), other => { return Err( format!("Step {}: unknown attestation location: {}", step_idx, other).into(), @@ -351,6 +351,8 @@ fn validate_lexicographic_head_among( step_idx: usize, block_registry: &HashMap, ) -> datatest_stable::Result<()> { + use ethlambda_types::attestation::AttestationData; + // Require at least 2 forks to test tiebreaker if fork_labels.len() < 2 { return Err(format!( @@ -361,7 +363,8 @@ fn validate_lexicographic_head_among( .into()); } - let blocks = st.blocks(); + let blocks: HashMap = st.iter_blocks().collect(); + let known_attestations: HashMap = st.iter_known_attestations().collect(); // Resolve all fork labels to roots and compute their weights // Map: label -> (root, slot, weight) @@ -386,7 +389,7 @@ fn validate_lexicographic_head_among( // Calculate attestation weight: count attestations voting for this fork // An attestation votes for this fork if its head is this block or a descendant let mut weight = 0; - for attestation in st.latest_known_attestations().values() { + for attestation in known_attestations.values() { let att_head_root = attestation.head.root; // Check if attestation head is this block or a descendant if att_head_root == *root { diff --git a/crates/storage/src/api/mod.rs b/crates/storage/src/api/mod.rs index f005aa8..f93e9d1 100644 --- a/crates/storage/src/api/mod.rs +++ b/crates/storage/src/api/mod.rs @@ -1,7 +1,5 @@ -#![allow(dead_code, unused_imports)] // Infrastructure not yet integrated with Store - mod tables; mod traits; -pub use tables::{Table, ALL_TABLES}; +pub use tables::{ALL_TABLES, Table}; pub use traits::{Error, PrefixResult, StorageBackend, StorageReadView, StorageWriteBatch}; diff --git a/crates/storage/src/backend/in_memory.rs b/crates/storage/src/backend/in_memory.rs index 9aca763..914cc21 100644 --- a/crates/storage/src/backend/in_memory.rs +++ b/crates/storage/src/backend/in_memory.rs @@ -1,10 +1,8 @@ -#![allow(dead_code)] // Infrastructure not yet integrated with Store - use std::collections::HashMap; use std::sync::{Arc, RwLock}; use crate::api::{ - Error, PrefixResult, StorageBackend, StorageReadView, StorageWriteBatch, Table, ALL_TABLES, + ALL_TABLES, Error, PrefixResult, StorageBackend, StorageReadView, StorageWriteBatch, Table, }; type TableData = HashMap, Vec>; diff --git a/crates/storage/src/backend/mod.rs b/crates/storage/src/backend/mod.rs index 288360c..f146446 100644 --- a/crates/storage/src/backend/mod.rs +++ b/crates/storage/src/backend/mod.rs @@ -1,5 +1,3 @@ -#![allow(dead_code, unused_imports)] // Infrastructure not yet integrated with Store - mod in_memory; pub use in_memory::InMemoryBackend; diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index fe5a7ac..ef45973 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -1,12 +1,13 @@ mod api; mod backend; -use std::collections::HashMap; +use api::{StorageBackend, Table}; +use backend::InMemoryBackend; use ethlambda_types::{ attestation::AttestationData, block::{AggregatedSignatureProof, Block, BlockBody}, - primitives::{H256, TreeHash}, + primitives::{Decode, Encode, H256, TreeHash}, signature::ValidatorSignature, state::{ChainConfig, Checkpoint, State}, }; @@ -50,6 +51,23 @@ impl ForkCheckpoints { } } +// ============ Key Encoding Helpers ============ + +/// Encode a SignatureKey (validator_id, root) to bytes. +/// Layout: validator_id (8 bytes SSZ) || root (32 bytes SSZ) +fn encode_signature_key(key: &SignatureKey) -> Vec { + let mut result = key.0.as_ssz_bytes(); + result.extend(key.1.as_ssz_bytes()); + result +} + +/// Decode a SignatureKey from bytes. +fn decode_signature_key(bytes: &[u8]) -> SignatureKey { + let validator_id = u64::from_ssz_bytes(&bytes[..8]).expect("valid validator_id"); + let root = H256::from_ssz_bytes(&bytes[8..]).expect("valid root"); + (validator_id, root) +} + /// Forkchoice store tracking chain state and validator attestations. /// /// This is the "local view" that a node uses to run LMD GHOST. It contains: @@ -98,46 +116,8 @@ pub struct Store { /// Fork choice will never revert finalized history. latest_finalized: Checkpoint, - /// Mapping from block root to Block objects. - /// - /// This is the set of blocks that the node currently knows about. - /// - /// Every block that might participate in fork choice must appear here. - blocks: HashMap, - - /// Mapping from block root to State objects. - /// - /// For each known block, we keep its post-state. - /// - /// These states carry justified and finalized checkpoints that we use to update the - /// `Store`'s latest justified and latest finalized checkpoints. - states: HashMap, - - /// Latest signed attestations by validator that have been processed. - /// - /// - These attestations are "known" and contribute to fork choice weights. - /// - Keyed by validator index to enforce one attestation per validator. - latest_known_attestations: HashMap, - - /// Latest signed attestations by validator that are pending processing. - /// - /// - These attestations are "new" and do not yet contribute to fork choice. - /// - They migrate to `latest_known_attestations` via interval ticks. - /// - Keyed by validator index to enforce one attestation per validator. - latest_new_attestations: HashMap, - - /// Per-validator XMSS signatures learned from gossip. - /// - /// Keyed by SignatureKey(validator_id, attestation_data_root). - gossip_signatures: HashMap, - - /// Aggregated signature proofs learned from blocks. - /// - Keyed by SignatureKey(validator_id, attestation_data_root). - /// - Values are lists of AggregatedSignatureProof, each containing the participants - /// bitfield indicating which validators signed. - /// - Used for recursive signature aggregation when building blocks. - /// - Populated by on_block. - aggregated_payloads: HashMap>, + /// Storage backend for blocks, states, attestations, and signatures. + backend: InMemoryBackend, } impl Store { @@ -162,11 +142,31 @@ impl Store { let anchor_state_root = anchor_state.tree_hash_root(); let anchor_block_root = anchor_block.tree_hash_root(); - let mut blocks = HashMap::new(); - blocks.insert(anchor_block_root, anchor_block); - - let mut states = HashMap::new(); - states.insert(anchor_block_root, anchor_state.clone()); + let backend = InMemoryBackend::new(); + + // Insert initial block and state + { + let mut batch = backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::Blocks, + vec![( + anchor_block_root.as_ssz_bytes(), + anchor_block.as_ssz_bytes(), + )], + ) + .expect("put block"); + batch + .put_batch( + Table::States, + vec![( + anchor_block_root.as_ssz_bytes(), + anchor_state.as_ssz_bytes(), + )], + ) + .expect("put state"); + batch.commit().expect("commit"); + } let anchor_checkpoint = Checkpoint { root: anchor_block_root, @@ -182,12 +182,7 @@ impl Store { safe_target: anchor_block_root, latest_justified: anchor_checkpoint, latest_finalized: anchor_checkpoint, - blocks, - states, - latest_known_attestations: HashMap::new(), - latest_new_attestations: HashMap::new(), - gossip_signatures: HashMap::new(), - aggregated_payloads: HashMap::new(), + backend, } } @@ -200,9 +195,35 @@ impl Store { safe_target: H256, latest_justified: Checkpoint, latest_finalized: Checkpoint, - blocks: HashMap, - states: HashMap, + blocks: impl IntoIterator, + states: impl IntoIterator, ) -> Self { + let backend = InMemoryBackend::new(); + + // Insert blocks and states + { + let mut batch = backend.begin_write().expect("write batch"); + let block_entries: Vec<_> = blocks + .into_iter() + .map(|(k, v)| (k.as_ssz_bytes(), v.as_ssz_bytes())) + .collect(); + if !block_entries.is_empty() { + batch + .put_batch(Table::Blocks, block_entries) + .expect("put blocks"); + } + let state_entries: Vec<_> = states + .into_iter() + .map(|(k, v)| (k.as_ssz_bytes(), v.as_ssz_bytes())) + .collect(); + if !state_entries.is_empty() { + batch + .put_batch(Table::States, state_entries) + .expect("put states"); + } + batch.commit().expect("commit"); + } + Self { time, config, @@ -210,12 +231,7 @@ impl Store { safe_target, latest_justified, latest_finalized, - blocks, - states, - latest_known_attestations: HashMap::new(), - latest_new_attestations: HashMap::new(), - gossip_signatures: HashMap::new(), - aggregated_payloads: HashMap::new(), + backend, } } @@ -284,66 +300,164 @@ impl Store { // ============ Blocks ============ - pub fn blocks(&self) -> &HashMap { - &self.blocks - } - - pub fn get_block(&self, root: &H256) -> Option<&Block> { - self.blocks.get(root) + /// Iterate over all (root, block) pairs. + pub fn iter_blocks(&self) -> impl Iterator + '_ { + let view = self.backend.begin_read().expect("read view"); + let entries: Vec<_> = view + .prefix_iterator(Table::Blocks, &[]) + .expect("iterator") + .filter_map(|res| res.ok()) + .map(|(k, v)| { + let root = H256::from_ssz_bytes(&k).expect("valid root"); + let block = Block::from_ssz_bytes(&v).expect("valid block"); + (root, block) + }) + .collect(); + entries.into_iter() + } + + 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()) + .expect("get") + .map(|bytes| Block::from_ssz_bytes(&bytes).expect("valid block")) } pub fn contains_block(&self, root: &H256) -> bool { - self.blocks.contains_key(root) + let view = self.backend.begin_read().expect("read view"); + view.get(Table::Blocks, &root.as_ssz_bytes()) + .expect("get") + .is_some() } pub fn insert_block(&mut self, root: H256, block: Block) { - self.blocks.insert(root, block); + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::Blocks, + vec![(root.as_ssz_bytes(), block.as_ssz_bytes())], + ) + .expect("put block"); + batch.commit().expect("commit"); } // ============ States ============ - pub fn states(&self) -> &HashMap { - &self.states - } - - pub fn get_state(&self, root: &H256) -> Option<&State> { - self.states.get(root) + /// Iterate over all (root, state) pairs. + pub fn iter_states(&self) -> impl Iterator + '_ { + 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() + } + + 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()) + .expect("get") + .map(|bytes| State::from_ssz_bytes(&bytes).expect("valid state")) } pub fn insert_state(&mut self, root: H256, state: State) { - self.states.insert(root, state); + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::States, + vec![(root.as_ssz_bytes(), state.as_ssz_bytes())], + ) + .expect("put state"); + batch.commit().expect("commit"); } // ============ Latest Known Attestations ============ - pub fn latest_known_attestations(&self) -> &HashMap { - &self.latest_known_attestations - } - - pub fn get_known_attestation(&self, validator_id: &u64) -> Option<&AttestationData> { - self.latest_known_attestations.get(validator_id) + /// Iterate over all (validator_id, attestation_data) pairs for known attestations. + pub fn iter_known_attestations(&self) -> impl Iterator + '_ { + let view = self.backend.begin_read().expect("read view"); + let entries: Vec<_> = view + .prefix_iterator(Table::LatestKnownAttestations, &[]) + .expect("iterator") + .filter_map(|res| res.ok()) + .map(|(k, v)| { + let validator_id = u64::from_ssz_bytes(&k).expect("valid validator_id"); + let data = AttestationData::from_ssz_bytes(&v).expect("valid attestation data"); + (validator_id, data) + }) + .collect(); + entries.into_iter() + } + + 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()) + .expect("get") + .map(|bytes| AttestationData::from_ssz_bytes(&bytes).expect("valid attestation data")) } pub fn insert_known_attestation(&mut self, validator_id: u64, data: AttestationData) { - self.latest_known_attestations.insert(validator_id, data); + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::LatestKnownAttestations, + vec![(validator_id.as_ssz_bytes(), data.as_ssz_bytes())], + ) + .expect("put attestation"); + batch.commit().expect("commit"); } // ============ Latest New Attestations ============ - pub fn latest_new_attestations(&self) -> &HashMap { - &self.latest_new_attestations - } - - pub fn get_new_attestation(&self, validator_id: &u64) -> Option<&AttestationData> { - self.latest_new_attestations.get(validator_id) + /// Iterate over all (validator_id, attestation_data) pairs for new attestations. + pub fn iter_new_attestations(&self) -> impl Iterator + '_ { + let view = self.backend.begin_read().expect("read view"); + let entries: Vec<_> = view + .prefix_iterator(Table::LatestNewAttestations, &[]) + .expect("iterator") + .filter_map(|res| res.ok()) + .map(|(k, v)| { + let validator_id = u64::from_ssz_bytes(&k).expect("valid validator_id"); + let data = AttestationData::from_ssz_bytes(&v).expect("valid attestation data"); + (validator_id, data) + }) + .collect(); + entries.into_iter() + } + + 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()) + .expect("get") + .map(|bytes| AttestationData::from_ssz_bytes(&bytes).expect("valid attestation data")) } pub fn insert_new_attestation(&mut self, validator_id: u64, data: AttestationData) { - self.latest_new_attestations.insert(validator_id, data); + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::LatestNewAttestations, + vec![(validator_id.as_ssz_bytes(), data.as_ssz_bytes())], + ) + .expect("put attestation"); + batch.commit().expect("commit"); } pub fn remove_new_attestation(&mut self, validator_id: &u64) { - self.latest_new_attestations.remove(validator_id); + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .delete_batch( + Table::LatestNewAttestations, + vec![validator_id.as_ssz_bytes()], + ) + .expect("delete attestation"); + batch.commit().expect("commit"); } /// Promotes all new attestations to known attestations. @@ -351,45 +465,124 @@ impl Store { /// Takes all attestations from `latest_new_attestations` and moves them /// to `latest_known_attestations`, making them count for fork choice. pub fn promote_new_attestations(&mut self) { - let mut new_attestations = std::mem::take(&mut self.latest_new_attestations); - self.latest_known_attestations - .extend(new_attestations.drain()); - self.latest_new_attestations = new_attestations; + // Read all new attestations + let view = self.backend.begin_read().expect("read view"); + let new_attestations: Vec<(Vec, Vec)> = view + .prefix_iterator(Table::LatestNewAttestations, &[]) + .expect("iterator") + .filter_map(|res| res.ok()) + .map(|(k, v)| (k.to_vec(), v.to_vec())) + .collect(); + drop(view); + + if new_attestations.is_empty() { + return; + } + + // Delete from new and insert to known in a single batch + let mut batch = self.backend.begin_write().expect("write batch"); + let keys_to_delete: Vec<_> = new_attestations.iter().map(|(k, _)| k.clone()).collect(); + batch + .delete_batch(Table::LatestNewAttestations, keys_to_delete) + .expect("delete new attestations"); + batch + .put_batch(Table::LatestKnownAttestations, new_attestations) + .expect("put known attestations"); + batch.commit().expect("commit"); } // ============ Gossip Signatures ============ - pub fn gossip_signatures(&self) -> &HashMap { - &self.gossip_signatures - } - - pub fn get_gossip_signature(&self, key: &SignatureKey) -> Option<&ValidatorSignature> { - self.gossip_signatures.get(key) + /// Iterate over all (signature_key, signature) pairs. + pub fn iter_gossip_signatures( + &self, + ) -> impl Iterator + '_ { + let view = self.backend.begin_read().expect("read view"); + let entries: Vec<_> = view + .prefix_iterator(Table::GossipSignatures, &[]) + .expect("iterator") + .filter_map(|res| res.ok()) + .filter_map(|(k, v)| { + let key = decode_signature_key(&k); + ValidatorSignature::from_bytes(&v) + .ok() + .map(|sig| (key, sig)) + }) + .collect(); + 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| ValidatorSignature::from_bytes(&bytes).ok()) } pub fn contains_gossip_signature(&self, key: &SignatureKey) -> bool { - self.gossip_signatures.contains_key(key) + 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, key: SignatureKey, signature: ValidatorSignature) { - self.gossip_signatures.insert(key, signature); + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::GossipSignatures, + vec![(encode_signature_key(&key), signature.to_bytes())], + ) + .expect("put signature"); + batch.commit().expect("commit"); } // ============ Aggregated Payloads ============ - pub fn aggregated_payloads(&self) -> &HashMap> { - &self.aggregated_payloads + /// Iterate over all (signature_key, proofs) pairs. + pub fn iter_aggregated_payloads( + &self, + ) -> impl Iterator)> + '_ { + let view = self.backend.begin_read().expect("read view"); + let entries: Vec<_> = view + .prefix_iterator(Table::AggregatedPayloads, &[]) + .expect("iterator") + .filter_map(|res| res.ok()) + .map(|(k, v)| { + let key = decode_signature_key(&k); + let proofs = + Vec::::from_ssz_bytes(&v).expect("valid proofs"); + (key, proofs) + }) + .collect(); + entries.into_iter() } pub fn get_aggregated_payloads( &self, key: &SignatureKey, - ) -> Option<&Vec> { - self.aggregated_payloads.get(key) + ) -> Option> { + let view = self.backend.begin_read().expect("read view"); + view.get(Table::AggregatedPayloads, &encode_signature_key(key)) + .expect("get") + .map(|bytes| { + Vec::::from_ssz_bytes(&bytes).expect("valid proofs") + }) } pub fn push_aggregated_payload(&mut self, key: SignatureKey, proof: AggregatedSignatureProof) { - self.aggregated_payloads.entry(key).or_default().push(proof); + // Read existing, add new, write back + let mut proofs = self.get_aggregated_payloads(&key).unwrap_or_default(); + proofs.push(proof); + + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::AggregatedPayloads, + vec![(encode_signature_key(&key), proofs.as_ssz_bytes())], + ) + .expect("put proofs"); + batch.commit().expect("commit"); } // ============ Derived Accessors ============ @@ -401,8 +594,8 @@ impl Store { .slot } - /// Returns a reference to the head state. - pub fn head_state(&self) -> &State { + /// Returns a clone of the head state. + pub fn head_state(&self) -> State { self.get_state(&self.head) .expect("head state is always available") } From c9937a32d75e5bce362b128afc88ec12cedd8682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:17:06 -0300 Subject: [PATCH 10/15] refactor: move Store to other file --- crates/storage/src/lib.rs | 601 +----------------------------------- crates/storage/src/store.rs | 599 +++++++++++++++++++++++++++++++++++ 2 files changed, 601 insertions(+), 599 deletions(-) create mode 100644 crates/storage/src/store.rs diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index ef45973..5442564 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -1,602 +1,5 @@ mod api; mod backend; +mod store; -use api::{StorageBackend, Table}; -use backend::InMemoryBackend; - -use ethlambda_types::{ - attestation::AttestationData, - block::{AggregatedSignatureProof, Block, BlockBody}, - primitives::{Decode, Encode, H256, TreeHash}, - signature::ValidatorSignature, - state::{ChainConfig, Checkpoint, State}, -}; -use tracing::info; - -/// Key for looking up individual validator signatures. -/// Used to index signature caches by (validator, message) pairs. -/// -/// Values are (validator_index, attestation_data_root). -pub type SignatureKey = (u64, H256); - -/// Checkpoints to update in the forkchoice store. -/// -/// Used with `Store::update_checkpoints` to update head and optionally -/// update justified/finalized checkpoints (only if higher slot). -pub struct ForkCheckpoints { - head: H256, - justified: Option, - finalized: Option, -} - -impl ForkCheckpoints { - /// Create checkpoints update with only the head. - pub fn head_only(head: H256) -> Self { - Self { - head, - justified: None, - finalized: None, - } - } - - /// Create checkpoints update with optional justified and finalized. - /// - /// The head is passed through unchanged. - pub fn new(head: H256, justified: Option, finalized: Option) -> Self { - Self { - head, - justified, - finalized, - } - } -} - -// ============ Key Encoding Helpers ============ - -/// Encode a SignatureKey (validator_id, root) to bytes. -/// Layout: validator_id (8 bytes SSZ) || root (32 bytes SSZ) -fn encode_signature_key(key: &SignatureKey) -> Vec { - let mut result = key.0.as_ssz_bytes(); - result.extend(key.1.as_ssz_bytes()); - result -} - -/// Decode a SignatureKey from bytes. -fn decode_signature_key(bytes: &[u8]) -> SignatureKey { - let validator_id = u64::from_ssz_bytes(&bytes[..8]).expect("valid validator_id"); - let root = H256::from_ssz_bytes(&bytes[8..]).expect("valid root"); - (validator_id, root) -} - -/// Forkchoice store tracking chain state and validator attestations. -/// -/// This is the "local view" that a node uses to run LMD GHOST. It contains: -/// -/// - which blocks and states are known, -/// - which checkpoints are justified and finalized, -/// - which block is currently considered the head, -/// - and, for each validator, their latest attestation that should influence fork choice. -/// -/// The `Store` is updated whenever: -/// - a new block is processed, -/// - an attestation is received (via a block or gossip), -/// - an interval tick occurs (activating new attestations), -/// - or when the head is recomputed. -#[derive(Clone)] -pub struct Store { - /// Current time in intervals since genesis. - time: u64, - - /// Chain configuration parameters. - config: ChainConfig, - - /// Root of the current canonical chain head block. - /// - /// This is the result of running the fork choice algorithm on the current contents of the `Store`. - head: H256, - - /// Root of the current safe target for attestation. - /// - /// This can be used by higher-level logic to restrict which blocks are - /// considered safe to attest to, based on additional safety conditions. - /// - safe_target: H256, - - /// Highest slot justified checkpoint known to the store. - /// - /// LMD GHOST starts from this checkpoint when computing the head. - /// - /// Only descendants of this checkpoint are considered viable. - latest_justified: Checkpoint, - - /// Highest slot finalized checkpoint known to the store. - /// - /// Everything strictly before this checkpoint can be considered immutable. - /// - /// Fork choice will never revert finalized history. - latest_finalized: Checkpoint, - - /// Storage backend for blocks, states, attestations, and signatures. - backend: InMemoryBackend, -} - -impl Store { - /// Initialize a Store from a genesis state. - pub fn from_genesis(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(genesis_state, genesis_block) - } - - /// Initialize a Store from an anchor state and block. - pub fn get_forkchoice_store(anchor_state: State, anchor_block: Block) -> Self { - let anchor_state_root = anchor_state.tree_hash_root(); - let anchor_block_root = anchor_block.tree_hash_root(); - - let backend = InMemoryBackend::new(); - - // Insert initial block and state - { - let mut batch = backend.begin_write().expect("write batch"); - batch - .put_batch( - Table::Blocks, - vec![( - anchor_block_root.as_ssz_bytes(), - anchor_block.as_ssz_bytes(), - )], - ) - .expect("put block"); - batch - .put_batch( - Table::States, - vec![( - anchor_block_root.as_ssz_bytes(), - anchor_state.as_ssz_bytes(), - )], - ) - .expect("put state"); - batch.commit().expect("commit"); - } - - let anchor_checkpoint = Checkpoint { - root: anchor_block_root, - slot: 0, - }; - - info!(%anchor_state_root, %anchor_block_root, "Initialized store"); - - Self { - time: 0, - config: anchor_state.config.clone(), - head: anchor_block_root, - safe_target: anchor_block_root, - latest_justified: anchor_checkpoint, - latest_finalized: anchor_checkpoint, - backend, - } - } - - /// Creates a new Store with the given initial values. - #[expect(clippy::too_many_arguments)] - pub fn new( - time: u64, - config: ChainConfig, - head: H256, - safe_target: H256, - latest_justified: Checkpoint, - latest_finalized: Checkpoint, - blocks: impl IntoIterator, - states: impl IntoIterator, - ) -> Self { - let backend = InMemoryBackend::new(); - - // Insert blocks and states - { - let mut batch = backend.begin_write().expect("write batch"); - let block_entries: Vec<_> = blocks - .into_iter() - .map(|(k, v)| (k.as_ssz_bytes(), v.as_ssz_bytes())) - .collect(); - if !block_entries.is_empty() { - batch - .put_batch(Table::Blocks, block_entries) - .expect("put blocks"); - } - let state_entries: Vec<_> = states - .into_iter() - .map(|(k, v)| (k.as_ssz_bytes(), v.as_ssz_bytes())) - .collect(); - if !state_entries.is_empty() { - batch - .put_batch(Table::States, state_entries) - .expect("put states"); - } - batch.commit().expect("commit"); - } - - Self { - time, - config, - head, - safe_target, - latest_justified, - latest_finalized, - backend, - } - } - - // ============ Time ============ - - pub fn time(&self) -> u64 { - self.time - } - - pub fn set_time(&mut self, time: u64) { - self.time = time; - } - - // ============ Config ============ - - pub fn config(&self) -> &ChainConfig { - &self.config - } - - // ============ Head ============ - - pub fn head(&self) -> H256 { - self.head - } - - // ============ Safe Target ============ - - pub fn safe_target(&self) -> H256 { - self.safe_target - } - - pub fn set_safe_target(&mut self, safe_target: H256) { - self.safe_target = safe_target; - } - - // ============ Latest Justified ============ - - pub fn latest_justified(&self) -> &Checkpoint { - &self.latest_justified - } - - // ============ Latest Finalized ============ - - pub fn latest_finalized(&self) -> &Checkpoint { - &self.latest_finalized - } - - // ============ Checkpoint Updates ============ - - /// Updates head, justified, and finalized checkpoints. - /// - /// - Head is always updated to the new value. - /// - Justified is updated if provided. - /// - Finalized is updated if provided. - pub fn update_checkpoints(&mut self, checkpoints: ForkCheckpoints) { - self.head = checkpoints.head; - - if let Some(justified) = checkpoints.justified { - self.latest_justified = justified; - } - - if let Some(finalized) = checkpoints.finalized { - self.latest_finalized = finalized; - } - } - - // ============ Blocks ============ - - /// Iterate over all (root, block) pairs. - pub fn iter_blocks(&self) -> impl Iterator + '_ { - let view = self.backend.begin_read().expect("read view"); - let entries: Vec<_> = view - .prefix_iterator(Table::Blocks, &[]) - .expect("iterator") - .filter_map(|res| res.ok()) - .map(|(k, v)| { - let root = H256::from_ssz_bytes(&k).expect("valid root"); - let block = Block::from_ssz_bytes(&v).expect("valid block"); - (root, block) - }) - .collect(); - entries.into_iter() - } - - 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()) - .expect("get") - .map(|bytes| Block::from_ssz_bytes(&bytes).expect("valid block")) - } - - 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()) - .expect("get") - .is_some() - } - - pub fn insert_block(&mut self, root: H256, block: Block) { - let mut batch = self.backend.begin_write().expect("write batch"); - batch - .put_batch( - Table::Blocks, - vec![(root.as_ssz_bytes(), block.as_ssz_bytes())], - ) - .expect("put block"); - batch.commit().expect("commit"); - } - - // ============ States ============ - - /// Iterate over all (root, state) pairs. - pub fn iter_states(&self) -> impl Iterator + '_ { - 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() - } - - 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()) - .expect("get") - .map(|bytes| State::from_ssz_bytes(&bytes).expect("valid state")) - } - - pub fn insert_state(&mut self, root: H256, state: State) { - let mut batch = self.backend.begin_write().expect("write batch"); - batch - .put_batch( - Table::States, - vec![(root.as_ssz_bytes(), state.as_ssz_bytes())], - ) - .expect("put state"); - batch.commit().expect("commit"); - } - - // ============ Latest Known Attestations ============ - - /// Iterate over all (validator_id, attestation_data) pairs for known attestations. - pub fn iter_known_attestations(&self) -> impl Iterator + '_ { - let view = self.backend.begin_read().expect("read view"); - let entries: Vec<_> = view - .prefix_iterator(Table::LatestKnownAttestations, &[]) - .expect("iterator") - .filter_map(|res| res.ok()) - .map(|(k, v)| { - let validator_id = u64::from_ssz_bytes(&k).expect("valid validator_id"); - let data = AttestationData::from_ssz_bytes(&v).expect("valid attestation data"); - (validator_id, data) - }) - .collect(); - entries.into_iter() - } - - 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()) - .expect("get") - .map(|bytes| AttestationData::from_ssz_bytes(&bytes).expect("valid attestation data")) - } - - pub fn insert_known_attestation(&mut self, validator_id: u64, data: AttestationData) { - let mut batch = self.backend.begin_write().expect("write batch"); - batch - .put_batch( - Table::LatestKnownAttestations, - vec![(validator_id.as_ssz_bytes(), data.as_ssz_bytes())], - ) - .expect("put attestation"); - batch.commit().expect("commit"); - } - - // ============ Latest New Attestations ============ - - /// Iterate over all (validator_id, attestation_data) pairs for new attestations. - pub fn iter_new_attestations(&self) -> impl Iterator + '_ { - let view = self.backend.begin_read().expect("read view"); - let entries: Vec<_> = view - .prefix_iterator(Table::LatestNewAttestations, &[]) - .expect("iterator") - .filter_map(|res| res.ok()) - .map(|(k, v)| { - let validator_id = u64::from_ssz_bytes(&k).expect("valid validator_id"); - let data = AttestationData::from_ssz_bytes(&v).expect("valid attestation data"); - (validator_id, data) - }) - .collect(); - entries.into_iter() - } - - 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()) - .expect("get") - .map(|bytes| AttestationData::from_ssz_bytes(&bytes).expect("valid attestation data")) - } - - pub fn insert_new_attestation(&mut self, validator_id: u64, data: AttestationData) { - let mut batch = self.backend.begin_write().expect("write batch"); - batch - .put_batch( - Table::LatestNewAttestations, - vec![(validator_id.as_ssz_bytes(), data.as_ssz_bytes())], - ) - .expect("put attestation"); - batch.commit().expect("commit"); - } - - pub fn remove_new_attestation(&mut self, validator_id: &u64) { - let mut batch = self.backend.begin_write().expect("write batch"); - batch - .delete_batch( - Table::LatestNewAttestations, - vec![validator_id.as_ssz_bytes()], - ) - .expect("delete attestation"); - batch.commit().expect("commit"); - } - - /// Promotes all new attestations to known attestations. - /// - /// Takes all attestations from `latest_new_attestations` and moves them - /// to `latest_known_attestations`, making them count for fork choice. - pub fn promote_new_attestations(&mut self) { - // Read all new attestations - let view = self.backend.begin_read().expect("read view"); - let new_attestations: Vec<(Vec, Vec)> = view - .prefix_iterator(Table::LatestNewAttestations, &[]) - .expect("iterator") - .filter_map(|res| res.ok()) - .map(|(k, v)| (k.to_vec(), v.to_vec())) - .collect(); - drop(view); - - if new_attestations.is_empty() { - return; - } - - // Delete from new and insert to known in a single batch - let mut batch = self.backend.begin_write().expect("write batch"); - let keys_to_delete: Vec<_> = new_attestations.iter().map(|(k, _)| k.clone()).collect(); - batch - .delete_batch(Table::LatestNewAttestations, keys_to_delete) - .expect("delete new attestations"); - batch - .put_batch(Table::LatestKnownAttestations, new_attestations) - .expect("put known attestations"); - batch.commit().expect("commit"); - } - - // ============ Gossip Signatures ============ - - /// Iterate over all (signature_key, signature) pairs. - pub fn iter_gossip_signatures( - &self, - ) -> impl Iterator + '_ { - let view = self.backend.begin_read().expect("read view"); - let entries: Vec<_> = view - .prefix_iterator(Table::GossipSignatures, &[]) - .expect("iterator") - .filter_map(|res| res.ok()) - .filter_map(|(k, v)| { - let key = decode_signature_key(&k); - ValidatorSignature::from_bytes(&v) - .ok() - .map(|sig| (key, sig)) - }) - .collect(); - 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| ValidatorSignature::from_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, key: SignatureKey, signature: ValidatorSignature) { - let mut batch = self.backend.begin_write().expect("write batch"); - batch - .put_batch( - Table::GossipSignatures, - vec![(encode_signature_key(&key), signature.to_bytes())], - ) - .expect("put signature"); - batch.commit().expect("commit"); - } - - // ============ Aggregated Payloads ============ - - /// Iterate over all (signature_key, proofs) pairs. - pub fn iter_aggregated_payloads( - &self, - ) -> impl Iterator)> + '_ { - let view = self.backend.begin_read().expect("read view"); - let entries: Vec<_> = view - .prefix_iterator(Table::AggregatedPayloads, &[]) - .expect("iterator") - .filter_map(|res| res.ok()) - .map(|(k, v)| { - let key = decode_signature_key(&k); - let proofs = - Vec::::from_ssz_bytes(&v).expect("valid proofs"); - (key, proofs) - }) - .collect(); - entries.into_iter() - } - - pub 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") - .map(|bytes| { - Vec::::from_ssz_bytes(&bytes).expect("valid proofs") - }) - } - - pub fn push_aggregated_payload(&mut self, key: SignatureKey, proof: AggregatedSignatureProof) { - // Read existing, add new, write back - let mut proofs = self.get_aggregated_payloads(&key).unwrap_or_default(); - proofs.push(proof); - - let mut batch = self.backend.begin_write().expect("write batch"); - batch - .put_batch( - Table::AggregatedPayloads, - vec![(encode_signature_key(&key), proofs.as_ssz_bytes())], - ) - .expect("put proofs"); - batch.commit().expect("commit"); - } - - // ============ Derived Accessors ============ - - /// Returns the slot of the current safe target block. - pub fn safe_target_slot(&self) -> u64 { - self.get_block(&self.safe_target) - .expect("safe target exists") - .slot - } - - /// Returns a clone of the head state. - pub fn head_state(&self) -> State { - self.get_state(&self.head) - .expect("head state is always available") - } -} +pub use store::{ForkCheckpoints, SignatureKey, Store}; diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs new file mode 100644 index 0000000..ca1d3f7 --- /dev/null +++ b/crates/storage/src/store.rs @@ -0,0 +1,599 @@ +use crate::api::{StorageBackend, Table}; +use crate::backend::InMemoryBackend; + +use ethlambda_types::{ + attestation::AttestationData, + block::{AggregatedSignatureProof, Block, BlockBody}, + primitives::{Decode, Encode, H256, TreeHash}, + signature::ValidatorSignature, + state::{ChainConfig, Checkpoint, State}, +}; +use tracing::info; + +/// Key for looking up individual validator signatures. +/// Used to index signature caches by (validator, message) pairs. +/// +/// Values are (validator_index, attestation_data_root). +pub type SignatureKey = (u64, H256); + +/// Checkpoints to update in the forkchoice store. +/// +/// Used with `Store::update_checkpoints` to update head and optionally +/// update justified/finalized checkpoints (only if higher slot). +pub struct ForkCheckpoints { + head: H256, + justified: Option, + finalized: Option, +} + +impl ForkCheckpoints { + /// Create checkpoints update with only the head. + pub fn head_only(head: H256) -> Self { + Self { + head, + justified: None, + finalized: None, + } + } + + /// Create checkpoints update with optional justified and finalized. + /// + /// The head is passed through unchanged. + pub fn new(head: H256, justified: Option, finalized: Option) -> Self { + Self { + head, + justified, + finalized, + } + } +} + +// ============ Key Encoding Helpers ============ + +/// Encode a SignatureKey (validator_id, root) to bytes. +/// Layout: validator_id (8 bytes SSZ) || root (32 bytes SSZ) +fn encode_signature_key(key: &SignatureKey) -> Vec { + let mut result = key.0.as_ssz_bytes(); + result.extend(key.1.as_ssz_bytes()); + result +} + +/// Decode a SignatureKey from bytes. +fn decode_signature_key(bytes: &[u8]) -> SignatureKey { + let validator_id = u64::from_ssz_bytes(&bytes[..8]).expect("valid validator_id"); + let root = H256::from_ssz_bytes(&bytes[8..]).expect("valid root"); + (validator_id, root) +} + +/// Forkchoice store tracking chain state and validator attestations. +/// +/// This is the "local view" that a node uses to run LMD GHOST. It contains: +/// +/// - which blocks and states are known, +/// - which checkpoints are justified and finalized, +/// - which block is currently considered the head, +/// - and, for each validator, their latest attestation that should influence fork choice. +/// +/// The `Store` is updated whenever: +/// - a new block is processed, +/// - an attestation is received (via a block or gossip), +/// - an interval tick occurs (activating new attestations), +/// - or when the head is recomputed. +#[derive(Clone)] +pub struct Store { + /// Current time in intervals since genesis. + time: u64, + + /// Chain configuration parameters. + config: ChainConfig, + + /// Root of the current canonical chain head block. + /// + /// This is the result of running the fork choice algorithm on the current contents of the `Store`. + head: H256, + + /// Root of the current safe target for attestation. + /// + /// This can be used by higher-level logic to restrict which blocks are + /// considered safe to attest to, based on additional safety conditions. + /// + safe_target: H256, + + /// Highest slot justified checkpoint known to the store. + /// + /// LMD GHOST starts from this checkpoint when computing the head. + /// + /// Only descendants of this checkpoint are considered viable. + latest_justified: Checkpoint, + + /// Highest slot finalized checkpoint known to the store. + /// + /// Everything strictly before this checkpoint can be considered immutable. + /// + /// Fork choice will never revert finalized history. + latest_finalized: Checkpoint, + + /// Storage backend for blocks, states, attestations, and signatures. + backend: InMemoryBackend, +} + +impl Store { + /// Initialize a Store from a genesis state. + pub fn from_genesis(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(genesis_state, genesis_block) + } + + /// Initialize a Store from an anchor state and block. + pub fn get_forkchoice_store(anchor_state: State, anchor_block: Block) -> Self { + let anchor_state_root = anchor_state.tree_hash_root(); + let anchor_block_root = anchor_block.tree_hash_root(); + + let backend = InMemoryBackend::new(); + + // Insert initial block and state + { + let mut batch = backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::Blocks, + vec![( + anchor_block_root.as_ssz_bytes(), + anchor_block.as_ssz_bytes(), + )], + ) + .expect("put block"); + batch + .put_batch( + Table::States, + vec![( + anchor_block_root.as_ssz_bytes(), + anchor_state.as_ssz_bytes(), + )], + ) + .expect("put state"); + batch.commit().expect("commit"); + } + + let anchor_checkpoint = Checkpoint { + root: anchor_block_root, + slot: 0, + }; + + info!(%anchor_state_root, %anchor_block_root, "Initialized store"); + + Self { + time: 0, + config: anchor_state.config.clone(), + head: anchor_block_root, + safe_target: anchor_block_root, + latest_justified: anchor_checkpoint, + latest_finalized: anchor_checkpoint, + backend, + } + } + + /// Creates a new Store with the given initial values. + #[expect(clippy::too_many_arguments)] + pub fn new( + time: u64, + config: ChainConfig, + head: H256, + safe_target: H256, + latest_justified: Checkpoint, + latest_finalized: Checkpoint, + blocks: impl IntoIterator, + states: impl IntoIterator, + ) -> Self { + let backend = InMemoryBackend::new(); + + // Insert blocks and states + { + let mut batch = backend.begin_write().expect("write batch"); + let block_entries: Vec<_> = blocks + .into_iter() + .map(|(k, v)| (k.as_ssz_bytes(), v.as_ssz_bytes())) + .collect(); + if !block_entries.is_empty() { + batch + .put_batch(Table::Blocks, block_entries) + .expect("put blocks"); + } + let state_entries: Vec<_> = states + .into_iter() + .map(|(k, v)| (k.as_ssz_bytes(), v.as_ssz_bytes())) + .collect(); + if !state_entries.is_empty() { + batch + .put_batch(Table::States, state_entries) + .expect("put states"); + } + batch.commit().expect("commit"); + } + + Self { + time, + config, + head, + safe_target, + latest_justified, + latest_finalized, + backend, + } + } + + // ============ Time ============ + + pub fn time(&self) -> u64 { + self.time + } + + pub fn set_time(&mut self, time: u64) { + self.time = time; + } + + // ============ Config ============ + + pub fn config(&self) -> &ChainConfig { + &self.config + } + + // ============ Head ============ + + pub fn head(&self) -> H256 { + self.head + } + + // ============ Safe Target ============ + + pub fn safe_target(&self) -> H256 { + self.safe_target + } + + pub fn set_safe_target(&mut self, safe_target: H256) { + self.safe_target = safe_target; + } + + // ============ Latest Justified ============ + + pub fn latest_justified(&self) -> &Checkpoint { + &self.latest_justified + } + + // ============ Latest Finalized ============ + + pub fn latest_finalized(&self) -> &Checkpoint { + &self.latest_finalized + } + + // ============ Checkpoint Updates ============ + + /// Updates head, justified, and finalized checkpoints. + /// + /// - Head is always updated to the new value. + /// - Justified is updated if provided. + /// - Finalized is updated if provided. + pub fn update_checkpoints(&mut self, checkpoints: ForkCheckpoints) { + self.head = checkpoints.head; + + if let Some(justified) = checkpoints.justified { + self.latest_justified = justified; + } + + if let Some(finalized) = checkpoints.finalized { + self.latest_finalized = finalized; + } + } + + // ============ Blocks ============ + + /// Iterate over all (root, block) pairs. + pub fn iter_blocks(&self) -> impl Iterator + '_ { + let view = self.backend.begin_read().expect("read view"); + let entries: Vec<_> = view + .prefix_iterator(Table::Blocks, &[]) + .expect("iterator") + .filter_map(|res| res.ok()) + .map(|(k, v)| { + let root = H256::from_ssz_bytes(&k).expect("valid root"); + let block = Block::from_ssz_bytes(&v).expect("valid block"); + (root, block) + }) + .collect(); + entries.into_iter() + } + + 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()) + .expect("get") + .map(|bytes| Block::from_ssz_bytes(&bytes).expect("valid block")) + } + + 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()) + .expect("get") + .is_some() + } + + pub fn insert_block(&mut self, root: H256, block: Block) { + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::Blocks, + vec![(root.as_ssz_bytes(), block.as_ssz_bytes())], + ) + .expect("put block"); + batch.commit().expect("commit"); + } + + // ============ States ============ + + /// Iterate over all (root, state) pairs. + pub fn iter_states(&self) -> impl Iterator + '_ { + 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() + } + + 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()) + .expect("get") + .map(|bytes| State::from_ssz_bytes(&bytes).expect("valid state")) + } + + pub fn insert_state(&mut self, root: H256, state: State) { + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::States, + vec![(root.as_ssz_bytes(), state.as_ssz_bytes())], + ) + .expect("put state"); + batch.commit().expect("commit"); + } + + // ============ Latest Known Attestations ============ + + /// Iterate over all (validator_id, attestation_data) pairs for known attestations. + pub fn iter_known_attestations(&self) -> impl Iterator + '_ { + let view = self.backend.begin_read().expect("read view"); + let entries: Vec<_> = view + .prefix_iterator(Table::LatestKnownAttestations, &[]) + .expect("iterator") + .filter_map(|res| res.ok()) + .map(|(k, v)| { + let validator_id = u64::from_ssz_bytes(&k).expect("valid validator_id"); + let data = AttestationData::from_ssz_bytes(&v).expect("valid attestation data"); + (validator_id, data) + }) + .collect(); + entries.into_iter() + } + + 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()) + .expect("get") + .map(|bytes| AttestationData::from_ssz_bytes(&bytes).expect("valid attestation data")) + } + + pub fn insert_known_attestation(&mut self, validator_id: u64, data: AttestationData) { + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::LatestKnownAttestations, + vec![(validator_id.as_ssz_bytes(), data.as_ssz_bytes())], + ) + .expect("put attestation"); + batch.commit().expect("commit"); + } + + // ============ Latest New Attestations ============ + + /// Iterate over all (validator_id, attestation_data) pairs for new attestations. + pub fn iter_new_attestations(&self) -> impl Iterator + '_ { + let view = self.backend.begin_read().expect("read view"); + let entries: Vec<_> = view + .prefix_iterator(Table::LatestNewAttestations, &[]) + .expect("iterator") + .filter_map(|res| res.ok()) + .map(|(k, v)| { + let validator_id = u64::from_ssz_bytes(&k).expect("valid validator_id"); + let data = AttestationData::from_ssz_bytes(&v).expect("valid attestation data"); + (validator_id, data) + }) + .collect(); + entries.into_iter() + } + + 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()) + .expect("get") + .map(|bytes| AttestationData::from_ssz_bytes(&bytes).expect("valid attestation data")) + } + + pub fn insert_new_attestation(&mut self, validator_id: u64, data: AttestationData) { + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::LatestNewAttestations, + vec![(validator_id.as_ssz_bytes(), data.as_ssz_bytes())], + ) + .expect("put attestation"); + batch.commit().expect("commit"); + } + + pub fn remove_new_attestation(&mut self, validator_id: &u64) { + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .delete_batch( + Table::LatestNewAttestations, + vec![validator_id.as_ssz_bytes()], + ) + .expect("delete attestation"); + batch.commit().expect("commit"); + } + + /// Promotes all new attestations to known attestations. + /// + /// Takes all attestations from `latest_new_attestations` and moves them + /// to `latest_known_attestations`, making them count for fork choice. + pub fn promote_new_attestations(&mut self) { + // Read all new attestations + let view = self.backend.begin_read().expect("read view"); + let new_attestations: Vec<(Vec, Vec)> = view + .prefix_iterator(Table::LatestNewAttestations, &[]) + .expect("iterator") + .filter_map(|res| res.ok()) + .map(|(k, v)| (k.to_vec(), v.to_vec())) + .collect(); + drop(view); + + if new_attestations.is_empty() { + return; + } + + // Delete from new and insert to known in a single batch + let mut batch = self.backend.begin_write().expect("write batch"); + let keys_to_delete: Vec<_> = new_attestations.iter().map(|(k, _)| k.clone()).collect(); + batch + .delete_batch(Table::LatestNewAttestations, keys_to_delete) + .expect("delete new attestations"); + batch + .put_batch(Table::LatestKnownAttestations, new_attestations) + .expect("put known attestations"); + batch.commit().expect("commit"); + } + + // ============ Gossip Signatures ============ + + /// Iterate over all (signature_key, signature) pairs. + pub fn iter_gossip_signatures( + &self, + ) -> impl Iterator + '_ { + let view = self.backend.begin_read().expect("read view"); + let entries: Vec<_> = view + .prefix_iterator(Table::GossipSignatures, &[]) + .expect("iterator") + .filter_map(|res| res.ok()) + .filter_map(|(k, v)| { + let key = decode_signature_key(&k); + ValidatorSignature::from_bytes(&v) + .ok() + .map(|sig| (key, sig)) + }) + .collect(); + 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| ValidatorSignature::from_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, key: SignatureKey, signature: ValidatorSignature) { + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::GossipSignatures, + vec![(encode_signature_key(&key), signature.to_bytes())], + ) + .expect("put signature"); + batch.commit().expect("commit"); + } + + // ============ Aggregated Payloads ============ + + /// Iterate over all (signature_key, proofs) pairs. + pub fn iter_aggregated_payloads( + &self, + ) -> impl Iterator)> + '_ { + let view = self.backend.begin_read().expect("read view"); + let entries: Vec<_> = view + .prefix_iterator(Table::AggregatedPayloads, &[]) + .expect("iterator") + .filter_map(|res| res.ok()) + .map(|(k, v)| { + let key = decode_signature_key(&k); + let proofs = + Vec::::from_ssz_bytes(&v).expect("valid proofs"); + (key, proofs) + }) + .collect(); + entries.into_iter() + } + + pub 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") + .map(|bytes| { + Vec::::from_ssz_bytes(&bytes).expect("valid proofs") + }) + } + + pub fn push_aggregated_payload(&mut self, key: SignatureKey, proof: AggregatedSignatureProof) { + // Read existing, add new, write back + let mut proofs = self.get_aggregated_payloads(&key).unwrap_or_default(); + proofs.push(proof); + + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch( + Table::AggregatedPayloads, + vec![(encode_signature_key(&key), proofs.as_ssz_bytes())], + ) + .expect("put proofs"); + batch.commit().expect("commit"); + } + + // ============ Derived Accessors ============ + + /// Returns the slot of the current safe target block. + pub fn safe_target_slot(&self) -> u64 { + self.get_block(&self.safe_target) + .expect("safe target exists") + .slot + } + + /// Returns a clone of the head state. + pub fn head_state(&self) -> State { + self.get_state(&self.head) + .expect("head state is always available") + } +} From 520abd867993d3d6cfe6e3bbba5a9a01fe84155d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:24:27 -0300 Subject: [PATCH 11/15] refactor: move other fields to backend --- crates/blockchain/src/lib.rs | 2 +- crates/blockchain/src/store.rs | 2 +- crates/storage/src/store.rs | 185 ++++++++++++++++++++------------- 3 files changed, 113 insertions(+), 76 deletions(-) diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 8c44a0e..efd58ef 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -207,7 +207,7 @@ impl BlockChainServer { slot: block.slot, }, target: store::get_attestation_target(&self.store), - source: *self.store.latest_justified(), + source: self.store.latest_justified(), }, }; diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 5cbd880..5d93d6e 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -455,7 +455,7 @@ pub fn produce_attestation_data(store: &Store, slot: u64) -> AttestationData { slot, head: head_checkpoint, target: target_checkpoint, - source: *store.latest_justified(), + source: store.latest_justified(), } } diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index ca1d3f7..0d86dfb 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -48,6 +48,15 @@ impl ForkCheckpoints { } } +// ============ Metadata Keys ============ + +const KEY_TIME: &[u8] = b"time"; +const KEY_CONFIG: &[u8] = b"config"; +const KEY_HEAD: &[u8] = b"head"; +const KEY_SAFE_TARGET: &[u8] = b"safe_target"; +const KEY_LATEST_JUSTIFIED: &[u8] = b"latest_justified"; +const KEY_LATEST_FINALIZED: &[u8] = b"latest_finalized"; + // ============ Key Encoding Helpers ============ /// Encode a SignatureKey (validator_id, root) to bytes. @@ -79,41 +88,12 @@ fn decode_signature_key(bytes: &[u8]) -> SignatureKey { /// - an attestation is received (via a block or gossip), /// - an interval tick occurs (activating new attestations), /// - or when the head is recomputed. +/// +/// 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. #[derive(Clone)] pub struct Store { - /// Current time in intervals since genesis. - time: u64, - - /// Chain configuration parameters. - config: ChainConfig, - - /// Root of the current canonical chain head block. - /// - /// This is the result of running the fork choice algorithm on the current contents of the `Store`. - head: H256, - - /// Root of the current safe target for attestation. - /// - /// This can be used by higher-level logic to restrict which blocks are - /// considered safe to attest to, based on additional safety conditions. - /// - safe_target: H256, - - /// Highest slot justified checkpoint known to the store. - /// - /// LMD GHOST starts from this checkpoint when computing the head. - /// - /// Only descendants of this checkpoint are considered viable. - latest_justified: Checkpoint, - - /// Highest slot finalized checkpoint known to the store. - /// - /// Everything strictly before this checkpoint can be considered immutable. - /// - /// Fork choice will never revert finalized history. - latest_finalized: Checkpoint, - - /// Storage backend for blocks, states, attestations, and signatures. + /// Storage backend for all store data. backend: InMemoryBackend, } @@ -141,9 +121,37 @@ impl Store { let backend = InMemoryBackend::new(); - // Insert initial block and state + let anchor_checkpoint = Checkpoint { + root: anchor_block_root, + slot: 0, + }; + + // Insert initial data { let mut batch = backend.begin_write().expect("write batch"); + + // Metadata + batch + .put_batch( + Table::Metadata, + vec![ + (KEY_TIME.to_vec(), 0u64.as_ssz_bytes()), + (KEY_CONFIG.to_vec(), anchor_state.config.as_ssz_bytes()), + (KEY_HEAD.to_vec(), anchor_block_root.as_ssz_bytes()), + (KEY_SAFE_TARGET.to_vec(), anchor_block_root.as_ssz_bytes()), + ( + KEY_LATEST_JUSTIFIED.to_vec(), + anchor_checkpoint.as_ssz_bytes(), + ), + ( + KEY_LATEST_FINALIZED.to_vec(), + anchor_checkpoint.as_ssz_bytes(), + ), + ], + ) + .expect("put metadata"); + + // Block and state batch .put_batch( Table::Blocks, @@ -162,25 +170,13 @@ impl Store { )], ) .expect("put state"); + batch.commit().expect("commit"); } - let anchor_checkpoint = Checkpoint { - root: anchor_block_root, - slot: 0, - }; - info!(%anchor_state_root, %anchor_block_root, "Initialized store"); - Self { - time: 0, - config: anchor_state.config.clone(), - head: anchor_block_root, - safe_target: anchor_block_root, - latest_justified: anchor_checkpoint, - latest_finalized: anchor_checkpoint, - backend, - } + Self { backend } } /// Creates a new Store with the given initial values. @@ -197,9 +193,32 @@ impl Store { ) -> Self { let backend = InMemoryBackend::new(); - // Insert blocks and states + // Insert all data { let mut batch = backend.begin_write().expect("write batch"); + + // Metadata + batch + .put_batch( + Table::Metadata, + vec![ + (KEY_TIME.to_vec(), time.as_ssz_bytes()), + (KEY_CONFIG.to_vec(), config.as_ssz_bytes()), + (KEY_HEAD.to_vec(), head.as_ssz_bytes()), + (KEY_SAFE_TARGET.to_vec(), safe_target.as_ssz_bytes()), + ( + KEY_LATEST_JUSTIFIED.to_vec(), + latest_justified.as_ssz_bytes(), + ), + ( + KEY_LATEST_FINALIZED.to_vec(), + latest_finalized.as_ssz_bytes(), + ), + ], + ) + .expect("put metadata"); + + // Blocks let block_entries: Vec<_> = blocks .into_iter() .map(|(k, v)| (k.as_ssz_bytes(), v.as_ssz_bytes())) @@ -209,6 +228,8 @@ impl Store { .put_batch(Table::Blocks, block_entries) .expect("put blocks"); } + + // States let state_entries: Vec<_> = states .into_iter() .map(|(k, v)| (k.as_ssz_bytes(), v.as_ssz_bytes())) @@ -218,62 +239,74 @@ impl Store { .put_batch(Table::States, state_entries) .expect("put states"); } + batch.commit().expect("commit"); } - Self { - time, - config, - head, - safe_target, - latest_justified, - latest_finalized, - backend, - } + Self { backend } + } + + // ============ Metadata Helpers ============ + + fn get_metadata(&self, key: &[u8]) -> T { + let view = self.backend.begin_read().expect("read view"); + let bytes = view + .get(Table::Metadata, key) + .expect("get") + .expect("metadata key exists"); + T::from_ssz_bytes(&bytes).expect("valid encoding") + } + + fn set_metadata(&self, key: &[u8], value: &T) { + let mut batch = self.backend.begin_write().expect("write batch"); + batch + .put_batch(Table::Metadata, vec![(key.to_vec(), value.as_ssz_bytes())]) + .expect("put metadata"); + batch.commit().expect("commit"); } // ============ Time ============ pub fn time(&self) -> u64 { - self.time + self.get_metadata(KEY_TIME) } pub fn set_time(&mut self, time: u64) { - self.time = time; + self.set_metadata(KEY_TIME, &time); } // ============ Config ============ - pub fn config(&self) -> &ChainConfig { - &self.config + pub fn config(&self) -> ChainConfig { + self.get_metadata(KEY_CONFIG) } // ============ Head ============ pub fn head(&self) -> H256 { - self.head + self.get_metadata(KEY_HEAD) } // ============ Safe Target ============ pub fn safe_target(&self) -> H256 { - self.safe_target + self.get_metadata(KEY_SAFE_TARGET) } pub fn set_safe_target(&mut self, safe_target: H256) { - self.safe_target = safe_target; + self.set_metadata(KEY_SAFE_TARGET, &safe_target); } // ============ Latest Justified ============ - pub fn latest_justified(&self) -> &Checkpoint { - &self.latest_justified + pub fn latest_justified(&self) -> Checkpoint { + self.get_metadata(KEY_LATEST_JUSTIFIED) } // ============ Latest Finalized ============ - pub fn latest_finalized(&self) -> &Checkpoint { - &self.latest_finalized + pub fn latest_finalized(&self) -> Checkpoint { + self.get_metadata(KEY_LATEST_FINALIZED) } // ============ Checkpoint Updates ============ @@ -284,15 +317,19 @@ impl Store { /// - Justified is updated if provided. /// - Finalized is updated if provided. pub fn update_checkpoints(&mut self, checkpoints: ForkCheckpoints) { - self.head = checkpoints.head; + let mut entries = vec![(KEY_HEAD.to_vec(), checkpoints.head.as_ssz_bytes())]; if let Some(justified) = checkpoints.justified { - self.latest_justified = justified; + entries.push((KEY_LATEST_JUSTIFIED.to_vec(), justified.as_ssz_bytes())); } if let Some(finalized) = checkpoints.finalized { - self.latest_finalized = finalized; + entries.push((KEY_LATEST_FINALIZED.to_vec(), finalized.as_ssz_bytes())); } + + let mut batch = self.backend.begin_write().expect("write batch"); + batch.put_batch(Table::Metadata, entries).expect("put"); + batch.commit().expect("commit"); } // ============ Blocks ============ @@ -586,14 +623,14 @@ 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(&self.safe_target()) .expect("safe target exists") .slot } /// Returns a clone of the head state. pub fn head_state(&self) -> State { - self.get_state(&self.head) + self.get_state(&self.head()) .expect("head state is always available") } } From 817a469ef5ffff928085f318be75729683e984a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:29:02 -0300 Subject: [PATCH 12/15] refactor: keep dyn reference of the StorageBackend --- crates/storage/src/api/traits.rs | 2 +- crates/storage/src/store.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/storage/src/api/traits.rs b/crates/storage/src/api/traits.rs index 1c30028..6014dda 100644 --- a/crates/storage/src/api/traits.rs +++ b/crates/storage/src/api/traits.rs @@ -7,7 +7,7 @@ pub type Error = Box; pub type PrefixResult = Result<(Box<[u8]>, Box<[u8]>), Error>; /// A storage backend that can create read views and write batches. -pub trait StorageBackend { +pub trait StorageBackend: Send + Sync { /// Begin a read-only transaction. fn begin_read(&self) -> Result, Error>; diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 0d86dfb..d0c2982 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use crate::api::{StorageBackend, Table}; use crate::backend::InMemoryBackend; @@ -94,7 +96,7 @@ fn decode_signature_key(bytes: &[u8]) -> SignatureKey { #[derive(Clone)] pub struct Store { /// Storage backend for all store data. - backend: InMemoryBackend, + backend: Arc, } impl Store { @@ -119,7 +121,7 @@ impl Store { let anchor_state_root = anchor_state.tree_hash_root(); let anchor_block_root = anchor_block.tree_hash_root(); - let backend = InMemoryBackend::new(); + let backend: Arc = Arc::new(InMemoryBackend::new()); let anchor_checkpoint = Checkpoint { root: anchor_block_root, @@ -191,7 +193,7 @@ impl Store { blocks: impl IntoIterator, states: impl IntoIterator, ) -> Self { - let backend = InMemoryBackend::new(); + let backend: Arc = Arc::new(InMemoryBackend::new()); // Insert all data { From 08cbb3b2ba2da9b3dae224e4be7a55c981f5fc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:29:52 -0300 Subject: [PATCH 13/15] refactor: remove unused method --- crates/storage/src/store.rs | 67 ------------------------------------- 1 file changed, 67 deletions(-) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index d0c2982..8f48b1a 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -181,73 +181,6 @@ impl Store { Self { backend } } - /// Creates a new Store with the given initial values. - #[expect(clippy::too_many_arguments)] - pub fn new( - time: u64, - config: ChainConfig, - head: H256, - safe_target: H256, - latest_justified: Checkpoint, - latest_finalized: Checkpoint, - blocks: impl IntoIterator, - states: impl IntoIterator, - ) -> Self { - let backend: Arc = Arc::new(InMemoryBackend::new()); - - // Insert all data - { - let mut batch = backend.begin_write().expect("write batch"); - - // Metadata - batch - .put_batch( - Table::Metadata, - vec![ - (KEY_TIME.to_vec(), time.as_ssz_bytes()), - (KEY_CONFIG.to_vec(), config.as_ssz_bytes()), - (KEY_HEAD.to_vec(), head.as_ssz_bytes()), - (KEY_SAFE_TARGET.to_vec(), safe_target.as_ssz_bytes()), - ( - KEY_LATEST_JUSTIFIED.to_vec(), - latest_justified.as_ssz_bytes(), - ), - ( - KEY_LATEST_FINALIZED.to_vec(), - latest_finalized.as_ssz_bytes(), - ), - ], - ) - .expect("put metadata"); - - // Blocks - let block_entries: Vec<_> = blocks - .into_iter() - .map(|(k, v)| (k.as_ssz_bytes(), v.as_ssz_bytes())) - .collect(); - if !block_entries.is_empty() { - batch - .put_batch(Table::Blocks, block_entries) - .expect("put blocks"); - } - - // States - let state_entries: Vec<_> = states - .into_iter() - .map(|(k, v)| (k.as_ssz_bytes(), v.as_ssz_bytes())) - .collect(); - if !state_entries.is_empty() { - batch - .put_batch(Table::States, state_entries) - .expect("put states"); - } - - batch.commit().expect("commit"); - } - - Self { backend } - } - // ============ Metadata Helpers ============ fn get_metadata(&self, key: &[u8]) -> T { From ff23dba3b5d18fa310c536c7946e40429e254942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:14:37 -0300 Subject: [PATCH 14/15] fix: change anchor checkpoint to use block slot --- crates/storage/src/store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 8f48b1a..4ed2af4 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -125,7 +125,7 @@ impl Store { let anchor_checkpoint = Checkpoint { root: anchor_block_root, - slot: 0, + slot: anchor_block.slot, }; // Insert initial data From 82da711509df6b5c584fc9ca7333a13a461ff5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:25:05 -0300 Subject: [PATCH 15/15] chore: fix lint --- crates/blockchain/src/store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index f847714..42b891d 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -38,7 +38,7 @@ fn update_head(store: &mut Store) { &attestations, 0, ); - if is_reorg(old_head, new_head, &store) { + if is_reorg(old_head, new_head, store) { metrics::inc_fork_choice_reorgs(); info!(%old_head, %new_head, "Fork choice reorg detected"); }