dig-coinstore
Persistent global coin state database for the DIG Network L2 blockchain.
Manages the authoritative database of all spent and unspent coins using the coinset model (UTXO-like). Accepts validated blocks, applies state transitions, and provides a rich query API for coin lookups by ID, puzzle hash, hint, parent, and height.
Crate boundary: Input = pre-validated BlockData. Output = CoinRecords, CoinStates, state roots, Merkle proofs. This crate does NOT run CLVM, produce blocks, or manage the mempool.
Derived from Chia's CoinStore and HintStore, with improvements: Merkle-committed state, embedded KV storage, snapshot/restore, tiered archival, and in-memory caching.
Quick Start
use CoinStore;
use ;
// Open or create the coinstate database.
let mut store = new?;
// Bootstrap the chain with genesis coins.
let genesis_coin = new;
let state_root = store.init_genesis?;
// Apply a block (pre-validated additions, removals, hints).
let result = store.apply_block?;
println!;
// Query coins.
let record = store.get_coin_record?;
let by_puzzle = store.get_coin_records_by_puzzle_hash?;
// Rollback for chain reorganization.
let rollback = store.rollback_to_block?;
Storage Backends
Feature-gated at compile time:
| Feature | Backend | Default | Notes |
|---|---|---|---|
rocksdb-storage |
RocksDB | Yes | LSM tree, bloom filters, write-optimized |
lmdb-storage |
LMDB | No | Memory-mapped, read-optimized |
full-storage |
Both | No | LMDB preferred when both enabled |
[]
= "0.1" # RocksDB (default)
= { = "0.1", = ["lmdb-storage"] } # LMDB
Re-exported Chia Types
These types are re-exported at the crate root so consumers don't need direct chia-protocol or dig-clvm dependencies:
| Type | Source | Description |
|---|---|---|
Coin |
chia-protocol via dig-clvm |
Coin identity: parent_coin_info, puzzle_hash, amount. coin_id() = sha256(parent || puzzle_hash || amount) |
Bytes32 |
chia-protocol via dig-clvm |
32-byte hash for coin IDs, puzzle hashes, block hashes, state roots |
CoinState |
chia-protocol via dig-clvm |
Lightweight sync view: coin, created_height: Option<u32>, spent_height: Option<u32> |
CoinStateFilters |
chia-protocol |
Batch query filters: include_spent, include_unspent, include_hinted, min_amount |
Core Types
CoinRecord
Full lifecycle state of one coin in the store. Persists after spending for history and rollback.
Key methods:
CoinRecord::new(coin, confirmed_height, timestamp, coinbase)— new unspent coinis_spent() -> bool—spent_height.is_some()spend(height)— mark spentcoin_id() -> CoinId— delegates toCoin::coin_id()to_coin_state() -> CoinState— lightweight sync viewfrom_chia_coin_record(ChiaCoinRecord) -> Self— Chia interop (import)to_chia_coin_record() -> ChiaCoinRecord— Chia interop (export)
BlockData
Input to apply_block(). Pre-extracted state changes from a validated block.
CoinAddition
Constructor: CoinAddition::from_coin(coin, same_as_parent) — computes coin_id via Coin::coin_id().
ApplyBlockResult
Returned on successful apply_block().
RollbackResult
Returned on successful rollback_to_block().
CoinStoreStats
Aggregated chain metrics from stats().
CoinStoreSnapshot
Serializable checkpoint for fast sync / backup / restore.
Type Aliases
pub type CoinId = Bytes32; // sha256(parent || puzzle_hash || amount)
pub type PuzzleHash = Bytes32; // sha256(serialized CLVM puzzle)
CoinStoreError
All fallible methods return Result<T, CoinStoreError>. Variants:
| Variant | Trigger | Fields |
|---|---|---|
HeightMismatch |
block.height != current + 1 |
expected: u64, got: u64 |
ParentHashMismatch |
block.parent_hash != tip_hash |
expected: Bytes32, got: Bytes32 |
StateRootMismatch |
Computed root != expected_state_root |
expected: Bytes32, computed: Bytes32 |
CoinNotFound |
Removal references missing coin | CoinId |
CoinAlreadyExists |
Addition duplicates existing coin | CoinId |
DoubleSpend |
Removal references already-spent coin | CoinId |
SpendCountMismatch |
Updated rows != expected removals | expected: usize, actual: usize |
InvalidRewardCoinCount |
Wrong coinbase count for height | expected: String, got: usize |
HintTooLong |
Hint > 32 bytes | length: usize, max: usize |
GenesisAlreadyInitialized |
Double init_genesis() call |
— |
NotInitialized |
Operation before init_genesis() |
— |
RollbackAboveTip |
target_height > current_height |
target: i64, current: u64 |
PuzzleHashBatchTooLarge |
Batch query exceeds limit | size: usize, max: usize |
StorageError |
Backend I/O failure | String |
SerializationError |
Bincode encode failure | String |
DeserializationError |
Bincode decode failure | String |
Public API Reference
Construction
Block Application
Rollback
Coin Queries
Aggregate Queries
Chain State
Hint Store
/// Standalone hint validation.
;
pub const MAX_HINT_LENGTH: usize = 32;
Snapshot / Restore
Configuration
let config = default // or CoinStoreConfig::default_with_path("./data")
.with_backend // or StorageBackend::Lmdb
.with_storage_path
.with_max_snapshots // auto-prune older
.with_max_query_results // batch query cap
.with_lmdb_map_size // 10 GiB
.with_rocksdb_write_buffer_size
.with_rocksdb_max_open_files
.with_bloom_filter; // 10 bits/key, ~1% FP
let store = with_config?;
Storage Schema
12 column families (RocksDB) / named databases (LMDB):
| CF | Key | Value | Purpose |
|---|---|---|---|
coin_records |
coin_id (32B) |
bincode CoinRecord |
Primary store |
coin_by_puzzle_hash |
puzzle_hash || coin_id (64B) |
coin_id |
Puzzle hash index |
unspent_by_puzzle_hash |
puzzle_hash || coin_id (64B) |
empty | Unspent-only index |
coin_by_parent |
parent_id || coin_id (64B) |
coin_id |
Parent index |
coin_by_confirmed_height |
height_BE || coin_id (40B) |
coin_id |
Creation height index |
coin_by_spent_height |
height_BE || coin_id (40B) |
coin_id |
Spend height index |
hints |
coin_id || hint (up to 64B) |
empty | Forward hint index |
hints_by_value |
hint || coin_id (up to 64B) |
empty | Reverse hint index |
merkle_nodes |
level(1B) || path(32B) |
hash (32B) |
Merkle internal nodes |
archive_coin_records |
coin_id (32B) |
bincode CoinRecord |
Archived spent coins |
state_snapshots |
height_BE (8B) |
bincode CoinStoreSnapshot |
Checkpoints |
metadata |
string key | bytes | Chain tip, counters, config |
All height keys use big-endian encoding so lexicographic byte comparison matches numeric order.
Merkle Tree
256-level sparse Merkle tree over all coin records, providing cryptographic state commitment.
- Leaf hash:
SHA256(0x00 || coin_record_bytes)— domain-separated - Node hash:
SHA256(0x01 || left || right)— domain-separated - Empty subtree: pre-computed for all 257 levels via
OnceLock(O(1) lookup) - Deferred root recomputation: mutations mark the tree dirty; root computed lazily on
root()call - Proof generation:
SparseMerkleProofwith 256 sibling hashes for inclusion/exclusion proofs - Proof verification: static computation — no tree state needed, only proof + trusted root
apply_block Pipeline
Phase 1 — Validation (no writes):
- Height continuity:
block.height == current + 1 - Parent hash:
block.parent_hash == tip_hash - Reward coins: ≥ 2 coinbase for non-genesis
- Removals: each exists and is unspent
- Additions: no duplicates
- Hints: length ≤ 32 bytes
Phase 2 — Mutation (atomic WriteBatch):
7. Insert coinbase + addition records with FF-eligible tracking
8. Mark removals as spent
9. Verify expected_state_root if provided
10. Store hints in forward + reverse indices
11. Batch update Merkle tree
12. Commit chain tip (height, hash, timestamp)
Phase 3 — Observability: 13. Log warning if elapsed > 10 seconds
Atomicity: If any validation fails, no mutations occur. If Phase 2 fails, the WriteBatch is not committed.
rollback_to_block Pipeline
- Delete coins confirmed after target height (all indices)
- Clean up hints for deleted coins (forward + reverse)
- Un-spend coins spent after target height (clear
spent_height, re-add to unspent index) - Recompute FF-eligible for un-spent coins (parent EXISTS check)
- Rebuild Merkle tree (batch remove + update)
- Atomic commit via WriteBatch
Negative target_height triggers full reset to height 0.
Thread Safety
CoinStore is Send + Sync. The Rust borrow checker enforces shared reads (&self) and exclusive writes (&mut self) at compile time. For runtime concurrency via Arc<RwLock<CoinStore>>, parking_lot::RwLock is recommended.
Specification
Full specification: docs/resources/SPEC.md
Requirements: docs/requirements/IMPLEMENTATION_ORDER.md
81 requirements across 9 phases, verified by 493 tests in 75 test files.