Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Storage Architecture

UTS uses a three-layer storage strategy, choosing the right technology for each workload’s access pattern and durability requirements.

Storage Overview

LayerTechnologyComponentPurposeData Stored
JournalRocksDBCalendar ServerHigh-throughput WALPending digest commitments
KV StoreRocksDBCalendar ServerMerkle tree storageTrees + leaf→root mappings
SQL StoreSQLiteCalendar Server (Stamper)Attestation metadataPending attestations + attempt history
Relayer DBSQLiteRelayer ServiceEvent indexing + batch stateCursors, batches, costs, event logs

Journal (RocksDB)

Purpose: Durable buffer between HTTP handler and stamper.

Access pattern: Append-only writes, sequential reads, bulk deletes.

RocksDB is ideal here because:

  • Synchronous writes guarantee durability before HTTP response.
  • Sequential key layout (monotonic u64 indices) enables efficient range scans.
  • Bulk deletes on commit are efficient via RocksDB’s compaction.

Column families:

  • entries — entry data keyed by write index.
  • metawrite_index and consumed_index metadata.

Capacity: Configurable (default: 1,048,576 entries). Back-pressure via Error::Full when capacity is reached.

See Journal / WAL for implementation details.

KV Store (RocksDB)

Purpose: Store Merkle trees and leaf→root mappings for proof retrieval.

Access pattern: Point lookups by 32-byte hash keys.

Two types of entries:

KeyValueSize
Leaf hash (32B)Root hash (32B)64 bytes per entry
Root hash (32B)Serialized treeVariable (depends on leaf count)

The KV store uses RocksDB’s default column family with DB::open_default(). Trees are serialized as raw byte arrays via MerkleTree::as_raw_bytes() for zero-copy storage and retrieval.

Retrieval logic (DbExt trait):

  • get_root_for_leaf(leaf): Returns the root hash for a given commitment.
  • load_trie(root): Deserializes the full Merkle tree for proof generation.

For single-leaf trees, the leaf itself is the root — the tree serialization is stored directly and detected by value length (≠ 32 bytes).

SQL Store — Stamper (SQLite)

Purpose: Track attestation lifecycle and transaction attempts.

Schema:

-- Pending attestation records
CREATE TABLE pending_attestations (
    id          INTEGER PRIMARY KEY,
    trie_root   TEXT NOT NULL,
    created_at  INTEGER NOT NULL,
    updated_at  INTEGER NOT NULL,
    result      TEXT NOT NULL DEFAULT 'pending'
    -- result: 'pending' | 'success' | 'max_attempts_exceeded'
);

-- Individual transaction attempts
CREATE TABLE attest_attempts (
    id              INTEGER PRIMARY KEY,
    attestation_id  INTEGER NOT NULL REFERENCES pending_attestations(id),
    chain_id        TEXT NOT NULL,
    tx_hash         TEXT,
    block_number    TEXT,
    created_at      INTEGER NOT NULL
);

Encoding: Large integers and 32-byte hashes are stored as text (hex or decimal strings) via a TextWrapper<T> pattern to improve human-readability. The performance impact is minimal and hence is an acceptable trade-off.

Relayer DB (SQLite)

Purpose: Event indexing, batch lifecycle management, and cost tracking.

The relayer database is more complex, with 10 tables across 3 migrations:

Indexer Tables

indexer_cursors       -- Track last-indexed block per event type
eth_block             -- Block metadata for indexed events
eth_transaction       -- Transaction metadata
eth_log               -- Log metadata

These four tables form a normalized chain of custody: block → transaction → log → event-specific table.

Event Tables

l1_anchoring_queued   -- L1AnchoringQueued events (user submissions)
l1_batch_arrived      -- L1BatchArrived events (cross-chain notifications)
l1_batch_finalized    -- L1BatchFinalized events (batch completions)

Batch Management

l1_batch              -- Batch lifecycle state machine
                      -- Columns: start_index, count, root, l1_tx_hash, l2_tx_hash, status
                      -- Status: Collected → L1Sent → L1Mined → L2Received
                      --       → L2FinalizeTxSent → L2Finalized

Cost Tracking

tx_receipt            -- Gas usage and pricing per transaction
batch_fee             -- Per-batch cost breakdown (L1 gas, L2 gas, cross-chain fee)

Why This Split?

ConcernRocksDBSQLite
High-throughput sequential writesExcellentAdequate
Point lookups by hashExcellentGood (with index)
Complex queries (JOINs, aggregations)Not supportedExcellent
Relational integrity (foreign keys)Not supportedBuilt-in
Schema evolution (migrations)ManualSQLx migrations

RocksDB handles the hot path (journal writes, tree storage) where throughput matters. SQLite handles the metadata path (attestation tracking, event indexing) where query flexibility matters.

This split avoids forcing either technology into a role it’s not designed for.