Catalogues & Pinned History
Most xahaud servers run online_delete to keep disk usage bounded: old ledgers are pruned and only a recent window is kept. That is the right default, but it leaves a gap β there is no built-in way to keep a specific historical range online forever without turning deletion off entirely.
Catalogues and pinned history close that gap:
- A catalogue (
.catl) is a single, optionally compressed archive file holding a contiguous range of ledgers. - Pinning keeps loaded ranges in a separate persistent backend that
online_deletenever touches.
Together they let you run an archival or historical-query node: rotate the hot data as usual, but keep chosen historical ranges queryable indefinitely.
When to use this
Section titled βWhen to use thisβUse pinned history when you are loading historical ledgers from a catalogue file, or running a historical-query / explorer service that needs specific ranges to stay online permanently.
You donβt need it for a standard validator or tracking node β online_delete alone is enough, and pinning only adds disk overhead.
Quick start (archival node)
Section titled βQuick start (archival node)βThe common path is to load a catalogue produced elsewhere. Creating one is only for a node that already holds the range (see Obtaining a catalogue).
- Configure the persistent backend (
pinned_type+pinned_path) alongside your rotatingnode_dbβ see Configuration. - Obtain or create a
.catlfile for the range you want. - Start the node normally.
- Load it:
catalogue_load <input_file>. - Watch progress with
catalogue_statusβ loads can take hours. - Verify with
server_infoβcomplete_ledgers_pinned.
How it fits together
Section titled βHow it fits togetherβ βββββββββββββββββββββββββββββββ β [node_db] β β β hot / recent β β type (rotating) ββββΌβββΊ rotating backend β online_delete=N β (pruned every N ledgers) β β pinned historyβ β pinned_type (persistent)ββββΌβββΊ persistent backend β pinned_path=/data/pinned β (never rotated) βββββββββββββββββββββββββββββββ β βΌ state.db βΈ PinnedLedgers tablePinned data lives in the persistent backend; the set of pinned ranges is recorded in state.db so it survives restarts. On startup the node reloads that range set, so complete_ledgers_pinned is accurate right away. (In network mode the ranges only fold into the overall complete_ledgers once the node syncs β see Verifying pinned history.)
Configuration
Section titled βConfigurationβAdd pinned_type and pinned_path to the [node_db] stanza, alongside a rotating type and online_delete:
[node_db]type=nudb # rotating backend (hot data)path=/data/rotatingonline_delete=2000 # rotation interval (see floor below)pinned_type=nudb # persistent backend β must be durablepinned_path=/data/pinned # where pinned data is kept; grows over timeAll four are required together. Setting pinned_type turns on extra startup validation; if a rule is broken, the node refuses to start with the message shown:
| Rule | Error if broken |
|---|---|
A rotating type must be set | type must be specified when using pinned_type |
pinned_path must be set (for on-disk backends) | pinned_path is required when pinned_type is set |
online_delete must be non-zero | pinned_type requires online_delete ... |
pinned_type must be durable: nudb or rocksdb | pinned_type must be a durable backend (NuDB or RocksDB) ... |
online_delete also carries its own floor, independent of pinning: when set it must be at least 256 (8 in standalone) and not below ledger_history. See Advanced Configuration for tuning it.
Obtaining a catalogue
Section titled βObtaining a catalogueβThere is currently no public catalogue archive. Operators create their own catalogues (catalogue_create, on a node that already holds the range) and distribute them β for example by scp or HTTP. The producer and consumer neednβt be the same operator, but a catalogue is network-specific and is rejected on load if its network ID doesnβt match.
A catalogue carries a SHA-512 that catalogue_load verifies automatically, and the same hash is reported when the file is created. The format has no built-in signature, so trusting a file you didnβt make is out of band: rely on the producer publishing (and ideally signing) the catalogueβs SHA-512, and confirm your copy matches before loading.
Creating a catalogue
Section titled βCreating a catalogueβ# catalogue_create <min_ledger> <max_ledger> <output_file> [compression_level]xahaud --conf /etc/xahaud.cfg catalogue_create 1000000 2000000 /data/catl/1m-2m.catl 6| Field | Required | Description |
|---|---|---|
min_ledger | yes | First ledger sequence (must be in the nodeβs complete_ledgers). |
max_ledger | yes | Last ledger sequence, inclusive. Must be >= min_ledger. |
output_file | yes | Absolute path. Must not already exist as a non-empty file. |
compression_level | no | 0β9 zlib level (default 0 = none; higher = smaller file, slower). Over JSON-RPC a boolean is also accepted (true β 6, false β 0). |
Use compression for files youβll store or share; 0 is fastest for a local one-off. The file carries a SHA-512 hash that catalogue_load verifies on the way in.
Success response:
{ "result": { "min_ledger": 1000000, "max_ledger": 2000000, "output_file": "/data/catl/1m-2m.catl", "file_size_human": "2.50 GiB", "ledgers_written": 1000001, "hash": "A1B2C3...", "status": "success" }}Loading a catalogue
Section titled βLoading a catalogueβcatalogue_load reads a catalogue back into the node, reconstructs and verifies each ledger, pins it, and records the pinned range in state.db.
# catalogue_load <input_file> [ignore_hash]xahaud --conf /etc/xahaud.cfg catalogue_load /data/catl/1m-2m.catl| Field | Required | Description |
|---|---|---|
input_file | yes | Absolute path to the catalogue file. |
ignore_hash | no | Pass the literal word ignore_hash (CLI) or true (JSON-RPC) to skip the whole-file hash check. |
On load, the node checks the fileβs magic bytes, version, network ID, size, and β unless ignore_hash is set β the whole-file hash. Each ledgerβs hash is then recomputed and checked as itβs rebuilt; that per-ledger check runs even with ignore_hash, so ignore_hash weakens but doesnβt remove integrity protection.
You can load several catalogues over time; their pinned ranges accumulate (gaps are fine, as the disjoint ranges in complete_ledgers show).
Success response (field names differ from catalogue_create):
{ "result": { "ledger_min": 1000000, "ledger_max": 2000000, "ledger_count": 1000001, "ledgers_loaded": 1000001, "file_size_human": "2.50 GiB", "hash": "A1B2C3...", "ignore_hash": false, "status": "success" }}If the catalogueβs max_ledger is newer than the nodeβs current ledger, the load also advances the nodeβs last-closed ledger to it. For typical archival loads of old history this doesnβt happen.
Monitoring progress
Section titled βMonitoring progressβPoll catalogue_status to watch a running job:
xahaud --conf /etc/xahaud.cfg catalogue_status{ "result": { "job_status": "job_in_progress", "job_type": "catalogue_load", "min_ledger": 1000000, "max_ledger": 2000000, "current_ledger": 1015234, "percent_complete": 1, "elapsed_seconds": 480, "estimated_time_remaining": "8 hours 37 minutes", "file": "/data/catl/1m-2m.catl", "file_size_human": "2.50 GiB" }}If current_ledger keeps advancing, the load is healthy; if itβs stuck, check the nodeβs log. When nothing is running, job_status is "no_job_running".
Verifying pinned history
Section titled βVerifying pinned historyβserver_info reports the overall range and the pinned subset:
xahaud --conf /etc/xahaud.cfg server_info{ "result": { "info": { "complete_ledgers": "1000000-2000000,95000000-95001500", "complete_ledgers_pinned": "1000000-2000000", "server_state": "full" } }}complete_ledgersβ everything the node can serve (pinned history plus the rotating window).complete_ledgers_pinnedβ just the permanently retained ranges.
To inspect the persisted ranges directly (the nodeβs SQLite/database_path directory holds state.db):
sqlite3 /data/state.db "SELECT RangeSet FROM PinnedLedgers WHERE Key = 1;"Reclaiming space
Section titled βReclaiming spaceβThe persistent backend is never rotated, so it grows as you add pinned history β monitor its disk usage. Note the on-disk size is not the catalogue file size: the file holds compressed deltas, while the backend stores reconstructed node data, so budget for more than the .catl size.
There is no command to un-pin a range. To reclaim the space you must stop the node and remove the pinned store manually.
How pinning interacts with online_delete
Section titled βHow pinning interacts with online_deleteβonline_delete rotates the NodeStore every online_delete ledgers. Pinning changes that in two ways:
- Pinned data sits outside rotation. Itβs written to the persistent backend and never rotated or copied, so pinned ranges always survive.
- SQL pruning skips pinned ranges. When trimming the relational tables, sequences inside pinned ranges are kept. Because pinned ranges can sit mid-window, pruning may loop over several disjoint intervals.
Troubleshooting
Section titled βTroubleshootingβ| Symptom | Cause / Fix |
|---|---|
Wonβt start: type must be specified when using pinned_type | Add a rotating type= to [node_db]. |
Wonβt start: pinned_path is required when pinned_type is set | Add pinned_path=/path. |
Wonβt start: pinned_type requires online_delete ... | Set online_delete (non-zero, and see its floor below). |
Wonβt start: pinned_type must be a durable backend ... | Use nudb or rocksdb for pinned_type. |
Wonβt start: online_delete must be at least 256 | Raise online_delete to β₯ 256 (8 in standalone). |
Wonβt start: online_delete must not be less than ledger_history | Raise online_delete to β₯ ledger_history, or lower ledger_history. |
Wonβt start: Persisted pinned interval ... is not present ... | pinned_path/pinned_type no longer points at the store with that data. Restore it, or clear the range in state.db (see Reclaiming space). |
catalogue_load fails immediately about pinned_type | Configure pinned_type before loading. |
error: "busy" | Another catalogue job is running. Poll catalogue_status and retry when idle. |
output_file already exists and is non-empty | Use a fresh path; create wonβt overwrite. |
error: "ledgerMissing" (partial file left) | A ledger in the range wasnβt held. Delete the partial file and create from a range the node fully holds. |
catalogue hash verification failed / network ID mismatch / file size mismatch | File is corrupt or built for another network. Get a fresh copy. |
Catalogue file contains a corrupted ledger. / Unexpected ledger out of sequence ... / Unexpected end of catalogue file. | File is damaged or truncated. Get a fresh copy; ledgers loaded before the failure stay pinned, so re-run to continue. |
| Loaded history not queryable yet | In network mode, wait for server_state: "full". |