Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/content/architecture/_meta.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
label: Architecture
order: 8
collapsed: true
73 changes: 73 additions & 0 deletions docs/content/architecture/crates.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
title: Crates & Modules
sidebar:
order: 1
---

Dolos is a Cargo workspace. The code is split so that generic data-node abstractions, Cardano-specific ledger logic, storage engines, and API servers each live in their own crate and depend on one another in a single direction. This page maps the crates and shows how the main `dolos` binary is organized.

## Workspace crates

| Crate | Path | Purpose |
| ----- | ---- | ------- |
| `dolos-core` | `crates/core/` | Chain-agnostic traits and types: `Domain`, `ChainLogic`, `WorkUnit`, and the `StateStore` / `ArchiveStore` / `WalStore` / `IndexStore` / `MempoolStore` storage traits. Knows nothing about Cardano. |
| `dolos-cardano` | `crates/cardano/` | The Cardano implementation of the core traits: block processing, ledger state, era rules, epoch transitions, and reward calculations. |
| `dolos-redb3` | `crates/redb3/` | Storage backend built on [redb](https://github.com/cberner/redb) (embedded, ACID, B+tree). Implements WAL, state, archive, indexes, and mempool. |
| `dolos-fjall` | `crates/fjall/` | Storage backend built on [fjall](https://github.com/fjall-rs/fjall) (LSM-tree), an alternative for write-heavy state and index stores. |
| `dolos-minibf` | `crates/minibf/` | Blockfrost-compatible HTTP API server. |
| `dolos-minikupo` | `crates/minikupo/` | Kupo-compatible HTTP API for datum/script resolution and pattern matching. |
| `dolos-trp` | `crates/trp/` | Transaction Resolver Protocol (Tx3) JSON-RPC server. |
| `dolos-testing` | `crates/testing/` | Shared test utilities and fixtures. |
| `xtask` | `xtask/` | Developer automation (`cargo xtask …`): test instances, ground-truth generation, DBSync queries. |
| `dolos` (root) | `src/` | The binary crate: CLI, sync pipeline, relay, serve orchestration, and the runtime adapters that wire everything together. |

The API-server crates and several subsystems are gated behind Cargo features. The default build enables `mithril`, `utils`, `grpc`, `minibf`, `minikupo`, and `trp`, so a standard `dolos` binary ships with every API surface. `dolos-trp` is an optional feature-gated dependency rather than a workspace member.

## Dependency layering

Dependencies point in one direction — from concrete implementations toward the abstract core. Nothing in `dolos-core` depends on a storage engine or an API server.

```mermaid
flowchart BT
core["dolos-core<br/><small>traits: Domain · ChainLogic · WorkUnit · stores</small>"]
cardano["dolos-cardano<br/><small>ChainLogic impl</small>"]
backends["dolos-redb3 / dolos-fjall<br/><small>StateStore/… impls</small>"]
services["dolos-minibf / minikupo / trp<br/><small>serving adapters over Domain</small>"]
bin["dolos<br/><small>root binary: CLI · sync · relay · serve · adapters</small>"]

cardano --> core
backends --> core
services --> core
bin --> cardano
bin --> backends
bin --> services
```

The root binary is the only place that picks concrete backends and API servers and assembles them into a running node.

## The `dolos` binary

The binary lives in `src/bin/dolos/` and dispatches a set of top-level subcommands:

| Command | What it does |
| ------- | ------------ |
| `init` | Interactive wizard to generate configuration. |
| `daemon` | Full node — runs the sync pipeline and all enabled API servers together. |
| `sync` | Sync only — pull blocks from the upstream peer and apply them to the ledger. |
| `serve` | Serve only — expose the APIs over already-synced data. |
| `data` | Inspect and export stored data (dump state, logs, blocks, WAL; prune; import archive). |
| `eval` | Evaluate a transaction against current ledger state (phase-1 and phase-2). |
| `doctor` | Diagnose and repair (WAL integrity, rollback, reset genesis/WAL, catch-up stores). |
| `bootstrap` | Initialize a node from a Mithril snapshot, a Dolos snapshot, or a relay. |

The `daemon`, `sync`, and `serve` commands map directly onto the sync pipeline and serving layer described in the following pages. For how to choose between them operationally, see [Operations → Modes](../operations/modes); for `bootstrap`, see the [Bootstrap section](../bootstrap).

## Key dependencies

Dolos leans on a small number of foundational crates:

- **[Pallas](https://github.com/txpipe/pallas)** — Cardano building blocks: CBOR codecs, ledger primitives, the Ouroboros mini-protocols, and phase-2 (Plutus) evaluation.
- **[gasket](https://github.com/construkts/gasket-rs)** — the staged, concurrent pipeline framework used by the sync loop.
- **redb** and **fjall** — the embedded storage engines behind the two storage backends.
- **tokio** — the async runtime shared by the pipeline and every API server.
- **tonic** (gRPC), **axum** (Mini-Blockfrost, Mini-Kupo), and **jsonrpsee** (TRP) — the serving frameworks.
62 changes: 62 additions & 0 deletions docs/content/architecture/data-layer.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: Data Layer
sidebar:
order: 2
---

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 four 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, pools, accounts, epoch state, protocol params
├── archive/ Historical blocks — full block bodies + temporal entity logs
└── index/ Secondary indexes — reverse lookups (address → UTxO, asset → UTxO, …)
```

| Store | Holds | Why it's separate |
| ----- | ----- | ----------------- |
| **WAL** | The most recent blocks, stored as `EntityDelta`s 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, 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. |
| **Archive** | Immutable history: full block bodies plus time-indexed logs of entity changes. | History is append-only and read differently from current state, so it uses its own layout (see below). |
| **Index** | Reverse lookups over UTxOs by address, payment credential, stake credential, policy, and asset. | The APIs need "find all UTxOs for X" queries that the primary state store isn't keyed for. |
| **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`, `IndexStore`, 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 can back state, archive, and indexes.
- **fjall** (`dolos-fjall`) — an LSM-tree engine tuned for write-heavy workloads with many hot keys, available for the state and index stores.
- **no-op** — a store that silently discards writes, used to *disable* the archive and/or index stores.
- **in-memory** — non-persistent variants used for testing and ephemeral nodes.

## Storage modes

The three storage modes exposed in configuration are really combinations of backend selection and history-pruning limits:

| Mode | Archive / Index | 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. |

Pruning is driven by `prune_history()` on the WAL and archive stores. See the [configuration schema](../configuration/schema#storage-section) 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](./sync-pipeline).
- **`EpochValue` snapshot 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](./ledger-model).
- **Archive dual storage.** Block bodies are written to append-only flatfile segments (one segment per Cardano epoch), while a compact index maps each slot to its `(segment, offset, length)` location. Historical entity changes are stored as `(slot, entity-key) → value` logs, enabling range queries over time.
- **Index dimensions.** The index store maintains multimaps keyed by address, payment credential, stake credential, policy id, and asset id, each pointing at the matching UTxOs (for current state) or slots (for historical 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`.
53 changes: 53 additions & 0 deletions docs/content/architecture/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: Overview
sidebar:
order: 0
---

This section describes how Dolos is built. It is a macro view of the system — the layers, the crates, the storage engine, the sync pipeline, and the serving layer — meant for contributors, integrators, and operators who want to understand the internals without reading the source. It is not an API reference; for endpoint-level detail see the [APIs section](../apis/grpc).

Dolos is a data node, not a consensus node. It connects to the Cardano network as an Ouroboros peer, keeps an up-to-date copy of the ledger, and serves that data through several query interfaces. Everything in the architecture follows from that goal: read the chain efficiently, keep resource usage low, and expose the result through the tools developers already use.

## A layered system

At the highest level, data flows in one direction — from an upstream peer, through the sync pipeline, into local storage — and is then read back out by the serving layer.

```mermaid
flowchart TD
peer["Upstream peer<br/><small>Ouroboros node-to-node (chainsync + blockfetch)</small>"]
sync["Sync pipeline<br/><small>pull → apply (→ submit) — gasket stages</small>"]
storage["Storage<br/><small>WAL · State · Archive · Index · Mempool (redb / fjall)</small>"]
serve["Serving layer<br/><small>gRPC · Mini-Blockfrost · Mini-Kupo · Ouroboros n2c socket · TRP</small>"]
clients["Clients<br/><small>dApps · wallets · explorers · cardano-cli</small>"]

peer -- "blocks / rollbacks" --> sync
sync -- "work units (apply / undo deltas)" --> storage
storage -- "read-only queries" --> serve
serve --> clients
```

Each layer is covered in its own page:

- [Crates & Modules](./crates) — how the codebase is split into a Cargo workspace.
- [Data Layer](./data-layer) — the stores, their traits, and the pluggable storage backends.
- [Sync Pipeline](./sync-pipeline) — how blocks get from the network into storage.
- [Ledger Model & Epoch Transitions](./ledger-model) — how Cardano ledger state evolves.
- [Serving Layer & APIs](./serving-layer) — how the query interfaces read that state.

## Chain-agnostic core, Cardano implementation

The most important structural idea in Dolos is the split between a **generic core** and a **chain-specific implementation**.

The `dolos-core` crate defines the abstractions that any UTxO blockchain data node needs, as a set of traits: `Domain` (the umbrella that bundles all the stores together), `ChainLogic` (how blocks turn into state changes), `WorkUnit` (a unit of pipelined ledger work), and the storage traits `StateStore`, `ArchiveStore`, `WalStore`, `IndexStore`, and `MempoolStore`. None of this code knows anything about Cardano specifically.

The `dolos-cardano` crate implements those traits for Cardano — block parsing, ledger rules, epoch transitions, and reward calculations. Everything else in the system is either a **storage backend** (an implementation of the store traits, e.g. `dolos-redb3` or `dolos-fjall`) or a **serving adapter** (an implementation of an API surface that reads from a `Domain`). This is what keeps the sync pipeline, the storage engine, and the API servers decoupled from each other and from the ledger rules.

## One domain, shared by sync and serve

At runtime the whole node is wired together by a single `DomainAdapter` (in `src/adapters/`), which holds handles to every store plus the live Cardano ledger state (`Arc<RwLock<CardanoLogic>>`). The sync pipeline takes the write side of that lock to apply blocks; the serving layer takes read handles to answer queries concurrently. A tip broadcast channel lets streaming endpoints (chain follow, watch) react to new blocks as they are applied.

Because state, archive, and index writes are committed as the pipeline advances, the serving layer reads an up-to-date view without the two sides blocking each other. Commits to the individual stores are sequential rather than atomic across stores, so a reader can briefly observe one store slightly ahead of another while a block is being applied.

## Trust model

Dolos relies on an honest upstream peer for block data — it validates transactions (phase-1 and phase-2) but does not run consensus or independently verify block production. This is the deliberate tradeoff that lets it stay lightweight. For the full rationale, see [What is Dolos](../what).
68 changes: 68 additions & 0 deletions docs/content/architecture/ledger-model.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
title: Ledger Model & Epoch Transitions
sidebar:
order: 4
---

This page is the deepest part of the architecture overview: how Cardano ledger state actually evolves inside Dolos. It stays at a conceptual level — enough to understand the moving parts and where they live in the code. Contributors working on ledger correctness should also read the "Fix Journal" and ledger notes in the repository's `CLAUDE.md`, which document the rules and edge cases in detail.

The Cardano ledger is implemented in the `dolos-cardano` crate as the `CardanoLogic` type, which fulfills the generic `ChainLogic` trait from `dolos-core`.

## Work units

When the sync pipeline hands a block to `CardanoLogic`, it does not mutate state directly — it produces **work units**, the pipelined units of ledger work introduced in the [Sync Pipeline](./sync-pipeline). Cardano defines these variants (`CardanoWorkUnit`):

| Work unit | When | What it does |
| --------- | ---- | ------------ |
| **Genesis** | node bootstrap | Builds the initial ledger from the genesis configuration. |
| **Roll** | every block | Applies a block: inputs, outputs, mints/burns, certificates, withdrawals, and parameter updates. |
| **Rupd** | mid-epoch (stability window) | Computes the epoch's reward distribution from a stake snapshot. |
| **Ewrap** | epoch boundary (close) | Closes the epoch: applies rewards, refunds deposits, enacts governance, and finalizes the pots. |
| **Estart** | epoch boundary (open) | Opens the next epoch: rotates snapshots, transitions pools/DReps/proposals, updates nonces, and advances the era. |
| **ForcedStop** | testing | Halts at a target epoch (used by the integration test harness). |

## The snapshot window

Cardano does not reward stake based on its *current* delegation — it uses delegation as it stood a few epochs earlier, so that the stake distribution used for leader election and rewards is stable and known in advance. Dolos models this with a rotating window of snapshots, `EpochValue<T>`, attached to every entity that participates in staking (accounts, pools, DReps).

Each `EpochValue` holds several named snapshots — the arrows show where each value moves on an epoch transition:

```mermaid
flowchart LR
next["next<br/><small>upcoming epoch (E+1)</small>"]
live["live<br/><small>current epoch (E)</small>"]
mark["mark<br/><small>previous epoch (E-1)</small>"]
set["set<br/><small>two epochs ago (E-2)<br/>stake used for this epoch's rewards</small>"]
go["go<br/><small>three epochs ago (E-3)</small>"]

next --> live --> mark --> set --> go
```

Reward calculations read from the appropriate historical slot (e.g. the `mark`/`set` snapshots), which is why a correct implementation must write each change into the right slot at the right time. Getting a snapshot's timing wrong is a common class of ledger bug.

## Epoch boundary phases

The transition between epochs happens in a defined order — this ordering matters, because rewards, snapshots, and deposit refunds interact.

```mermaid
flowchart LR
rupd["RUPD<br/><small>snapshot rewards at stability window (mid-epoch)</small>"]
ewrap["EWRAP<br/><small>close epoch (at boundary)</small>"]
estart["ESTART<br/><small>open next epoch (at boundary)</small>"]

rupd --> ewrap --> estart
```

1. **RUPD** — partway through the epoch, at the randomness-stability window, Dolos snapshots the stake distribution and computes the rewards that will be paid out. Registration status at this moment determines who is eligible.
2. **EWRAP** — at the boundary, the epoch is *closed*: pending rewards are applied to accounts (filtered for anyone who deregistered since RUPD), deposits are refunded, governance actions are enacted, and the treasury/reserves accounting is settled.
3. **ESTART** — the next epoch is *opened*: the snapshot window rotates, pool/DRep/proposal state transitions forward, epoch nonces update, era changes take effect, and the chain cursor advances.

## The pots

Dolos tracks the Cardano money supply as a set of **pots** — reserves, treasury, the rewards pot, the UTxO total, and collected fees — plus outstanding deposits. These satisfy the ledger invariant that they always sum to the fixed maximum supply. During an epoch, changes accumulate as a delta; at the boundary the delta is applied to produce the new pots: monetary expansion is drawn from reserves, the treasury takes its cut, rewards are distributed, and fees roll over. The formulas (monetary expansion η, epoch incentives, and how unspendable rewards are routed) are documented alongside the implementation in `crates/cardano/src/pots.rs` and in `CLAUDE.md`.

## Sharding

The epoch boundary touches every stake account, which on mainnet is a large set. To keep transitions fast, EWRAP and ESTART split the per-account work across a fixed number of shards (`ACCOUNT_SHARDS`), each covering a prefix range of the account key space, and run them in parallel through the work-unit `load` step before a final global pass. Progress is tracked per shard so an interrupted boundary can resume rather than restart.

Source: `crates/cardano/src/{work,model/*,rupd,ewrap,estart,pots,rewards,shard}.rs`.
Loading
Loading