dig-blockstore 0.1.1

DIG L2 block persistence — RocksDB-backed block store with canonical chain indexing
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# dig-blockstore

Persistent block storage for the DIG L2 network. RocksDB-backed with six column families, dual-layer canonical chain indexing (memory-mapped file + RocksDB), sharded in-memory LRU caches, zstd compression with trained dictionaries, async write pipeline, rollback/reorg support, checkpoint storage, pruning, and snapshot export/import.

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│                      BlockStore                         │
│  ┌──────────┐  ┌──────────┐  ┌───────────────────────┐ │
│  │ block    │  │ header   │  │ record cache          │ │
│  │ cache    │  │ cache    │  │ (HashMap, RAM-only)    │ │
│  │ (sharded │  │ (sharded │  │                       │ │
│  │  LRU)    │  │  LRU)    │  │ + height index cache  │ │
│  └────┬─────┘  └────┬─────┘  │ + hash→height cache   │ │
│       │              │        └───────────┬───────────┘ │
│  ┌────┴──────────────┴────────────────────┴───────────┐ │
│  │              RocksDB (6 column families)            │ │
│  │  CF_BLOCKS  CF_HEADERS  CF_CANONICAL  CF_METADATA  │ │
│  │  CF_ATTESTED  CF_CHECKPOINTS                       │ │
│  └────────────────────────────────────────────────────┘ │
│  ┌────────────────────────────────────────────────────┐ │
│  │  canonical.bin (memory-mapped dense height→hash)   │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```

## Quick Start

```rust
use dig_blockstore::{
    BlockStore, BlockStoreConfig, ChainTip,
    L2Block, L2BlockHeader, Bytes32, BlockStatus,  // re-exported from dig-block / chia-protocol
};

// Open or create a store
let config = BlockStoreConfig {
    path: "data/my-blockstore".into(),
    ..Default::default()
};
let store = BlockStore::open(config)?;

// Initialize genesis
store.init_genesis(&genesis_block)?;

// Extend the canonical chain
store.extend_chain(&block)?;

// Query by hash or height
let block = store.get_block(&hash)?;
let header = store.get_header_by_height(42)?;
let tip = store.tip(); // Option<ChainTip>
```

---

## Public API Reference

### BlockStore — Constructors

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `open(config)` | `BlockStoreConfig` | `Result<BlockStore>` | Open or create a store at `config.path`. Creates all column families, loads tip from metadata, optionally warms caches. |
| `open_readonly(path)` | `impl AsRef<Path>` | `Result<BlockStore>` | Open existing database read-only. All mutation methods return `ERR_MUTATION_READ_ONLY`. Path must exist. |

### BlockStore — Genesis & Chain Tip

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `init_genesis(&self, block)` | `&L2Block` | `Result<()>` | Initialize an empty store with the genesis block. Atomic WriteBatch writes block + header + canonical index + tip + genesis hash. Fails if already initialized. |
| `tip(&self)` || `Option<ChainTip>` | Current canonical chain tip (hash + height). `None` before genesis. Lock-free read from `RwLock`. |
| `height(&self)` || `Option<u64>` | Shorthand for `tip().map(\|t\| t.height)`. |
| `set_tip(&self, tip)` | `ChainTip` | `Result<()>` | Persist a new chain tip to `CF_METADATA`/`META_TIP` (40 bytes: hash \|\| height LE). Updates in-memory cache. |

### BlockStore — Block Storage (Write)

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `put_block(&self, block, canonical)` | `&L2Block, bool` | `Result<bool>` | Store a block. Returns `true` if novel, `false` if duplicate (idempotent). When `canonical=true`, also writes height→hash to `CF_CANONICAL`. Triggers dictionary training when block count reaches 1000. |
| `put(&self, block, canonical)` | `&L2Block, bool` | `Result<bool>` | Alias for `put_block`. |
| `extend_chain(&self, block)` | `&L2Block` | `Result<bool>` | Primary ingestion API: `put(block, true)` + `set_tip`. Returns `false` for duplicates. Does NOT validate parent-hash linkage (caller's responsibility). |
| `put_pipelined(&self, block, canonical)` | `L2Block, bool` | `Result<Receiver<Result<bool>>>` | Async batched write. Enqueues into bounded channel; background task flushes via single WriteBatch. Returns oneshot receiver for per-block ack. Requires tokio runtime. |

### BlockStore — Block Retrieval (Read)

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `get_block(&self, hash)` | `&Bytes32` | `Result<Option<L2Block>>` | Three-tier lookup: sharded LRU cache → RocksDB CF_BLOCKS (zstd decompress) → `None`. Populates cache on miss. |
| `get_header(&self, hash)` | `&Bytes32` | `Result<Option<L2BlockHeader>>` | Header cache → RocksDB CF_HEADERS (bincode, no zstd) → `None`. |
| `get_record(&self, hash)` | `&Bytes32` | `Result<Option<BlockRecord>>` | Record cache → header cache (derive) → CF_HEADERS (deserialize + derive) → `None`. Records are never persisted. |
| `has_block(&self, hash)` | `&Bytes32` | `Result<bool>` | Lightweight existence check. Probes caches first (no LRU promotion), then RocksDB key existence (no deserialization). |
| `get_blocks_by_hash(&self, hashes)` | `&[Bytes32]` | `Result<Vec<Option<L2Block>>>` | Batch retrieval. Cache hits served individually; all misses consolidated into one `multi_get_cf`. Output index matches input order. |

### BlockStore — Canonical Chain (Height-Based)

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `get_hash_by_height(&self, height)` | `u64` | `Result<Option<Bytes32>>` | Resolve canonical hash. Checks: BTreeMap cache → mmap `canonical.bin` → RocksDB `CF_CANONICAL`. Returns `None` for non-canonical or pruned heights. |
| `get_block_by_height(&self, height)` | `u64` | `Result<Option<L2Block>>` | `get_hash_by_height``get_block`. |
| `get_header_by_height(&self, height)` | `u64` | `Result<Option<L2BlockHeader>>` | `get_hash_by_height``get_header`. |
| `get_record_by_height(&self, height)` | `u64` | `Result<Option<BlockRecord>>` | `get_hash_by_height``get_record`. |
| `get_epoch_block_hashes(&self, epoch)` | `u64` | `Result<Vec<Bytes32>>` | Collect canonical hashes for all heights in the epoch. Stops at chain tip. Uses `dig_epoch::epoch_height_range`. |
| `set_canonical(&self, hash)` | `&Bytes32` | `Result<()>` | Mark an already-stored block as canonical at its height. Updates CF_CANONICAL + mmap + caches. |
| `set_canonical_batch(&self, hashes)` | `&[Bytes32]` | `Result<()>` | Atomic batch canonicalization via single WriteBatch. |

### BlockStore — Range Queries

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `get_blocks_in_range(&self, start, end)` | `u64, u64` | `Result<Vec<L2Block>>` | Eagerly load canonical blocks in `[start, end]` inclusive. Ascending order. Skips gaps. |
| `get_records_in_range(&self, start, end)` | `u64, u64` | `Result<Vec<BlockRecord>>` | Same range semantics but returns lightweight records (no zstd decompression). |
| `stream_blocks_in_range(&self, start, end)` | `u64, u64` | `StreamBlocksInRange` | Iterator with RocksDB readahead. Yields `Result<L2Block>` per height. Memory-efficient for large ranges. |

### BlockStore — Rollback & Reorg

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `blocks_to_revert(&self, target_height)` | `u64` | `Result<Vec<Bytes32>>` | **Read-only preview.** Returns canonical hashes that WOULD be reverted, descending height order. Empty if `target >= tip` or no tip. |
| `rollback_to_height(&self, target_height)` | `u64` | `Result<Vec<Bytes32>>` | Revert canonical chain. Validates: NoTip, RollbackAboveTip, RollbackBelowMin. Deletes CF_CANONICAL entries, truncates mmap, updates tip, marks records non-canonical. Block data preserved. Returns reverted hashes (descending). |
| `find_common_ancestor(&self, hash, max_depth)` | `&Bytes32, u64` | `Result<Option<(Bytes32, u64)>>` | Walk parent_hash chain backward. Returns first canonical ancestor as `(hash, height)`. `None` if not found within `max_depth` steps. |
| `apply_reorg(&self, ancestor_height, new_chain_hashes)` | `u64, &[Bytes32]` | `Result<ReorgResult>` | **Atomic fork-switch.** Single WriteBatch: delete old canonical entries above ancestor, write new entries, update tip. Post-commit: mmap truncate + rewrite, cache updates. |

### BlockStore — Attestation Storage

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `put_attestation(&self, hash, attested)` | `&Bytes32, &AttestedBlock` | `Result<()>` | Persist attested block in CF_ATTESTED. Overwrites existing. |
| `get_attestation(&self, hash)` | `&Bytes32` | `Result<Option<AttestedBlock>>` | Retrieve attestation by block hash. |

### BlockStore — Checkpoint Storage

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `put_checkpoint(&self, checkpoint)` | `&StoredCheckpoint` | `Result<()>` | Store checkpoint keyed by epoch (big-endian). Idempotent overwrite. |
| `get_checkpoint(&self, epoch)` | `u64` | `Result<Option<StoredCheckpoint>>` | Retrieve checkpoint by epoch. |
| `get_latest_checkpoint(&self)` || `Result<Option<StoredCheckpoint>>` | Reverse iterator on CF_CHECKPOINTS. Returns highest-epoch entry. |
| `get_checkpoints_in_range(&self, start, end)` | `u64, u64` | `Result<Vec<StoredCheckpoint>>` | Forward iterator. Range `[start, end]` inclusive. Ascending epoch order. |

### BlockStore — Snapshot Export/Import

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `export_snapshot(&self, start, end, writer)` | `u64, u64, &mut impl Write` | `Result<SnapshotManifest>` | Write manifest + length-prefixed compressed blocks + SHA-256 checksum. Uses pre-compressed bytes from CF_BLOCKS (no recompression). |
| `import_snapshot(&self, reader)` | `&mut impl Read` | `Result<SnapshotManifest>` | Read manifest, validate version, ingest blocks (height contiguity + parent link checks), verify trailing SHA-256 checksum. |

### BlockStore — Pruning

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `prune_before_height(&self, height)` | `u64` | `Result<usize>` | Delete blocks/headers/attestations/canonical entries below height. Includes non-canonical blocks. Single WriteBatch. Updates `min_retained_height`. Returns count pruned. |
| `prune_checkpoints_before_epoch(&self, epoch)` | `u64` | `Result<usize>` | Delete checkpoints with epoch < target. Returns count pruned. |
| `min_retained_height(&self)` || `Result<u64>` | Pruning floor. Returns 0 if no pruning has occurred. Fast (AtomicU64 read). |

### BlockStore — Statistics & Maintenance

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `stats(&self)` || `Result<StorageStats>` | Aggregate counts: block_count, canonical_block_count, header_count, checkpoint_count, attested_count, tip_height, min_height, total_size_bytes. |
| `flush(&self)` || `Result<()>` | Force WAL flush to disk. |
| `compact(&self)` || `Result<()>` | Trigger manual compaction on all column families. |

### BlockStore — Async Methods

| Method | Input | Output | Description |
|--------|-------|--------|-------------|
| `get_block_async(&self, hash)` | `&Bytes32` | `Result<Option<L2Block>>` | Cache hit served directly; miss dispatched to `spawn_blocking`. |
| `get_header_async(&self, hash)` | `&Bytes32` | `Result<Option<L2BlockHeader>>` | Same pattern as `get_block_async`. |
| `get_block_by_height_async(&self, height)` | `u64` | `Result<Option<L2Block>>` | Always `spawn_blocking` (height→hash uses I/O). |

---

## Data Types

### ChainTip

```rust
pub struct ChainTip {
    pub hash: Bytes32,   // 32-byte block identity hash
    pub height: u64,     // block height
}
```

- `to_bytes() -> [u8; 40]` — Encode: `hash[0..32] || height_LE[32..40]`
- `from_bytes(&[u8]) -> Result<Self>` — Decode from exactly 40 bytes

### BlockRecord

In-memory block metadata. Never persisted to RocksDB. Derived from headers via `from_header`.

```rust
pub struct BlockRecord {
    pub hash: Bytes32,
    pub height: u64,
    pub epoch: u64,
    pub parent_hash: Bytes32,
    pub in_canonical_chain: bool,  // from BlockStatus::is_canonical()
    pub status: BlockStatus,       // Validated | SoftFinalized | HardFinalized | Orphaned | Rejected | Pending
    pub timestamp: u64,
    pub proposer_index: u32,
    pub spend_bundle_count: u32,
    pub total_cost: u64,
    pub total_fees: u64,
    pub additions_count: u32,
    pub removals_count: u32,
    pub block_size: u64,           // 0 until filled by future API
    pub l1_height: u32,
    pub l1_hash: Bytes32,
    pub state_root: Bytes32,
}
```

### StorageStats

```rust
pub struct StorageStats {
    pub block_count: u64,            // all blocks including forks
    pub canonical_block_count: u64,  // main chain only
    pub header_count: u64,
    pub checkpoint_count: u64,
    pub attested_count: u64,
    pub tip_height: Option<u64>,
    pub min_height: Option<u64>,     // pruning floor
    pub total_size_bytes: u64,       // approximate disk footprint
}
```

### ReorgResult

```rust
pub struct ReorgResult {
    pub reverted: Vec<Bytes32>,  // removed from canonical (descending height)
    pub applied: Vec<Bytes32>,   // added to canonical (ascending height)
    pub new_tip: ChainTip,
}
```

### StoredCheckpoint

```rust
pub struct StoredCheckpoint {
    pub checkpoint: Checkpoint,           // epoch, state_root, block_root, counts, fees
    pub signer_bitmap: SignerBitmap,
    pub aggregate_signature: Signature,   // BLS G2
    pub aggregate_pubkey: PublicKey,       // BLS G1
    pub score: u64,
    pub submitter: u32,
    pub l1_height: Option<u32>,
    pub l1_coin_id: Option<Bytes32>,
    pub stored_at: u64,                   // Unix timestamp
}
```

### SnapshotManifest

```rust
pub struct SnapshotManifest {
    pub version: u32,         // current: 1
    pub start_height: u64,
    pub end_height: u64,
    pub block_count: u64,     // must equal end_height - start_height + 1
    pub state_root: Bytes32,
    pub checksum: Bytes32,    // SHA-256 of all block data
}
```

---

## Error Types

```rust
pub enum BlockStoreError {
    RocksDb(rocksdb::Error),                          // RocksDB operation failed
    Serialization(String),                             // bincode/structural failure
    Compression(String),                               // zstd compress/decompress failure
    BlockNotFound(Bytes32),                            // hash not in CF_BLOCKS
    CheckpointNotFound(u64),                           // no checkpoint at epoch
    BlockNotInStore(Bytes32),                          // hash not in store (chain operation)
    RollbackBelowMin { target: u64, min: u64 },       // target < min_retained_height
    RollbackAboveTip { target: u64, tip: u64 },       // target > current tip
    NoTip,                                             // no chain tip set
    SchemaMismatch { expected: u32, found: u32 },      // on-disk version mismatch
    NotInitialized,                                    // store has no genesis
    EmptyReorgChain,                                   // apply_reorg with empty hashes
    PipelineClosed,                                    // async channel closed
}
```

---

## Configuration

All fields of `BlockStoreConfig` with defaults:

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `path` | `PathBuf` | `"data/blockstore"` | RocksDB directory |
| `block_cache_capacity` | `usize` | `1000` | Max blocks in sharded LRU |
| `header_cache_capacity` | `usize` | `2000` | Max headers in sharded LRU |
| `cache_shards` | `usize` | `16` | Shard count (power of 2) |
| `warm_cache_on_open` | `bool` | `true` | Preload recent blocks on open |
| `warm_cache_depth` | `u64` | `64` | Heights to warm from tip |
| `write_buffer_size` | `usize` | `67_108_864` | RocksDB memtable (64 MiB) |
| `block_cache_size` | `usize` | `134_217_728` | RocksDB block cache (128 MiB) |
| `max_open_files` | `i32` | `1000` | File descriptor budget |
| `enable_blob_db` | `bool` | `true` | BlobDB for large block values |
| `compress_blocks` | `bool` | `true` | Application-level zstd |
| `compression_level` | `i32` | `3` | Zstd level |
| `use_compression_dict` | `bool` | `true` | Trained dictionary after 1000 blocks |
| `max_decompressed_block_bytes` | `usize` | `134_217_728` | Decompression bomb cap (128 MiB) |
| `zstd_dictionary_override` | `Option<Vec<u8>>` | `None` | Inject dictionary for tests |
| `write_pipeline_batch_size` | `usize` | `64` | Max blocks per WriteBatch flush |
| `write_pipeline_flush_ms` | `u64` | `100` | Partial batch flush timer |
| `write_pipeline_channel_capacity` | `usize` | `256` | Bounded async channel depth |
| `sync_writes` | `bool` | `false` | WAL sync per write |
| `readahead_size` | `usize` | `2_097_152` | Sequential readahead hint (2 MiB) |
| `canonical_height_cache_capacity` | `usize` | `10_000` | Height→hash BTreeMap entries |
| `hash_to_height_cache_capacity` | `usize` | `10_000` | Hash→height reverse LRU entries |
| `enable_compaction_pruning` | `bool` | `false` | Register compaction filter |
| `min_retained_height` | `Option<u64>` | `None` | Pruning floor (config override) |

---

## Column Families

| Name | Key | Value | Purpose |
|------|-----|-------|---------|
| `blocks` | `hash (32 bytes)` | `zstd-compressed bincode L2Block` | Full block bodies |
| `headers` | `hash (32 bytes)` | `bincode L2BlockHeader` | Block headers (no compression) |
| `canonical` | `height (8 bytes BE)` | `hash (32 bytes)` | Canonical chain height→hash index |
| `metadata` | `UTF-8 string` | `varies` | Tip, genesis hash, schema version, zstd dict, min height |
| `checkpoints` | `epoch (8 bytes BE)` | `bincode StoredCheckpoint` | Consensus checkpoints by epoch |
| `attested` | `hash (32 bytes)` | `bincode AttestedBlock` | Attested block data |

---

## Key Encoding Functions

| Function | Input | Output | Description |
|----------|-------|--------|-------------|
| `hash_key(hash)` | `&Bytes32` | `&[u8; 32]` | Raw 32-byte hash for CF_BLOCKS/CF_HEADERS/CF_ATTESTED keys |
| `height_key(height)` | `u64` | `[u8; 8]` | Big-endian 8-byte key for CF_CANONICAL (preserves sort order) |
| `decode_height_key(key)` | `&[u8; 8]` | `u64` | Decode big-endian height key |
| `epoch_key(epoch)` | `u64` | `[u8; 8]` | Big-endian 8-byte key for CF_CHECKPOINTS |
| `decode_epoch_key(key)` | `&[u8; 8]` | `u64` | Decode big-endian epoch key |
| `metadata_key(name)` | `&str` | `&[u8]` | UTF-8 bytes for CF_METADATA keys |

---

## Wire Format (Chia Streamable)

| Function | Input | Output | Description |
|----------|-------|--------|-------------|
| `block_to_wire_bytes(block)` | `&L2Block` | `Result<Vec<u8>>` | Chia Streamable encoding (big-endian, length-prefixed lists). NOT bincode. |
| `block_from_wire_bytes(bytes)` | `&[u8]` | `Result<L2Block>` | Decode Streamable wire format. Rejects trailing garbage. |

---

## Snapshot Stream Format

```
[ SnapshotManifest (bincode) ]
[ block_0_len: u32 LE ] [ block_0_compressed_bytes ]
[ block_1_len: u32 LE ] [ block_1_compressed_bytes ]
...
[ SHA-256 checksum: 32 bytes ]
```

The checksum covers all bytes from the start of the manifest through the last block (exclusive of the trailing 32-byte checksum itself). Computed via `chia_sha2::Sha256`.

---

## Crate Dependencies

| Crate | Purpose |
|-------|---------|
| `dig-block` | L2Block, L2BlockHeader, BlockStatus, AttestedBlock, Checkpoint types |
| `dig-epoch` | Epoch height range calculation |
| `dig-constants` | Network constants |
| `chia-protocol` | Bytes32 fixed-size hash type |
| `chia-bls` | BLS Signature, PublicKey for checkpoint storage |
| `chia-sha2` | SHA-256 for snapshot checksums |
| `chia-traits` | Streamable trait for wire format |
| `rocksdb` | Storage backend with column families, WriteBatch, iterators |
| `zstd` | Block body compression with optional trained dictionary |
| `bincode` | Fast binary serialization for headers, checkpoints, blocks |
| `serde` | Serialization framework (derive) |
| `thiserror` | Error type derivation |
| `parking_lot` | Fast RwLock/Mutex (no poisoning) |
| `lru` | LRU cache implementation |
| `tokio` | Async runtime for write pipeline and async read APIs |
| `memmap2` | Memory-mapped canonical.bin file |

---

## Thread Safety

`BlockStore` is `Send + Sync + Clone`. All public methods take `&self`.

- **Clone** is cheap (`Arc<Inner>` bump).
- **Concurrent reads** are safe: sharded LRU caches use per-shard `parking_lot::RwLock`.
- **Concurrent writes** are safe: RocksDB handles internal locking; record cache uses `Mutex`.
- **Async** methods (`get_block_async`, `put_pipelined`) are designed for `tokio` runtimes.

All upstream types re-exported from crate root (`L2Block`, `Bytes32`, `BlockStatus`, etc.) — consumers need only `dig-blockstore` in their `Cargo.toml`.