Skip to content

Commit a8f6597

Browse files
committed
refactor: continue and add cold
1 parent 1a9f59f commit a8f6597

35 files changed

Lines changed: 2189 additions & 1180 deletions

crates/storage/Cargo.toml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,24 @@ bytes = "1.11.0"
1515
reth.workspace = true
1616
reth-db = { workspace = true, features = ["test-utils"] }
1717
reth-db-api.workspace = true
18-
reth-libmdbx.workspace = true
18+
signet-db.workspace = true
1919
thiserror.workspace = true
20+
tokio.workspace = true
21+
tokio-util = { version = "0.7", features = ["rt"] }
22+
tracing.workspace = true
2023
trevm.workspace = true
2124

25+
reth-libmdbx = { workspace = true, optional = true }
26+
27+
2228
[dev-dependencies]
2329
serial_test = "3.3.1"
2430
tempfile.workspace = true
31+
32+
33+
[features]
34+
default = ["mdbx", "in-mem"]
35+
mdbx = ["dep:reth-libmdbx", "impls"]
36+
in-mem = ["impls"]
37+
test-utils = ["in-mem"]
38+
impls = []

crates/storage/README.md

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
# Signet Storage
22

3-
High-level API for Signet's storage layer
3+
High-level APIs for Signet's storage layer.
44

5-
This library contains the following:
5+
## Design Overview
66

7-
- Traits for serializing and deserializing Signet data structures as DB keys/
8-
value.
9-
- Traits for hot and cold storage operations.
10-
- Relevant KV table definitions.
7+
We divide the storage system into two main components:
118

12-
## Significant Traits
9+
1. Hot storage, used in the critical consensus path.
10+
2. Cold storage, used for historical data, RPC queries, and archival.
1311

14-
- `HotKv` - Encapsulates logic for reading and writing to hot storage.
15-
- `ColdKv` - Encapsulates logic for reading and writing to cold storage.
16-
- `KeySer` - Provides methods for serializing a type as a DB key.
17-
- `ValueSer` - Provides methods for serializing a type as a DB value.
12+
Hot and cold storage have different designs because they serve different
13+
purposes:
14+
15+
- **Mutability**: Hot state changes constantly during block execution; cold
16+
data is finalized history that only grows (or truncates during reorgs).
17+
- **Access patterns**: State execution requires fast point lookups; historical
18+
queries are block-centric and sequential.
19+
- **Consistency**: Hot storage needs ACID transactions to maintain consistent
20+
state mid-block; cold storage can use eventual consistency via async ops.
21+
22+
This separation allows us to optimize each layer for its specific access
23+
patterns and performance requirements. Hot storage needs to be fast and mutable,
24+
while cold storage can be optimized for bulk writes, and asynchronous access.
25+
26+
See the module documentation for `hot` and `cold` for more details on each
27+
design.
28+
29+
```ignore,bash
30+
cargo doc --no-deps --open -p signet-storage
31+
```
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
//! Conformance tests for ColdStorage backends.
2+
//!
3+
//! These tests verify that any backend implementation behaves correctly
4+
//! according to the ColdStorage trait contract. To use these tests with
5+
//! a custom backend, call the test functions with your backend instance.
6+
7+
use crate::cold::{BlockData, BlockTag, ColdResult, ColdStorage, HeaderSpecifier};
8+
use alloy::primitives::{B256, BlockNumber};
9+
use reth::primitives::Header;
10+
11+
/// Run all conformance tests against a backend.
12+
///
13+
/// This is the main entry point for testing a custom backend implementation.
14+
pub async fn conformance<B: ColdStorage>(backend: &B) -> ColdResult<()> {
15+
test_empty_storage(backend).await?;
16+
test_append_and_read_header(backend).await?;
17+
test_header_hash_lookup(backend).await?;
18+
test_header_tag_lookup(backend).await?;
19+
test_transaction_lookups(backend).await?;
20+
test_receipt_lookups(backend).await?;
21+
test_truncation(backend).await?;
22+
test_batch_append(backend).await?;
23+
test_latest_block_tracking(backend).await?;
24+
Ok(())
25+
}
26+
27+
/// Create test block data for conformance tests.
28+
///
29+
/// Creates a minimal valid block with the given block number.
30+
pub fn make_test_block(block_number: BlockNumber) -> BlockData {
31+
let header = Header { number: block_number, ..Default::default() };
32+
33+
BlockData::new(header, vec![], vec![], vec![], None)
34+
}
35+
36+
/// Test that empty storage returns None/empty for all lookups.
37+
pub async fn test_empty_storage<B: ColdStorage>(backend: &B) -> ColdResult<()> {
38+
assert!(backend.get_header(HeaderSpecifier::Number(0)).await?.is_none());
39+
assert!(backend.get_header(HeaderSpecifier::Hash(B256::ZERO)).await?.is_none());
40+
assert!(backend.get_header(HeaderSpecifier::Tag(BlockTag::Latest)).await?.is_none());
41+
assert!(backend.get_latest_block().await?.is_none());
42+
assert!(backend.get_transactions_in_block(0).await?.is_empty());
43+
assert!(backend.get_receipts_in_block(0).await?.is_empty());
44+
assert_eq!(backend.get_transaction_count(0).await?, 0);
45+
Ok(())
46+
}
47+
48+
/// Test basic append and read for headers.
49+
pub async fn test_append_and_read_header<B: ColdStorage>(backend: &B) -> ColdResult<()> {
50+
let block_data = make_test_block(100);
51+
let expected_header = block_data.header.clone();
52+
53+
backend.append_block(block_data).await?;
54+
55+
let retrieved = backend.get_header(HeaderSpecifier::Number(100)).await?;
56+
assert!(retrieved.is_some());
57+
assert_eq!(retrieved.unwrap(), expected_header);
58+
59+
Ok(())
60+
}
61+
62+
/// Test header lookup by hash.
63+
pub async fn test_header_hash_lookup<B: ColdStorage>(backend: &B) -> ColdResult<()> {
64+
let block_data = make_test_block(101);
65+
let header_hash = block_data.header.hash_slow();
66+
67+
backend.append_block(block_data).await?;
68+
69+
let retrieved = backend.get_header(HeaderSpecifier::Hash(header_hash)).await?;
70+
assert!(retrieved.is_some());
71+
72+
// Non-existent hash should return None
73+
let missing = backend.get_header(HeaderSpecifier::Hash(B256::ZERO)).await?;
74+
assert!(missing.is_none());
75+
76+
Ok(())
77+
}
78+
79+
/// Test header lookup by tag.
80+
pub async fn test_header_tag_lookup<B: ColdStorage>(backend: &B) -> ColdResult<()> {
81+
backend.append_block(make_test_block(50)).await?;
82+
backend.append_block(make_test_block(51)).await?;
83+
backend.append_block(make_test_block(52)).await?;
84+
85+
// Latest should return block 52
86+
let latest = backend.get_header(HeaderSpecifier::Tag(BlockTag::Latest)).await?;
87+
assert!(latest.is_some());
88+
89+
// Earliest should return block 50
90+
let earliest = backend.get_header(HeaderSpecifier::Tag(BlockTag::Earliest)).await?;
91+
assert!(earliest.is_some());
92+
93+
Ok(())
94+
}
95+
96+
/// Test transaction lookups by hash and by block+index.
97+
pub async fn test_transaction_lookups<B: ColdStorage>(backend: &B) -> ColdResult<()> {
98+
// Create block with empty transactions for now
99+
let block_data = make_test_block(200);
100+
101+
backend.append_block(block_data).await?;
102+
103+
let txs = backend.get_transactions_in_block(200).await?;
104+
let count = backend.get_transaction_count(200).await?;
105+
assert_eq!(txs.len() as u64, count);
106+
107+
Ok(())
108+
}
109+
110+
/// Test receipt lookups.
111+
pub async fn test_receipt_lookups<B: ColdStorage>(backend: &B) -> ColdResult<()> {
112+
let block_data = make_test_block(201);
113+
114+
backend.append_block(block_data).await?;
115+
116+
let receipts = backend.get_receipts_in_block(201).await?;
117+
// Empty receipts for now
118+
assert!(receipts.is_empty());
119+
120+
Ok(())
121+
}
122+
123+
/// Test truncation removes data correctly.
124+
pub async fn test_truncation<B: ColdStorage>(backend: &B) -> ColdResult<()> {
125+
// Append blocks 300, 301, 302
126+
backend.append_block(make_test_block(300)).await?;
127+
backend.append_block(make_test_block(301)).await?;
128+
backend.append_block(make_test_block(302)).await?;
129+
130+
// Truncate above 300 (removes 301, 302)
131+
backend.truncate_above(300).await?;
132+
133+
// Block 300 should still exist
134+
assert!(backend.get_header(HeaderSpecifier::Number(300)).await?.is_some());
135+
136+
// Blocks 301, 302 should be gone
137+
assert!(backend.get_header(HeaderSpecifier::Number(301)).await?.is_none());
138+
assert!(backend.get_header(HeaderSpecifier::Number(302)).await?.is_none());
139+
140+
// Latest should now be 300
141+
assert_eq!(backend.get_latest_block().await?, Some(300));
142+
143+
Ok(())
144+
}
145+
146+
/// Test batch append.
147+
pub async fn test_batch_append<B: ColdStorage>(backend: &B) -> ColdResult<()> {
148+
let blocks = vec![make_test_block(400), make_test_block(401), make_test_block(402)];
149+
150+
backend.append_blocks(blocks).await?;
151+
152+
assert!(backend.get_header(HeaderSpecifier::Number(400)).await?.is_some());
153+
assert!(backend.get_header(HeaderSpecifier::Number(401)).await?.is_some());
154+
assert!(backend.get_header(HeaderSpecifier::Number(402)).await?.is_some());
155+
156+
Ok(())
157+
}
158+
159+
/// Test latest block tracking.
160+
pub async fn test_latest_block_tracking<B: ColdStorage>(backend: &B) -> ColdResult<()> {
161+
// Append out of order
162+
backend.append_block(make_test_block(502)).await?;
163+
assert_eq!(backend.get_latest_block().await?, Some(502));
164+
165+
backend.append_block(make_test_block(500)).await?;
166+
// Latest should still be 502
167+
assert_eq!(backend.get_latest_block().await?, Some(502));
168+
169+
backend.append_block(make_test_block(505)).await?;
170+
assert_eq!(backend.get_latest_block().await?, Some(505));
171+
172+
Ok(())
173+
}

crates/storage/src/cold/error.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//! Error types for cold storage operations.
2+
3+
/// Result type alias for cold storage operations.
4+
pub type ColdResult<T, E = ColdStorageError> = Result<T, E>;
5+
6+
/// Error type for cold storage operations.
7+
#[derive(Debug, thiserror::Error)]
8+
pub enum ColdStorageError {
9+
/// An error occurred in the storage backend.
10+
#[error("Backend error: {0}")]
11+
Backend(#[from] Box<dyn core::error::Error + Send + Sync + 'static>),
12+
13+
/// The requested resource was not found.
14+
#[error("Not found: {0}")]
15+
NotFound(String),
16+
17+
/// The storage task was cancelled.
18+
#[error("Task cancelled")]
19+
Cancelled,
20+
}
21+
22+
impl ColdStorageError {
23+
/// Create a new backend error from any error type.
24+
pub fn backend<E>(error: E) -> Self
25+
where
26+
E: core::error::Error + Send + Sync + 'static,
27+
{
28+
Self::Backend(Box::new(error))
29+
}
30+
}

0 commit comments

Comments
 (0)