A unified Solana indexer and RPC server. Single binary that replaces the need for separate block history, account indexing, and DAS API services.
light-rpc subscribes to a Solana validator's stream (Yellowstone Dragon's Mouth gRPC, native Richat gRPC, or QUIC firehose) and indexes everything into a tiered storage system. It then serves the full Solana JSON-RPC API from one HTTP endpoint β no validator node required. Cold start can rebuild full account state from a local Solana snapshot (source.snapshot_dir).
Validator (Richat gRPC)
β
βΌ
ββ StreamSource ββββββββββββββββββββββββββββββββ
β gRPC subscription, commitment tracking, β
β block accumulation, account updates β
ββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
β mpsc channel
βΌ
ββ StorageWriter βββββββββββββββββββββββββββββββ
β β
β Block pipeline: β
β β LZ4 block files (historical data) β
β β RocksDB indexes (slot, tx, signatures) β
β β
β Account pipeline: β
β β RocksDB (program accounts, fast lookup) β
β β PostgreSQL (tokens, mints, relational) β
β β
ββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
β broadcast channel
βΌ
ββ MemoryCache βββββββββββββββββββββββββββββββββ
β ~260 recent blocks, 512 blockhashes, β
β atomic slot tracking per commitment level β
ββββββββββββ¬ββββββββββββββββββββββββββββββββββββ
β
βΌ
ββ RPC Server (Axum + jsonrpsee) βββββββββββββββ
β 35 JSON-RPC methods on single endpoint β
β Compression (gzip, brotli), CORS, metrics β
ββββββββββββββββββββββββββββββββββββββββββββββββ
Single binary, single process. No separate ingest and API services to coordinate. The gRPC source, storage writer, and RPC server run as isolated async tasks connected by bounded channels. If the RPC server is under heavy load, the ingestion pipeline is unaffected β they share no resources except the broadcast channel.
Tiered read path. Every query checks memory first (sub-ms), then RocksDB indexes (single disk read), then block files or PostgreSQL. Recent data is always fast.
Isolated PostgreSQL writes. Token and account writes to PostgreSQL go through a separate bounded channel to a dedicated writer task. If PG is slow, the channel buffers and the main pipeline never stalls. Account state is last-write-wins, so dropped updates during backpressure are safe.
Unified RocksDB. Block indexes and account data share one RocksDB instance with 7 column families. One compaction budget, one memory pool, one thing to tune.
getBlockβ full block with transactionsgetBlocksβ confirmed slots in a rangegetBlocksWithLimitβ confirmed slots starting at a slot, limited countgetBlockHeightβ current block height by commitmentgetBlockTimeβ unix timestamp for a slotgetSlotβ current slot by commitmentgetLatestBlockhashβ latest blockhash with validity windowisBlockhashValidβ check if a blockhash is still recentgetFirstAvailableBlockβ oldest slot still served from local storage
getTransactionβ transaction details by signaturegetSignaturesForAddressβ transaction history for an addressgetSignatureStatusesβ confirmation status of signaturesgetTransactionsForAddressβ full decoded transactions for a wallet, auto-expanding owned token accounts (ATAs) in one paginated call
getAccountInfoβ account data by pubkeygetMultipleAccountsβ batch account lookupgetProgramAccountsβ all accounts owned by a programgetBalanceβ SOL balance for a pubkey
getTokenAccountsByOwnerβ token accounts for a walletgetTokenAccountsByDelegateβ delegated token accountsgetTokenSupplyβ total supply of a mintgetTokenLargestAccountsβ largest holders of a mint
getAssetβ NFT/compressed NFT metadatagetAssetsByOwnerβ assets owned by a walletgetAssetsByCreatorβ assets by creator addressgetAssetsByGroupβ assets by collection/groupgetAssetsByAuthorityβ assets by authoritysearchAssetsβ full-text asset searchgetAssetProofβ merkle proof for compressed NFTs
getVersionβ node version infogetGenesisHashβ cluster genesis hashgetIdentityβ node identity pubkeygetHealthβ service healthgetEpochInfoβ current epoch / slot index / slots-in-epochgetEpochScheduleβ epoch schedule constantsgetMinimumBalanceForRentExemptionβ rent-exempt minimum for a data length
Methods like sendTransaction and simulateTransaction are forwarded to a configured upstream validator RPC.
data/
βββ rocksdb/ # Unified RocksDB (7 column families)
β βββ slot_index # slot β block metadata (time, height, hash)
β βββ tx_index # signature β transaction location
β βββ sfa_index # address + slot β signatures (prefix scan)
β βββ accounts # pubkey β serialized account data (all kinds)
β βββ program_index # program_id + pubkey β (prefix scan)
β βββ owner_atas # owner + token_account β (SPL ATA enumeration)
β βββ mint_top_holders # mint β top-K (amount, token_account)
β
βββ blocks/ # LZ4-compressed block files
βββ s{shard}/ # sharded by slot / 10000
βββ {slot}.blk
PostgreSQL stores relational data that benefits from SQL queries:
token_mintsβ SPL token mint metadatatoken_accountsβ SPL token account balances (indexed by owner, mint)address_transactionsβ address β transaction history with block timesslot_statusβ slot commitment progression
source:
# gRPC endpoint (Yellowstone Dragon's Mouth or Richat). Set exactly one
# of `endpoint` or `quic`.
endpoint: "http://127.0.0.1:10000"
grpc_mode: yellowstone # "yellowstone" (default) or "richat"
# quic: # QUIC firehose (richat-client::quic)
# endpoint: "127.0.0.1:10101"
x_token: ~ # Optional auth token
commitment: finalized
max_message_size: 67108864 # 64MB
snapshot_dir: "/tmp/light-rpc-snapshots" # Local Solana snapshot dir for cold-start
storage:
rocksdb:
path: "data/rocksdb"
write_buffer_size: 268435456 # 256MB
max_open_files: -1 # unlimited; required so compaction
# can drain a large L0 backlog
blocks:
path: "data/blocks"
compression: lz4
max_stored_blocks: 500000
postgres:
url: "postgres://user:pass@localhost:5432/light_rpc"
max_connections: 50
pipeline:
source_to_write: 2048 # Channel capacities
write_to_read: 1024
pg_write_buffer: 10000
rpc:
endpoint: "0.0.0.0:8876"
request_timeout_secs: 60
upstream: ~ # Optional validator RPC for forwarding
forwarded_methods:
- sendTransaction
- simulateTransaction
metrics:
endpoint: "0.0.0.0:9090" # Prometheus metricsSee config.example.yml for all options.
cargo build --releaseThe release binary is ~21MB (LTO + stripped).
# Validate config
./target/release/light-rpc --config config.yml --check
# Run
./target/release/light-rpc --config config.yml
# With debug logging
RUST_LOG=debug ./target/release/light-rpc --config config.ymldocker build -t light-rpc .
docker run -v ./config.yml:/etc/light-rpc/config.yml light-rpcPrometheus metrics are served on the configured metrics endpoint (default :9090):
| Metric | Type | Description |
|---|---|---|
li_ingested_blocks_total |
counter | Total blocks ingested from gRPC |
li_ingested_accounts_total |
counter | Total account updates ingested |
li_ingested_txs_total |
counter | Total transactions ingested |
li_latest_slot{commitment} |
gauge | Latest slot by commitment level |
li_rpc_requests_total{method} |
counter | RPC requests by method |
li_rpc_errors_total{method} |
counter | RPC errors by method |
li_rpc_latency_seconds{method} |
histogram | RPC method latency |
li_storage_write_seconds |
histogram | Storage write latency per block |
li_pg_write_seconds |
histogram | PostgreSQL batch write latency |
li_memory_cached_blocks |
gauge | Blocks currently in memory cache |
li_pipeline_channel_size{channel} |
gauge | Pipeline channel utilization |
li_pg_drop_total{kind} |
counter | PG write jobs dropped (channel full) |
li_clickhouse_drop_total |
counter | ClickHouse write jobs dropped (channel full) |
li_source_malformed_total |
counter | Stream messages skipped (bad pubkey/signature) |
li_rocksdb_quarantined_total |
counter | Corrupt SSTs auto-quarantined |
li_rocksdb_l0_files{cf} |
gauge | L0 file count per column family |
li_rocksdb_live_data_bytes{cf} |
gauge | Live data size per column family |
src/
βββ main.rs # Entry point, pipeline orchestration
βββ lib.rs # Crate root, module exports
βββ config.rs # YAML configuration types
βββ types.rs # Shared types (Slot, BlockWithData, AccountUpdate, etc.)
βββ metrics.rs # Prometheus metric definitions
βββ source/
β βββ stream.rs # Richat gRPC subscription, block accumulation
β βββ commitment.rs # Slot commitment state machine
βββ storage/
β βββ rocks.rs # Unified RocksDB (7 column families)
β βββ files.rs # LZ4 block file storage
β βββ postgres.rs # PostgreSQL operations (tokens, migrations)
β βββ accounts.rs # Account classification and serialization
β βββ write.rs # Write worker (blocks + accounts β storage)
β βββ read.rs # Read worker (memory cache + tiered lookup)
βββ rpc/
βββ server.rs # Axum HTTP server, JSON-RPC dispatch
βββ upstream.rs # Upstream validator RPC forwarding
βββ methods/
βββ blocks.rs # getBlock, getSlot, getLatestBlockhash, ...
βββ transactions.rs # getTransaction, getSignaturesForAddress, ...
βββ accounts.rs # getAccountInfo, getProgramAccounts, ...
βββ tokens.rs # getTokenAccountsByOwner, getTokenSupply, ...
βββ assets.rs # getAsset, getAssetsByOwner, searchAssets, ...
- Rust stable (1.86+)
- PostgreSQL 14+
- A Richat/Yellowstone gRPC source (validator with geyser plugin)
light-rpc is much lighter than a full validator. A single mainnet node fully indexing all accounts, transactions, and blocks comfortably fits on:
| Resource | Minimum | Recommended |
|---|---|---|
| CPU | 4 cores | 8 cores |
| RAM | 16 GB | 32 GB |
| Disk | 500 GB NVMe SSD | 1 TB NVMe SSD |
| Network | 1 Gbps | 1 Gbps |
Observed steady-state on a mainnet node (2-day retention, gRPC source, all account CFs populated): ~1β3 cores sustained, ~50 GB RES (most of it RocksDB block cache and mmap'd SSTs β the OS reclaims it under pressure), ~390 GB on-disk for RocksDB. PostgreSQL adds ~30 GB.
Disk grows roughly linearly with retention (storage.blocks.max_stored_blocks) and with how aggressively you keep tx_index / sfa_index history. Account CFs are bounded by chain state, not retention.
AGPL-3.0-only. See LICENSE for the full text and LICENSE.md for licensing notes.