Olympus
System Design

Architecture

Validium architecture — private execution, on-chain verifiability, EVM dual-layer integration, crash recovery, and lock-free reads.

Validium Architecture

Olympus implements a validium-style architecture. The matching engine executes off-chain at full speed (the "private" path), while cryptographic commitments are periodically posted on-chain (the "public" path). On-chain batch roots provide an audit trail — the exchange is operated by a trusted FMI, so verifiability serves institutional audit and regulatory replay rather than decentralized per-trade proof checking.

Validium Architecture

Why validium over a rollup?

A rollup posts full transaction data on-chain for data availability. This limits throughput to what the L1 can absorb. A validium keeps transaction data off-chain and only posts commitments (state roots, trade roots). The on-chain batch roots serve as an audit commitment — regulators and participants can verify aggregate trade inclusion against these roots. This approach sustains hundreds of thousands of orders per second while maintaining a cryptographic audit trail.

Pipeline Overview

Transactions flow through a deterministic pipeline of typed channels:

Pipeline Overview

Each stage is a distinct Rust crate within the workspace. Channels are crossbeam MPSC (multi-producer, single-consumer) for the hot path. The EVM layer provides a second entry point: smart contracts submit trading intents via the CoreWriter system contract, which are extracted after block sealing and injected into the sequencer for core processing.

Components

API (crates/api)

The REST API layer built with Axum. Handles:

  • TradingPOST /api/v1/order for order placement, DELETE /api/v1/order for cancellation
  • Market data/api/v1/depth, /api/v1/exchangeInfo, /api/v1/ticker/bookTicker
  • Account/api/v1/balance, /api/v1/openOrders
  • History/api/v1/myTrades, /api/v1/allOrders, /api/v1/klines (queries Postgres read store, see Data Persistence)
  • Admin — instrument management, bridge control, account crediting
  • WebSocketGET /ws upgrades to WebSocket for real-time market data. Order book diffs and mid prices are published at OLYMPUS_MARKET_DATA_INTERVAL_MS (default 100ms). Trades and account updates are published per-tick (event-driven). See WebSocket docs.
  • Health & metrics/health, /metrics (Prometheus-compatible)

Authentication uses EIP-712 typed data signing (Hyperliquid-compatible phantom agent pattern). In dev mode, the X-Account-Address header bypasses signature verification.

Design decision: lock-free reads. All read handlers (depth, exchangeInfo, bookTicker, balance) read from an Arc<ArcSwap<EngineSnapshot>> instead of locking the engine. The engine thread publishes a new snapshot after every tick via an atomic pointer swap (ArcSwap::store). This means reads are always O(1) with no contention against the writer. The trade-off is that reads may be one tick stale in batch mode, or up to OLYMPUS_SNAPSHOT_INTERVAL_US (default 500µs) in continuous mode. This is acceptable for market data and balance queries.

Sequencer (crates/sequencer)

Provides global ordering for the exchange by assigning monotonically increasing sequence numbers to batches of transactions ("ticks"):

  • Batch mode (OLYMPUS_CONTINUOUS_MATCHING=false): batches transactions at OLYMPUS_TICK_INTERVAL_MS intervals, then flushes to the engine for matching
  • Continuous mode (OLYMPUS_CONTINUOUS_MATCHING=true): the sequencer is bypassed — the ContinuousRunner processes each order individually on arrival (event-driven, microsecond matching latency). OLYMPUS_TICK_INTERVAL_MS controls only the persistence/commitment batch interval, not matching speed.
  • On crash recovery, resumes from append_log.last_sequence() + 1
  • pending_count() feeds the sequencer_pending_transactions Prometheus metric

Core (crates/core)

The matching engine and ledger, running on a dedicated OS thread for predictable latency:

  • Order book (orderbook.rs) — price-time priority CLOB using BTreeMap<Reverse<Price>, VecDeque<Order>> for bids and BTreeMap<Price, VecDeque<Order>> for asks. O(log N) insertion, O(1) best-price peek.
  • Ledger (ledger.rs) — double-entry accounting with three balance states: available, reserved (backing resting orders), locked (bridge operations). All types derive Serialize/Deserialize for snapshot persistence.
  • Merkle tree (merkle.rs) — binary SHA-256 tree built from trade leaf hashes after every tick. The root (trades_root) is included in TickResult and committed on-chain as part of settlement batches. Computed in-memory only (no persistence).
  • Snapshot (snapshot.rs) — EngineSnapshot captures instrument configs, orderbook depth (including last_trade_price, last_trade_qty, last_trade_timestamp_ns), all balances, and per-instrument asset_scales. Published via ArcSwap for lock-free API reads and EVM read precompile access.

Design decision: engine owns itself. The engine thread takes ownership of CoreEngine directly (moved into the closure). There is no Arc<Mutex<CoreEngine>>. This eliminates all lock contention on the hot path. The engine communicates outward in two ways: (1) it sends ProcessedTick to the event processor channel, and (2) it publishes EngineSnapshot via ArcSwap::store().

Design decision: Serialize/Deserialize on all engine internals. CoreEngine, MatchingEngine, OrderBook, and Ledger all derive serde traits. This enables JSON-based engine snapshots for crash recovery. The OrderBook uses BTreeMap<Reverse<Price>, VecDeque<Order>>Reverse<T> implements serde when T does, so this works without custom serialization. If binary size becomes a bottleneck, bincode can replace JSON with no code changes.

Supported order types:

  • Limit (Good-til-Cancel)
  • Market
  • Immediate-or-Cancel (IOC)

Bridge (crates/bridge)

Connects the off-chain matching engine to the on-chain EVM layer:

  • Token Factory — the OlympusTokenFactory contract at 0x0D00 deploys per-asset ERC-20 tokens via CREATE2 (salt = keccak256(symbol)), giving deterministic addresses across restarts. Tokens are named "Olympus {ticker}" with symbol "{ticker}o" (e.g. "Olympus AAPL" / "AAPLo"). Multiple instruments sharing the same base asset (e.g. "AAPL-USD" and "AAPL-BTC") share the same AAPLo token.
  • Token Deployment — deploys tokens via factory.createToken(name, symbol) at startup and when new instruments introduce new assets
  • Mint — calls factory.mint(token, recipient, amount) (proxied to child token) after asset lock
  • Unlock — calls factory.burnFrom(token, user, amount) (proxied to child token) when assets are unlocked
  • SettlementSettlementService batches tick results and commits state/trade roots on-chain
  • Token Registry — in-memory TokenRegistry mapping asset (e.g. "AAPL") to deterministic CREATE2 token addresses
  • Persistent SignerOLYMPUS_BRIDGE_SIGNER_KEY env var provides a persistent private key for restart-stable EVM state

See Bridge & Settlement for detailed mechanics.

Settlement Contract (contracts/Settlement.sol)

A minimal Solidity contract injected at genesis at 0x0B00; initialized by the bridge signer at startup:

function commitBatch(
    uint64 fromSeq, uint64 toSeq,
    bytes32 stateRoot, bytes32 tradesRoot,
    uint64 tradeCount
) external onlyOperator;

Emits indexed BatchCommitted events that Blockscout automatically discovers. Public stateRoots(uint64) and tradesRoots(uint64) mappings allow anyone to verify a committed root.

EVM (crates/evm)

An embedded reth execution environment running a Hyperliquid-style dual-layer architecture. The EVM layer operates alongside the core engine, providing bidirectional communication:

  • Block production for bridge, settlement, and CoreWriter transactions
  • JSON-RPC on port 8545
  • Chain ID 1337
  • Effectively zero gas fees (base fee = 0; max_fee_per_gas = 7 wei to satisfy reth tx pool minimum)

Read path — precompiles (crates/evm/src/precompiles/reader.rs): Eight custom REVM precompiles at addresses 0x08000x0807 provide lock-free reads of aggregate market data from the engine's EngineSnapshot. Precompiles access the same Arc<ArcSwap<EngineSnapshot>> used by API read handlers. All monetary values are converted from i64 native scale to U256 with 18 decimals for EVM compatibility. Each precompile costs 1 gas (nominal). The OlympusReader contract at 0x0C00 provides typed Solidity wrappers around all 8 precompiles, deployed at genesis alongside CoreWriter — developers can call OlympusReader(0x0C00).getBBO("AAPL-USD") directly without library linking.

AddressPrecompileData
0x0800GetBBOBest bid/ask price and size
0x0801GetDepthSnapshotConfigurable depth levels
0x0802GetMidPrice(bid + ask) / 2
0x0803GetLastTradePrice, quantity, timestamp
0x0804GetOHLCVStub (returns zeros — kline snapshot integration pending)
0x0805GetInstrumentConfigTick size, lot size, base/quote assets, status
0x0806GetActiveInstrumentsList of non-halted instruments
0x0807GetBridgeStatusBridge active boolean

Write path — CoreWriter (crates/evm/src/core_writer.rs): The OlympusCoreWriter system contract at 0x0A00 accepts trading intents (placeOrder, cancelOrder, cancelReplace, lockAsset, unlockAsset). Calls emit event logs. After the EVM block is sealed, CoreWriterExtractor (crates/evm/src/core_writer_extractor.rs) filters logs by the CoreWriter address and decodes them into core Transaction objects. The decoder converts U256 18-decimal values back to i64 native scale. msg.sender becomes the core AccountId directly (both are 20-byte addresses).

Result path — system events (crates/evm/src/system_events.rs): After the core engine processes CoreWriter-originated transactions, results (CoreWriterResult) are encoded as EVM event logs from system address 0x0900 and injected into the next block. Events include OrderAccepted, OrderRejected, OrderFilled, AssetLockConfirmed, and AssetUnlockConfirmed.

Privacy boundary: Only aggregate market data crosses from core to EVM. Individual orders, positions, and account balances never enter EVM state. See EVM Dual-Layer Integration for full details.

Store (crates/store)

RocksDB-based persistence through a unified Store struct that opens a single DB handle:

  • AppendLog — durable tick log in CF_SEQUENCER_LOG. Every tick is persisted before any other processing. Supports replay_from(sequence) for crash recovery.
  • SnapshotStore — periodic engine snapshots in CF_SNAPSHOTS. Serialized CoreEngine state for fast recovery without full replay. Design decision: single DB handle. Both AppendLog and SnapshotStore previously called open_db() independently, which fails because RocksDB allows only one handle per path. The Store struct opens once and hands out Arc<DB> to each sub-store. This also means all column families are created atomically at startup.

Common (crates/common)

Shared types, error definitions, and Prometheus metric handles used across all crates. Key types:

  • ProcessedTick — pairs the input Tick with its TickResult for deterministic replay
  • Tick — includes a source: TransactionSource field (Api or CoreWriter) to distinguish API-submitted vs. EVM-originated transactions
  • TickResult — includes trades_root: [u8; 32] (merkle root over trades) and core_writer_results: Vec<CoreWriterResult> for routing results back to the EVM as system events
  • CoreWriterResult — captures fill price, fill quantity, remaining quantity, rejection reason, and other fields needed to encode system events for CoreWriter-originated transactions

Threading Model

Main Thread
+-- Tokio runtime
|   +-- API server (Axum -- serves REST + WebSocket + metrics)
|   +-- Sequencer task (batch mode only -- configurable tick flush)
|   +-- Event processor task
|   |   +-- Persist tick to RocksDB append log
|   |   +-- Settlement service (batch + commit to chain)
|   |   +-- Bridge signing (mint/unlock > reth txpool)
|   |   +-- Inject CoreWriterResults as system events into next EVM block
|   +-- CoreWriter extractor task
|       +-- Extracts CoreWriter logs from sealed EVM blocks
|       +-- Decodes logs into core Transactions (U256 > i64 scale conversion)
|       +-- Injects decoded transactions into sequencer
+-- Core engine thread (dedicated OS thread)
|   +-- Batch mode: receives ticks from sequencer, matches, publishes snapshot after each tick
|   +-- Continuous mode: receives transactions, matches individually
|       +-- Debounced snapshot publish (every OLYMPUS_SNAPSHOT_INTERVAL_US us)
|       +-- Batched broadcast sends (flushed with snapshot)
|       +-- Busy-spin recv loop (OLYMPUS_SPIN_ITERS spins before blocking)
|       +-- Commitment timer batches results for hasher + persistence
+-- Hasher thread (dedicated OS thread)
|   +-- Receives PendingHash from engine (crossbeam channel)
|   +-- Computes Blake3 state hash + merkle trades root
|   +-- Sends ProcessedTick to event processor
+-- EVM (reth node)
    +-- Block production, JSON-RPC server
    +-- Read precompiles (0x0800-0x0807) -- lock-free reads from ArcSwap<EngineSnapshot>
    +-- CoreWriter contract (0x0A00) -- emits event logs for trading intents
    +-- System events (0x0900) -- injected fills/rejects/acks from core engine

The core matching engine runs on a dedicated OS thread to avoid interference from async I/O, providing sub-millisecond matching latency. The engine thread is the sole writer — it owns CoreEngine and communicates only through channels and atomic snapshot publishing. The EVM layer feeds back into the pipeline: the CoreWriter extractor pulls trading intents from sealed blocks and injects them into the sequencer, while the event processor pushes CoreWriterResult data back to the EVM as system events. This creates a closed loop with a 1-block/tick delay on the write path.

Data Flow

Batch Mode (default)

  1. Client submits a signed order to POST /api/v1/order
  2. API verifies the EIP-712 signature, validates instrument config from the snapshot, creates a Transaction
  3. Transaction is sent to the sequencer channel (crossbeam unbounded MPSC)
  4. Sequencer assigns a monotonic sequence number and batches it into the current tick
  5. On tick flush (every OLYMPUS_TICK_INTERVAL_MS), the Tick is sent to the core engine channel
  6. Core engine processes orders via match_tick(): matches against the book, updates the ledger
  7. Core publishes a new EngineSnapshot via ArcSwap::store() and broadcasts market data + persistence data
  8. Core sends PendingHash { tick, match_result } to the hasher thread
  9. Hasher computes Blake3 state hash + merkle trades root, produces ProcessedTick
  10. Event processor persists the tick to the RocksDB append log
  11. Settlement service accumulates results; when batch threshold is reached, commits on-chain
  12. Bridge processes BridgeInstruction::Mint events, signs EVM transactions, submits to reth

Continuous Mode

  1. Client submits an order to POST /api/v1/order
  2. API creates a Transaction and sends it directly to the engine channel (bypasses sequencer)
  3. Engine drains all available transactions via try_recv and matches each immediately via match_order()
  4. Results are accumulated into broadcast buffers and commitment buffers
  5. On snapshot debounce interval (OLYMPUS_SNAPSHOT_INTERVAL_US): engine publishes EngineSnapshot, flushes batched market data + persistence broadcasts
  6. On commitment interval (OLYMPUS_TICK_INTERVAL_MS): engine batches accumulated transactions into a Tick and sends PendingHash to the hasher thread
  7. Steps 9-12 from batch mode apply identically

EVM-Originated Transactions (CoreWriter Path)

  1. Smart contract calls CoreWriter.placeOrder() (or cancelOrder, lockAsset, etc.) at address 0x0A00
  2. The CoreWriter contract validates inputs and emits an event log
  3. reth seals the EVM block
  4. CoreWriter extractor filters the block's logs for the 0x0A00 address
  5. Each matching log is decoded into a core Transaction — U256 18-decimal values are converted back to i64 native scale using the instrument's asset_scales; msg.sender becomes the AccountId
  6. Decoded transactions are injected into the sequencer with source: CoreWriter
  7. Core engine processes them in the next tick (same matching logic as API-submitted orders)
  8. TickResult includes core_writer_results for any CoreWriter-originated transactions
  9. Event processor encodes results as system events (OrderAccepted, OrderRejected, OrderFilled, AssetLockConfirmed, AssetUnlockConfirmed) and injects them from system address 0x0900 into the next EVM block

The 1-block/tick delay between steps 2 and 9 is inherent — it protects the core engine from EVM execution timing and preserves deterministic tick ordering.

In parallel, two broadcast channels feed downstream consumers:

  • Market data broadcast (TickMarketData) — trades stripped of PII feed the WebSocket publisher and kline aggregator
  • Persistence broadcast (TickPersistenceData) — full trades + enriched order events feed the Postgres writer task

See Data Persistence for the complete read store architecture.

Crash Recovery

On startup, the system recovers state without data loss:

  1. Load latest snapshotsnapshot_store.load_latest_snapshot() returns the most recent serialized CoreEngine, if any
  2. Replay from logappend_log.replay_from(snapshot_seq + 1) deserializes and replays all ticks after the snapshot
  3. Resume sequencerappend_log.last_sequence() + 1 becomes the next sequence number, ensuring no sequence gaps
  4. Publish initial snapshot — An EngineSnapshot is created from the recovered engine before the API starts serving

If no snapshot exists, the entire append log is replayed from sequence 0. This is correct but slower for long histories — hence the periodic snapshot at every 1000 ticks.

Trades Root

Every TickResult with trades computes a trades_root in-memory:

trades_root = MerkleTree::from_leaves(trades.map(hash_trade)).root()

hash_trade deterministically hashes: sequence, instrument_id, price, quantity, buyer_account, seller_account, buyer_order_id, seller_order_id, timestamp_ns.

The trades_root is included in TickResult and committed on-chain as part of settlement batches via commitBatch(). This provides an aggregate cryptographic commitment for audit purposes. Merkle trees are not persisted — they are computed in-memory per tick and discarded after the root is extracted.

Storage

Column FamilyContentsPurpose
sequencer_logSerialized Tick objectsDeterministic replay for crash recovery
snapshotsSerialized CoreEngine stateFast recovery checkpoint
accounts(reserved)Future use
orderbooks(reserved)Future use
evm_state(reserved)Future use
evm_blocks(reserved)Future use
bridge_state(reserved)Future use

All column families use LZ4 compression.

Metrics

Prometheus metrics are exposed at GET /metrics and scraped by the bundled Prometheus instance.

Core engine

MetricTypeDescription
matching_latency_nsHistogramTime to process one tick (nanoseconds)
orders_totalCounterTotal orders submitted
trades_totalCounterTotal trades executed
ticks_processed_totalCounterTotal ticks processed

Sequencer

MetricTypeDescription
sequencer_pending_transactionsGaugeTransactions buffered in the current tick
sequencer_latest_sequenceGaugeLatest tick sequence number

Settlement

MetricTypeDescription
settlement_commits_totalCounterBatches committed on-chain
settlement_latest_sequenceGaugeLatest committed tick sequence
settlement_batch_sizeHistogramTicks per settlement batch

Continuous matching

MetricTypeDescription
continuous_orders_matched_totalCounterTotal orders matched in continuous mode
snapshot_debounce_batch_sizeHistogramOrders accumulated between snapshot publishes

Persistence & snapshots

MetricTypeDescription
snapshot_publish_latency_nsHistogramTime to publish engine snapshot via ArcSwap
tick_log_write_latency_nsHistogramTime to write one tick to RocksDB

Bridge & EVM

MetricTypeDescription
bridge_mints_totalCounterBridge mint operations
bridge_invariant_deltaGaugeWrapped supply minus locked balance (must be 0)
evm_block_numberGaugeCurrent EVM block height
evm_block_time_nsHistogramTime to produce one EVM block

API

MetricTypeDescription
http_requests_totalCounterTotal HTTP requests
ws_connections_activeGaugeActive WebSocket connections (future)

Environment Variables

VariableDefaultDescription
OLYMPUS_BRIDGE_SIGNER_KEY(unset)Hex-encoded private key for the bridge signer. If unset, a random key is generated (EVM state won't survive restarts)
OLYMPUS_ENV(unset)Set to dev for dev mode (port 3000, relaxed auth)
OLYMPUS_LISTEN_ADDR0.0.0.0:3000 (dev) / 0.0.0.0:8080 (prod)API listen address
OLYMPUS_ADMIN_KEY(unset)Required for admin endpoints
OLYMPUS_DATA_DIR./data/olympus-chainRocksDB data directory
DATABASE_URL(unset)Postgres connection string for read store. If unset, history endpoints return 503
OLYMPUS_CONTINUOUS_MATCHINGfalseEnable continuous matching mode (bypasses sequencer)
OLYMPUS_TICK_INTERVAL_MS1Tick/commitment interval in milliseconds
OLYMPUS_SNAPSHOT_INTERVAL_US500Microseconds between snapshot publishes in continuous mode
OLYMPUS_SPIN_ITERS256Busy-spin iterations before blocking recv in continuous mode. Set to 0 for shared-core environments
OLYMPUS_ENGINE_CORE(unset)Pin engine thread to CPU core ID
OLYMPUS_HASHER_CORE(unset)Pin hasher thread to CPU core ID
BLOCKSCOUT_API(unset)Blockscout backend URL for automatic contract verification at startup. If unset, verification is skipped. Docker: http://blockscout-backend:4000. Native: http://localhost:4001
SIM_SETTLEMENT_BATCH_SIZE100Ticks per settlement batch
SIM_SETTLEMENT_BATCH_AGE_MS5000Max batch age before forced flush

On this page