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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions TODO-static-block-storage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Static Block Storage TODO

Current spec: [`specs/static-blocks.md`](./specs/static-blocks.md)

Implemented:
- static block file format spec
- `StaticBlockStore::open/get/put`
- snappy-framed block records
- fixed-size `.off` sidecar files
- global `static_blocks.conf` commit marker
- startup healing for interrupted writes

Remaining:

1. Wire startup/config.
- add CLI/config path for enabling static block storage
- initialize `HotColdDB::static_blocks`
- reject checkpoint sync, late activation, and historical backfill init modes

2. Bump schema.
- `DBColumn::BeaconBlockSlot` was added
- update schema version in `beacon_node/store/src/metadata.rs`

3. Verify static fallback reads.
- after `static_blocks.get(slot)`, decode and verify the block root matches the requested root
- treat mismatches as corruption

4. Update invariants.
- archived finalized blocks no longer require hot-db block bodies
- root/slot indices must remain consistent with static storage

5. Add tests.
- archive/read happy path
- skip-slot dedup
- out-of-order put rejection
- crash windows around data, `.off`, and `.conf`
- wrong `BeaconBlockSlot`
- unsupported startup modes

6. Decide decompression bound wiring.
- current implementation uses a local 10 MiB bound
- consider passing consensus `max_payload_size` or another store config value
1 change: 1 addition & 0 deletions beacon_node/store/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ redb = { version = "2.1.3", optional = true }
safe_arith = { workspace = true }
serde = { workspace = true }
smallvec = { workspace = true }
snap = { workspace = true }
ssz_types = { workspace = true }
state_processing = { workspace = true }
strum = { workspace = true }
Expand Down
16 changes: 16 additions & 0 deletions beacon_node/store/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::config::StoreConfigError;
use crate::hot_cold_store::{HotColdDBError, StateSummaryIteratorError};
use crate::static_blobs::StaticBlobStoreError;
use crate::static_blocks::StaticBlockStoreError;
use crate::{DBColumn, hdiff};
#[cfg(feature = "leveldb")]
use leveldb::error::Error as LevelDBError;
Expand All @@ -14,6 +16,8 @@ pub enum Error {
SszDecodeError(DecodeError),
BeaconStateError(BeaconStateError),
HotColdDBError(HotColdDBError),
StaticBlockStoreError(StaticBlockStoreError),
StaticBlobStoreError(StaticBlobStoreError),
DBError {
message: String,
},
Expand Down Expand Up @@ -129,6 +133,18 @@ impl From<HotColdDBError> for Error {
}
}

impl From<StaticBlockStoreError> for Error {
fn from(e: StaticBlockStoreError) -> Error {
Error::StaticBlockStoreError(e)
}
}

impl From<StaticBlobStoreError> for Error {
fn from(e: StaticBlobStoreError) -> Error {
Error::StaticBlobStoreError(e)
}
}

impl From<BeaconStateError> for Error {
fn from(e: BeaconStateError) -> Error {
Error::BeaconStateError(e)
Expand Down
34 changes: 33 additions & 1 deletion beacon_node/store/src/hot_cold_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::metadata::{
SCHEMA_VERSION_KEY, SPLIT_KEY, STATE_UPPER_LIMIT_NO_RETAIN, SchemaVersion,
};
use crate::state_cache::{PutStateOutcome, StateCache};
use crate::static_blobs::StaticBlobStore;
use crate::static_blocks::StaticBlockStore;
use crate::{
BlobSidecarListFromRoot, DBColumn, DatabaseBlock, Error, ItemStore, KeyValueStoreOp, StoreItem,
Expand Down Expand Up @@ -76,6 +77,8 @@ pub struct HotColdDB<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> {
/// reads fall through to it after missing in `hot_db`. When `None` (legacy mode), all
/// finalized blinded blocks remain in `hot_db` as today.
pub static_blocks: Option<Arc<StaticBlockStore>>,
/// Optional slot-keyed archive for finalized blob sidecars.
pub static_blobs: Option<Arc<StaticBlobStore>>,
/// LRU cache of deserialized blocks and blobs. Updated whenever a block or blob is loaded.
block_cache: Option<Mutex<BlockCache<E>>>,
/// Cache of beacon states.
Expand Down Expand Up @@ -242,6 +245,7 @@ impl<E: EthSpec> HotColdDB<E, MemoryStore<E>, MemoryStore<E>> {
blobs_db: MemoryStore::open(),
hot_db: MemoryStore::open(),
static_blocks: None,
static_blobs: None,
block_cache: NonZeroUsize::new(config.block_cache_size)
.map(BlockCache::new)
.map(Mutex::new),
Expand Down Expand Up @@ -297,6 +301,7 @@ impl<E: EthSpec> HotColdDB<E, BeaconNodeBackend<E>, BeaconNodeBackend<E>> {
cold_db: BeaconNodeBackend::open(&config, cold_path)?,
hot_db,
static_blocks: None,
static_blobs: None,
block_cache: NonZeroUsize::new(config.block_cache_size)
.map(BlockCache::new)
.map(Mutex::new),
Expand Down Expand Up @@ -2700,10 +2705,37 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
Ok(BlobSidecarListFromRoot::NoBlobs)
}
}
None => Ok(BlobSidecarListFromRoot::NoRoot),
None => self.get_static_blobs(block_root),
}
}

/// Fetch blobs from the slot-keyed static archive after a blob-db miss.
fn get_static_blobs(&self, block_root: &Hash256) -> Result<BlobSidecarListFromRoot<E>, Error> {
let Some(static_blobs) = &self.static_blobs else {
return Ok(BlobSidecarListFromRoot::NoRoot);
};
let Some(slot) = self.get_finalized_blinded_block_slot(block_root)? else {
return Ok(BlobSidecarListFromRoot::NoRoot);
};
let Some(blobs_bytes) = static_blobs.get(slot)? else {
return Ok(BlobSidecarListFromRoot::NoBlobs);
};

let blobs: Vec<Arc<BlobSidecar<E>>> = Vec::<_>::from_ssz_bytes(&blobs_bytes)?;
let Some(max_blobs_per_block) = blobs
.first()
.map(|blob| self.spec.max_blobs_per_block(blob.epoch()))
else {
return Ok(BlobSidecarListFromRoot::NoBlobs);
};

let blobs = BlobSidecarList::new(blobs, max_blobs_per_block as usize)?;
self.block_cache
.as_ref()
.inspect(|cache| cache.lock().put_blobs(*block_root, blobs.clone()));
Ok(BlobSidecarListFromRoot::Blobs(blobs))
}

/// Fetch all keys in the data_column column with prefix `block_root`
pub fn get_data_column_keys(&self, block_root: Hash256) -> Result<Vec<ColumnIndex>, Error> {
self.blobs_db
Expand Down
2 changes: 2 additions & 0 deletions beacon_node/store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod metadata;
pub mod metrics;
pub mod reconstruct;
pub mod state_cache;
pub mod static_blobs;
pub mod static_blocks;

pub mod database;
Expand All @@ -30,6 +31,7 @@ pub use self::blob_sidecar_list_from_root::BlobSidecarListFromRoot;
pub use self::config::StoreConfig;
pub use self::hot_cold_store::{HotColdDB, HotStateSummary, Split};
pub use self::memory_store::MemoryStore;
pub use self::static_blobs::StaticBlobStore;
pub use self::static_blocks::StaticBlockStore;
pub use crate::metadata::BlobInfo;
pub use errors::Error;
Expand Down
59 changes: 59 additions & 0 deletions beacon_node/store/src/static_blobs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! Slot-keyed archive API for finalized blob sidecars.
//!
//! This is the minimal surface needed to test HotColdDB integration. The file
//! backend is intentionally not implemented yet.

use std::{
fmt, io,
path::{Path, PathBuf},
};
use types::Slot;

#[derive(Debug)]
pub struct StaticBlobStore {
root_dir: PathBuf,
}

#[derive(Debug)]
pub enum StaticBlobStoreError {
Io(io::Error),
Unsupported(&'static str),
}

impl fmt::Display for StaticBlobStoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "static blob store io error: {e}"),
Self::Unsupported(message) => {
write!(f, "static blob store unsupported operation: {message}")
}
}
}
}

impl From<io::Error> for StaticBlobStoreError {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}

impl StaticBlobStore {
/// Open the archive rooted at `path`.
pub fn open(path: &Path) -> Result<Self, StaticBlobStoreError> {
Ok(Self {
root_dir: path.to_path_buf(),
})
}

/// Read SSZ-encoded blob sidecars for `slot`, if present.
pub fn get(&self, _slot: Slot) -> Result<Option<Vec<u8>>, StaticBlobStoreError> {
let _ = &self.root_dir;
Err(StaticBlobStoreError::Unsupported("get"))
}

/// Store SSZ-encoded blob sidecars at `slot`.
pub fn put(&self, _slot: Slot, _bytes: &[u8]) -> Result<(), StaticBlobStoreError> {
let _ = &self.root_dir;
Err(StaticBlobStoreError::Unsupported("put"))
}
}
Loading
Loading