A Midnight node is a Cardano partner chain, which means it has to watch the Cardano main chain to do its job. It reads token registrations, stake distributions, governance UTXOs and bridge transfers from Cardano and folds them into its own blocks.

The documented way to do that is to run cardano-node next to it, plus cardano-db-sync, plus the PostgreSQL database db-sync fills. On mainnet that’s 64GB RAM and a machine that takes days to sync before your Midnight node has even started. It’s an absurd amount of infrastructure to hang off a Raspberry Pi. But is it really needed?

Midnight node boot banner on the Raspberry Pi console
The Pi console at boot. Note the last line, Main chain follower backend: Blockfrost, where a normal setup would be reaching for a Postgres connection string.

Six queries in a trenchcoat

The first useful discovery is that the Midnight node doesn’t really need “a Cardano database”. It needs six data sources, and between them they ask a surprisingly small number of questions:

  • which Cardano block is stable at a given time, and what’s at this block hash
  • which transactions touched this address between block X and block Y
  • which transactions touched this asset in the same range
  • the inputs and outputs of a transaction, and any datum attached
  • the epoch nonce, and each pool’s active stake for an epoch

Every one of those maps onto an endpoint that a Blockfrost-compatible API already exposes. The db-sync SQL is doing something clever, since four indexed queries give you every relevant event in a block range, but nothing about it is fundamentally relational.

So I wrote a Blockfrost backend for the node. Set blockfrost_endpoint in the config and all six data sources read from an HTTP API. Leave it unset and nothing changes.

The catch is that “equivalent” is doing a lot of work in that sentence. This data goes into consensus: every node has to derive exactly the same events from the same range, or they disagree about what a block contains. So the interesting part of the project wasn’t writing the HTTP calls, it was proving they agree with db-sync down to the last event. More on that below.

Enter Dolos

Reading from hosted Blockfrost works and is genuinely pleasant. An API key and you’re running, and the free tier is more than generous: 50,000 requests a day against the 20,000 to 33,000 a node at tip actually uses. You can follow preview, preprod or on it indefinitely without paying anything, which is a lovely place for the barrier to entry to be. For development, experimenting, or watching the chain, it is the obvious answer and I would reach for it first.

However, what if you want a really trusted setup?

Dolos closes that gap. It’s a “data node” from the TxPipe folks: it follows the Cardano chain over the normal node-to-node protocol, keeps the slice of state you actually need, and serves it over several APIs, including a Blockfrost-compatible one they call minibf. It’s a single Rust binary with a TOML config, and it bootstraps from Mithril snapshots rather than replaying the chain from genesis.

Which makes the shape of the thing rather nice:

┌────────────────────────────────────────────────┐
│ Raspberry Pi 4 (4 GB)                          │
│                                                │
│  ┌───────────────┐  HTTP   ┌────────────────┐  │
│  │ midnight-node │────────▶│ dolos (minibf) │──┼──▶ Cardano
│  │    preview    │  :3000  │                │  │    relays
│  └───────────────┘         └────────────────┘  │
└────────────────────────────────────────────────┘

No PostgreSQL. No db-sync. No cardano-node.

The relevant bit of dolos.toml is short:

[upstream]
peer_address = "preview-node.play.dev.cardano.org:3001"
network_magic = 2

[serve.minibf]
listen_address = "[::]:3000"

and on the node side, two settings:

blockfrost_endpoint = "http://[::1]:3000"

One important caveat before you try this: minibf needs txpipe/dolos#1151 to be accepted upstream. That PR is what makes the endpoint set complete enough for a Midnight node, so this will only work out of the box from the next stable Dolos release. Until it lands you need a build with that change.

Does it actually agree with db-sync?

This was the part I cared about, because “close enough” isn’t a thing in consensus.

I wrote a parity test that builds both backends, real db-sync and the HTTP backend, then points them at the same anchor block, calls each data source through the same trait, and asserts the results are equal. Not “looks similar”: assert_eq! on the returned structures.

Against a live preview db-sync, with a 400,000-block window:

calldb-syncDolos
get_candidates712 ms1 ms~700x faster
get_transfers461 ms1 ms~460x faster
get_stable_block_for412 ms1 ms~410x faster
get_federated_authority_data325 ms7 ms~46x faster
get_ariadne_parameters299 ms8 ms~37x faster
get_epoch_nonce109 ms6 ms~18x faster
get_utxos_up_to_capacity1,667 ms9,822 ms~6x slower

6,103 cNIGHT events compared, identical. Registrations, deregistrations, token creates and spends, in the same order with the same range bounds.

I ran the same comparison against preprod and mainnet db-sync instances too, for roughly 8,000 events across the three networks, plus one real bridge transfer with its checkpoint on mainnet.

The numbers on the Pi

The Pi is a 4 GB Raspberry Pi 4 with an external SSD, running the node as a plain systemd unit out of /opt/midnight:

[Service]
User=midnight
Environment=CFG_PRESET=preview
Environment=BASE_PATH=/opt/midnight/data
Environment=BLOCKFROST_ENDPOINT=http://127.0.0.1:3000
ExecStart=/opt/midnight/bin/midnight-node --name my-pi-node --port 30333
Restart=on-failure

That endpoint is Dolos, running on the same board out of /opt/midnight/bin/dolos with its own unit. Both processes on one 4 GB Pi:

RSS
midnight-node~816 MB
dolos~719 MB
total~1.5 GB of 3.7 GB

Disk is 19 GB of a 29 GB SSD for both together. Dolos’s Cardano state is the larger share, the node’s chain and ledger about 7 GB. No swap pressure. The node’s memory is mostly its sliding window of Cardano observations, which is a config knob, so you can trade memory for more fetch rounds if you’re tight.

Request volume at tip is roughly 2 requests per Midnight block, about 28,000 a day. Bootstrapping preview from genesis cost around half a million requests over about twelve hours. Against local Dolos those numbers stop being a budget and start being just… traffic on loopback.

Those two numbers pull in different directions, and it’s worth being precise about it. Steady state is cheap: 28,000 requests a day sits comfortably inside the free tier’s 50,000, so once you’re at tip you can stay there for nothing. The bootstrap is the expensive part, because half a million requests is roughly ten days of free-tier allowance. Perfectly doable if you’re patient, since the node just resumes each day when the quota resets. On the developer plan the quota stops being the constraint at all: the preview bootstrap ran flat out and was done in few hours.

With the quota out of the way, what’s left is how fast the node can execute blocks.

networkblocksratebootstrap
preview~226,000~12 blocks/sec~5 hours
preprod~1.91M~11 blocks/sec~1.5 days
mainnet~1.95M~9 blocks/sec~2.5 days

Where this actually stands

Running: the Pi follows preview at tip against Dolos on the same board, and the numbers above are measured from that. The parity evidence, roughly 8,000 cNIGHT events matching db-sync across preview, preprod and mainnet plus the Dolos comparison, comes from a bigger machine, because it needs a db-sync instance to compare against and that is exactly the thing I am trying not to run on a Pi.

A Pi 5 with 8 GB should handle mainnet comfortably, though, and I don’t think that’s a stretch given what’s above. Preview needs about 1.5 GB across both processes on the Pi 4, so there’s real headroom at 8 GB, and sync speed already matches a cloud VM. Size the disk for Cardano rather than for Midnight: the node’s own chain and ledger state is small, and it’s Dolos’s copy of mainnet that decides how big a drive you need.