Skip to content

Storage

Kaizen uses a dual storage architecture:

  1. RocksDB + JMT: Current state for execution, Merkle proofs
  2. PostgreSQL: Event history for queries and analytics

Storage Architecture

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:

  1. RocksDB Friendly: Efficient batch updates. Good for bundling multiple state changes every 100ms.
  2. Proof Size Optimization: Smaller proof size than Sparse Merkle Tree (256 hashes vs variable).
  3. Production Tested: Running at thousands of TPS in Aptos.

Comparison with Alternatives

TreeProsCons
Sparse MTSimple implementationLarge proof size (always 256 hashes)
Patricia TrieEthereum compatibleComplex implementation, rebalancing overhead
JMTBatch optimized, efficient proofsFew references outside Aptos
Verkle TreeSmallest proofsRequires KZG, complex

Column Families

RocksDB uses a small number of column families:

Column FamilyDescription
jmt_nodesJMT tree node data
jmt_valuesJMT leaf values (all state data)
metaMetadata (current version, etc.)
blocksBlock data (headers + transactions)
block_hashesBlock height → hash mapping

State Keys (JMT)

All application state is stored in the JMT using typed StateKey prefixes:

Key TypeDescription
BalanceAccount balance
ApiWalletsAPI wallet list for account
ProcessedTxProcessed transaction (replay check)
RfqThesisRFQ thesis data
UserThesesThesis IDs by user
SolverThesesThesis IDs by solver
ActiveThesesList of active thesis IDs
OraclePairConfigOracle pair configuration
OracleLatestLatest price per pair
OracleRingBufferPrice history (ring buffer slot)
MarketOiMarket open interest
UserOiUser open interest
UserLimitsPer-user trading limits
GlobalConfigGlobal configuration
BlacklistBlacklisted addresses
Admin/Feeder/RelayerRole assignments
WithdrawalRequestPending 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 hours

Storage 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:

  1. Borsh serialized - Deterministic binary encoding
  2. 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

WAL Write Path

This ensures:

  1. Durability: Event in WAL before response
  2. Performance: Batched PostgreSQL writes
  3. Recovery: WAL replay on crash

When to Use Which?

Use CaseStorageReason
TX executionRocksDBFast state reads, deterministic
Merkle proofsRocksDB (JMT)Tree structure required
User thesis historyPostgreSQLComplex queries
Analytics dashboardPostgreSQLAggregations
Price chartsPostgreSQLTime-series queries
Block syncRocksDBBlock-by-block replay

See Event Indexer for detailed PostgreSQL schema and usage.