Data Layer
Dolos keeps everything it needs on local disk in a handful of embedded stores — key-value engines for live data, plus append-only flatfiles for archived block bodies. Rather than one monolithic database, it uses several purpose-built stores — each optimized for a different access pattern — behind a common set of traits, so the underlying engine can be swapped without touching the rest of the system.
The stores
Under the configured storage.path, a running node maintains three on-disk stores plus an in-memory (or persistent) mempool:
<storage.path>/├── wal/ Write-Ahead Log — recent blocks as reversible deltas (rollback buffer)├── state/ Ledger state — current UTxO set (with its by-address/asset/… tags), pools, accounts, epoch state, protocol params└── archive/ Historical blocks — full block bodies, temporal entity logs, and the reverse lookups over them| Store | Holds | Why it’s separate |
|---|---|---|
| WAL | The most recent blocks, stored as EntityDeltas that can be applied or undone. | Rollbacks (chain reorganizations) are resolved by replaying these deltas in reverse. It is a bounded buffer, not long-term history. |
| State | The current ledger: UTxO set and its reverse-lookup tags (by address, payment credential, stake credential, policy, asset, reference script), stake pool registry, account/stake state, DReps, protocol parameters, and per-epoch ledger state. | This is the hot, always-current view the ledger rules read and write on every block. The tags are a projection of the UTxO set, so they live beside it and commit with it. |
| Archive | Immutable history: full block bodies, time-indexed logs of entity changes, and the reverse lookups over them — slots by address, payment credential, stake credential, policy, asset, datum and more, plus slot by block hash, block number and tx hash. | History is append-only and read differently from current state, so it uses its own layout (see below). The lookups are a projection of the blocks, so they live beside them and commit with them. |
| Mempool | Submitted-but-unconfirmed transactions and their lifecycle state. | Pending transactions are transient and overlaid on top of committed state during validation. |
Storage traits and pluggable backends
Each store is defined as a trait in dolos-core — WalStore, StateStore, ArchiveStore, and MempoolStore — and the rest of Dolos only ever talks to those traits. Concrete implementations are selected at runtime in src/adapters/storage.rs, which wraps each backend in an enum so a node can mix engines per store.
Available backends:
- redb (
dolos-redb3) — an embedded ACID B+tree store. It backs the WAL (the only WAL implementation) and the mempool. It is not a state or archive backend. - fjall (
dolos-fjall) — an LSM-tree engine tuned for write-heavy workloads with many hot keys. The only persistent backend for the state and archive stores, and the default for both. - no-op — a store that silently discards writes, used to disable the archive.
- in-memory — non-persistent stores for testing and ephemeral nodes. State and archive have builtin implementations in
dolos-corebacked by ordered maps, which serve their traits in full; the WAL and mempool use redb’s memory backend instead.
Storage modes
The three storage modes exposed in configuration are really combinations of backend selection and history-pruning limits:
| Mode | Archive | History | Use case |
|---|---|---|---|
| Ledger-only | no-op | none | Only the tip of the chain — smallest footprint, query-light deployments. |
| Sliding history | enabled, pruned | a rolling window (sync.max_history) | Recent history for most dApp queries without a full archive. |
| Full archive | enabled, unpruned | complete | The entire chain history — research, validation, explorers. |
archive.backend = "no_op" is the ledger-only switch, and it drops the historical lookups with the archive that hosts them. The live-UTxO tags are not affected: they project the UTxO set, so they stay in the state store in every mode (about 3.5 GB on mainnet).
Pruning is driven by prune_history() on the WAL and archive stores. In the archive it covers the block bodies, the logs, and the archive-tags and index-exact keyspaces, so a sliding-history node’s index footprint is window-sized too. See the configuration schema for the exact keys.
Data model primitives
A few concepts recur across the stores:
- Namespaced entities. State and archive data are organized into namespaces (UTxOs, pools, accounts, rewards, …), each a logical table keyed by an entity key. A store may hold a single value per key or multiple values per key.
EntityDelta(apply / undo). Every state change is expressed as a delta that carries enough of the previous value to reverse itself. Applying deltas rolls the ledger forward; undoing them (from the WAL) rolls it back. This is the mechanism that makes rollbacks exact — see the Sync Pipeline.EpochValuesnapshot window. Cardano staking reads state as it was several epochs ago. State entities that participate in staking are stored as a rotating window of snapshots (live / mark / set / go / next). The details are covered in the Ledger Model.- Archive dual storage. Block bodies are written to append-only flatfile segments (one segment per Cardano epoch) as one zstd frame per block, compressed with the dictionary bundled in
dolos-flatfiles, while a compact index maps each slot to its(segment, offset, length)frame location. Every frame is written and synced before the location that names it is committed, so a crash leaves at most dead space at a segment’s end and never a location pointing at bytes that are not there. Historical entity changes are stored as(slot, entity-key) → valuelogs, enabling range queries over time. - Index dimensions. Tag multimaps keyed by address, payment credential, stake credential, policy id, and asset id. The live ones point at the matching UTxOs and live in the state store’s
state-tagskeyspace, written in the same batch as the UTxO set; the historical ones point at slots and live in the archive store’sarchive-tagskeyspace, beside theindex-exactkeyspace that resolves a block hash, block number or transaction hash to its slot — both written in the same batch as the blocks they project.
Two fjall databases, eight keyspaces:
| Store | Keyspaces | One batch commits |
|---|---|---|
| state | state-cursor, state-utxos, state-entities, state-tags | entities + UTxO set + live-UTxO tags + cursor |
| archive | archive-blocks, archive-logs, archive-tags, index-exact | block locations + logs + archive tags + exact lookups |
Source: crates/core/src/{state,archive,wal,indexes,mempool}.rs, crates/redb3/src/*, crates/fjall/src/*, and storage configuration in crates/core/src/config.rs.