Storage
Kaizen uses a dual storage architecture:
- RocksDB + JMT: Current state for execution, Merkle proofs
- PostgreSQL: Event history for queries and analytics
RocksDB State Storage
Storage is abstracted via the kaizen_core::VersionedStorage trait. This trait defines version-based state management, commits, and state root calculation.
pub trait VersionedStorage: Send + Sync {
fn version(&self) -> u64;
fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, CoreError>;
fn get_at_version(&self, key: &[u8], version: u64) -> Result<Option<Vec<u8>>, CoreError>;
fn set(&mut self, key: &[u8], value: Vec<u8>) -> Result<(), CoreError>;
fn delete(&mut self, key: &[u8]) -> Result<(), CoreError>;
fn commit(&mut self) -> Result<B256, CoreError>; // Returns state_root
fn state_root(&self) -> B256;
}Default implementation is RocksDbStorage, with InMemoryStorage available for testing.
Why RocksDB + JMT?
RocksDB
High-performance key-value store from Facebook. LSM-tree based with good write performance, widely used in blockchains.
JMT (Jellyfish Merkle Tree)
Merkle Tree developed by Aptos. Selection reasons:
- RocksDB Friendly: Efficient batch updates. Good for bundling multiple state changes every 100ms.
- Proof Size Optimization: Smaller proof size than Sparse Merkle Tree (256 hashes vs variable).
- Production Tested: Running at thousands of TPS in Aptos.
Comparison with Alternatives
| Tree | Pros | Cons |
|---|---|---|
| Sparse MT | Simple implementation | Large proof size (always 256 hashes) |
| Patricia Trie | Ethereum compatible | Complex implementation, rebalancing overhead |
| JMT | Batch optimized, efficient proofs | Few references outside Aptos |
| Verkle Tree | Smallest proofs | Requires KZG, complex |
Column Families
RocksDB uses a small number of column families:
| Column Family | Description |
|---|---|
jmt_nodes | JMT tree node data |
jmt_values | JMT leaf values (all state data) |
meta | Metadata (current version, etc.) |
blocks | Block data (headers + transactions) |
block_hashes | Block height → hash mapping |
State Keys (JMT)
All application state is stored in the JMT using typed StateKey prefixes:
| Key Type | Description |
|---|---|
Balance | Account balance |
ApiWallets | API wallet list for account |
ProcessedTx | Processed transaction (replay check) |
RfqThesis | RFQ thesis data |
UserTheses | Thesis IDs by user |
SolverTheses | Thesis IDs by solver |
ActiveTheses | List of active thesis IDs |
OraclePairConfig | Oracle pair configuration |
OracleLatest | Latest price per pair |
OracleRingBuffer | Price history (ring buffer slot) |
MarketOi | Market open interest |
UserOi | User open interest |
UserLimits | Per-user trading limits |
GlobalConfig | Global configuration |
Blacklist | Blacklisted addresses |
Admin/Feeder/Relayer | Role assignments |
WithdrawalRequest | Pending withdrawal data |
Oracle Ring Buffer
Oracle prices come in every 100ms. Storing all history in state would be too large, so Ring Buffer keeps only recent data in state.
Ring Buffer Size: 65536 slots
Slot Duration: 100ms
Total Duration: 65536 × 100ms = 6553.6 seconds ≈ 1.8 hoursStorage per Market
- 16 bytes per slot (timestamp 8 + price 8)
- 65536 × 16 = 1MB per market
- 50 markets = 50MB
Small enough to not burden RocksDB.
Why Ring Buffer is Needed
For breach detection, must verify price at specific points. For example, to verify "price was out of range 10 minutes ago", that timestamp's data must exist.
What if data outside Ring Buffer is needed?
Fetch and backfill from external oracle (Pyth, etc.). This is public data anyway, so it can be fetched anytime.
JMT Key Hashing
State keys use typed StateKey enums that are:
- Borsh serialized - Deterministic binary encoding
- SHA256 hashed - 256-bit JMT
KeyHash
impl StateKey {
pub fn hash(&self) -> KeyHash {
let bytes = borsh::to_vec(self).expect("serialization");
let hash = sha2::Sha256::digest(&bytes);
KeyHash(hash.into())
}
}This approach:
- Type-safe: Rust enum prevents key collisions
- Deterministic: Borsh ensures consistent serialization
- Uniform distribution: SHA256 hash spreads keys evenly in JMT
PostgreSQL Event Storage
For queries and analytics, events are stored in PostgreSQL using the Event Indexer.
Event Sourcing
Events contain all fields needed to reconstruct state, enabling:
- Complex queries (JOINs, aggregations)
- Historical analytics
- Audit trails
- Full-text search
Write Path with WAL
This ensures:
- Durability: Event in WAL before response
- Performance: Batched PostgreSQL writes
- Recovery: WAL replay on crash
When to Use Which?
| Use Case | Storage | Reason |
|---|---|---|
| TX execution | RocksDB | Fast state reads, deterministic |
| Merkle proofs | RocksDB (JMT) | Tree structure required |
| User thesis history | PostgreSQL | Complex queries |
| Analytics dashboard | PostgreSQL | Aggregations |
| Price charts | PostgreSQL | Time-series queries |
| Block sync | RocksDB | Block-by-block replay |
See Event Indexer for detailed PostgreSQL schema and usage.
