Skip to content

Commit a649196

Browse files
authored
chore: some storage adjustments (leanEthereum#427)
1 parent fc07e2d commit a649196

8 files changed

Lines changed: 1068 additions & 259 deletions

File tree

src/lean_spec/subspecs/node/node.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,20 @@ def from_genesis(cls, config: NodeConfig) -> Node:
209209
store = state.to_forkchoice_store(block, validator_id)
210210

211211
# Persist genesis to database if available.
212+
#
213+
# Atomic write ensures a crash during genesis persistence
214+
# does not leave the database in a partial state.
212215
if database is not None:
213216
block_root = hash_tree_root(block)
214-
database.put_block(block, block_root)
215-
database.put_state(state, block_root)
216-
database.put_head_root(block_root)
217-
database.put_justified_checkpoint(store.latest_justified)
218-
database.put_finalized_checkpoint(store.latest_finalized)
219-
database.put_block_root_by_slot(block.slot, block_root)
217+
with database.batch_write():
218+
database.put_block(block, block_root)
219+
database.put_state(state, block_root)
220+
database.put_head_root(block_root)
221+
database.put_justified_checkpoint(store.latest_justified)
222+
database.put_finalized_checkpoint(store.latest_finalized)
223+
database.put_block_root_by_slot(block.slot, block_root)
224+
database.put_block_root_by_state_root(hash_tree_root(state), block_root)
225+
database.put_genesis_time(config.genesis_time)
220226

221227
# Create shared dependencies.
222228
clock = SlotClock(genesis_time=config.genesis_time, time_fn=config.time_fn)
@@ -353,6 +359,12 @@ def _try_load_from_database(
353359
if justified is None or finalized is None:
354360
return None
355361

362+
# Fall back to genesis time stored in the database.
363+
#
364+
# This enables self-contained restarts without external config.
365+
if genesis_time is None:
366+
genesis_time = database.get_genesis_time()
367+
356368
# Compute store time from wall clock to avoid post-restart drift.
357369
#
358370
# Using only the head block's slot would set the store time to the

src/lean_spec/subspecs/storage/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
"""
77

88
from .database import Database
9-
from .namespaces import BlockNamespace, CheckpointNamespace, StateNamespace
9+
from .exceptions import StorageCorruptionError, StorageError, StorageReadError, StorageWriteError
1010
from .sqlite import SQLiteDatabase
1111

1212
__all__ = [
1313
"Database",
1414
"SQLiteDatabase",
15-
"BlockNamespace",
16-
"StateNamespace",
17-
"CheckpointNamespace",
15+
"StorageCorruptionError",
16+
"StorageError",
17+
"StorageReadError",
18+
"StorageWriteError",
1819
]

src/lean_spec/subspecs/storage/database.py

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77

88
from __future__ import annotations
99

10+
from collections.abc import Generator
11+
from contextlib import contextmanager
1012
from typing import TYPE_CHECKING, Protocol
1113

1214
if TYPE_CHECKING:
1315
from lean_spec.subspecs.containers import Block, Checkpoint, State
1416
from lean_spec.subspecs.containers.attestation import AttestationData
1517
from lean_spec.subspecs.containers.slot import Slot
1618
from lean_spec.subspecs.containers.validator import ValidatorIndex
17-
from lean_spec.types import Bytes32
19+
from lean_spec.types import Bytes32, Uint64
1820

1921

2022
class Database(Protocol):
@@ -26,10 +28,11 @@ class Database(Protocol):
2628
2729
Storage Organization
2830
--------------------
29-
- Blocks: Indexed by root hash
30-
- States: Indexed by root hash
31+
- Blocks: Indexed by block root hash
32+
- States: Indexed by associated block root hash (not state root)
3133
- Checkpoints: Justified and finalized tracking
3234
- Attestations: Latest attestation per validator
35+
- State root index: Maps state roots to block roots
3336
"""
3437

3538
# Block Operations
@@ -72,10 +75,10 @@ def has_block(self, root: Bytes32) -> bool:
7275

7376
def get_state(self, root: Bytes32) -> State | None:
7477
"""
75-
Retrieve a state by its root hash.
78+
Retrieve a state by its associated block root.
7679
7780
Args:
78-
root: SSZ hash tree root of the state.
81+
root: Block root hash associated with this state.
7982
8083
Returns:
8184
State if found, None otherwise.
@@ -84,11 +87,11 @@ def get_state(self, root: Bytes32) -> State | None:
8487

8588
def put_state(self, state: State, root: Bytes32) -> None:
8689
"""
87-
Store a state with its root hash.
90+
Store a state indexed by its associated block root.
8891
8992
Args:
9093
state: State to store.
91-
root: Pre-computed root hash (avoids recomputation).
94+
root: Block root hash associated with this state.
9295
"""
9396
...
9497

@@ -97,7 +100,7 @@ def has_state(self, root: Bytes32) -> bool:
97100
Check if a state exists in storage.
98101
99102
Args:
100-
root: SSZ hash tree root of the state.
103+
root: Block root hash associated with the state.
101104
102105
Returns:
103106
True if state exists.
@@ -223,6 +226,93 @@ def put_block_root_by_slot(self, slot: Slot, root: Bytes32) -> None:
223226
"""
224227
...
225228

229+
# State Root Index Operations
230+
231+
def get_block_root_by_state_root(self, state_root: Bytes32) -> Bytes32 | None:
232+
"""
233+
Look up the block root associated with a state root.
234+
235+
Needed for checkpoint sync and API endpoints that query by state root.
236+
237+
Args:
238+
state_root: SSZ hash tree root of the state.
239+
240+
Returns:
241+
Associated block root, or None if not indexed.
242+
"""
243+
...
244+
245+
def put_block_root_by_state_root(self, state_root: Bytes32, block_root: Bytes32) -> None:
246+
"""
247+
Index a block root by the state root it produced.
248+
249+
Args:
250+
state_root: SSZ hash tree root of the post-state.
251+
block_root: Root of the block that produced this state.
252+
"""
253+
...
254+
255+
# Genesis Time
256+
257+
def get_genesis_time(self) -> Uint64 | None:
258+
"""
259+
Retrieve the stored genesis time.
260+
261+
Enables self-contained restarts without external genesis config.
262+
263+
Returns:
264+
Genesis time as Unix timestamp, or None if not set.
265+
"""
266+
...
267+
268+
def put_genesis_time(self, genesis_time: Uint64) -> None:
269+
"""
270+
Store genesis time for future restarts.
271+
272+
Args:
273+
genesis_time: Unix timestamp of genesis (slot 0).
274+
"""
275+
...
276+
277+
# Transaction Control
278+
279+
def commit(self) -> None:
280+
"""
281+
Commit pending writes to durable storage.
282+
283+
All writes via put_* methods are buffered until commit() or batch_write().
284+
Callers must explicitly commit after writes.
285+
"""
286+
...
287+
288+
@contextmanager
289+
def batch_write(self) -> Generator[None]:
290+
"""
291+
Context manager for atomic multi-write operations.
292+
293+
All writes within the block are committed atomically on exit.
294+
Rolls back on exception to prevent partial writes.
295+
"""
296+
...
297+
298+
# Pruning
299+
300+
def prune_before_slot(self, slot: Slot, keep_roots: frozenset[Bytes32]) -> int:
301+
"""
302+
Remove blocks and states with slots strictly before the given slot.
303+
304+
Preserves entries whose roots are in keep_roots (e.g., the finalized block).
305+
Cleans up associated slot index entries.
306+
307+
Args:
308+
slot: Prune entries with slots strictly below this value.
309+
keep_roots: Roots to preserve regardless of slot.
310+
311+
Returns:
312+
Total number of entries pruned across all tables.
313+
"""
314+
...
315+
226316
# Lifecycle
227317

228318
def close(self) -> None:
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Storage exception hierarchy.
3+
4+
Wraps low-level database and serialization errors with storage-specific context.
5+
This allows callers to handle storage failures uniformly without knowing the
6+
underlying backend (SQLite, etc.).
7+
"""
8+
9+
10+
class StorageError(Exception):
11+
"""Base exception for storage operations."""
12+
13+
14+
class StorageReadError(StorageError):
15+
"""Failed to read from storage."""
16+
17+
18+
class StorageWriteError(StorageError):
19+
"""Failed to write to storage."""
20+
21+
22+
class StorageCorruptionError(StorageError):
23+
"""Stored data failed deserialization."""

src/lean_spec/subspecs/storage/namespaces.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ class CheckpointNamespace:
8686
KEY_HEAD: str = "head"
8787
"""Key for head block root."""
8888

89+
KEY_GENESIS_TIME: str = "genesis_time"
90+
"""Key for genesis time. Enables self-contained restarts without external config."""
91+
8992
CREATE_TABLE: str = """
9093
CREATE TABLE IF NOT EXISTS checkpoints (
9194
key TEXT PRIMARY KEY,
@@ -137,9 +140,41 @@ class SlotIndexNamespace:
137140
"""SQL to create slot index table."""
138141

139142

140-
# Singleton instances for convenient access
143+
@dataclass(frozen=True, slots=True)
144+
class StateRootIndexNamespace:
145+
"""
146+
Namespace for state root to block root mapping.
147+
148+
Enables lookup of block root by state root.
149+
Needed for checkpoint sync and API queries by state root.
150+
"""
151+
152+
TABLE_NAME: str = "state_root_index"
153+
"""Table name for state root index."""
154+
155+
CREATE_TABLE: str = """
156+
CREATE TABLE IF NOT EXISTS state_root_index (
157+
state_root BLOB PRIMARY KEY,
158+
block_root BLOB NOT NULL
159+
)
160+
"""
161+
"""SQL to create state root index table."""
162+
163+
141164
BLOCKS: Final = BlockNamespace()
165+
"""Block storage namespace."""
166+
142167
STATES: Final = StateNamespace()
168+
"""State storage namespace."""
169+
143170
CHECKPOINTS: Final = CheckpointNamespace()
171+
"""Checkpoint tracking namespace."""
172+
144173
ATTESTATIONS: Final = AttestationNamespace()
174+
"""Attestation storage namespace."""
175+
145176
SLOT_INDEX: Final = SlotIndexNamespace()
177+
"""Slot-to-root index namespace."""
178+
179+
STATE_ROOT_INDEX: Final = StateRootIndexNamespace()
180+
"""State root to block root index namespace."""

0 commit comments

Comments
 (0)