Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Initial Block Download & Fast Sync

This chapter covers getting a satd node to the chain tip: AssumeUTXO fast sync, the assumevalid script-verification skip options, dual-engine shadow verification, and the IBD performance and storage tuning flags. Points that differ from Bitcoin Core are called out throughout.

For the per-key table of defaults, reload disposition, and Core-vs-satd status, see the Configuration Flag Reference. This chapter explains how the pieces work and when to use them.

How satd syncs: the IBD pipeline

satd does not download and verify blocks one at a time in lockstep. IBD is a pipeline that keeps the network, the disk, and the CPU busy at the same time.

  • Parallel block download. satd fetches blocks from many peers at once, the way BitTorrent downloads pieces from a swarm. Download throughput scales with aggregate peer bandwidth instead of one peer's round-trip time. -maxahead bounds how far ahead of the connect tip downloaded blocks may be staged.
  • Background prefetch workers. -prefetchworkers threads pre-read blocks from the flat files without holding the chainstate lock. They deserialize each block, compute txids, run the context-free transaction validation, and resolve the block's UTXO inputs into the coin cache. All of this happens off the connect thread. When the connect thread reaches a block, its inputs are already cached.
  • Speculative script verification. Prefetch workers verify scripts ahead of connection and mark the transactions they verified. The connect thread does not verify them again. In assumevalid mode, the connect step can then go straight to applying UTXO changes for the trusted range.
  • Asynchronous shadow verification. The second consensus engine (see below) runs on its own worker pool, never on the connect path. It adds almost no wall-clock cost.

Network I/O, block pre-processing, script verification, and chainstate writes overlap instead of running one after another.

AssumeUTXO fast sync

AssumeUTXO makes a node usable in minutes instead of days. The node loads a UTXO-set snapshot at a recent height and serves wallets and queries from it immediately. It validates the historical chain from genesis to the snapshot in the background. satd's implementation is shipped and Bitcoin Core-compatible.

Loading a snapshot

  • loadtxoutset <path> (RPC) loads a UTXO snapshot file. The node then holds two chainstates. The snapshot chainstate becomes the active tip; wallets, Esplora, Electrum, and RPC serve from it immediately. A background chainstate validates from genesis up to the snapshot's anchor. When background validation completes, the snapshot is marked validated and the node is a normal fully-validated node.
  • --fast-start=<url|path> (startup flag) automates the sequence. satd downloads the snapshot (or reads a local file), waits for header sync to reach the snapshot's anchor, and calls loadtxoutset itself. Remote sources must be https://; plain http:// is refused, and TLS certificates are verified. Use --fast-start-sha256=<hex> to pin the download's digest. Download progress appears in the pre-RPC startup TUI gauge.
  • getchainstates (RPC, Core 27+ compatible) reports progress. A node with no snapshot reports a single fully-validated chainstate. After loadtxoutset it reports a second, background chainstate, and the snapshot entry carries snapshot_blockhash and validated: false until background validation finishes.
  • dumptxoutset <path> (RPC) writes a Bitcoin Core-compatible UTXO snapshot from your own node.

Trust model

At load, satd verifies the snapshot file against a hardcoded anchor hash. A tampered or wrong-height snapshot is rejected no matter where it came from. satd hosts no snapshots and does no P2P snapshot fetch; the operator names a trusted https:// source or a local file. The historical chain is still fully validated in the background. AssumeUTXO shortens the time to a usable node; it does not skip validation.

Difference from Bitcoin Core. The RPCs (loadtxoutset, getchainstates, dumptxoutset) and the two-chainstate model match Core. The --fast-start download-verify-load flag and --fast-start-sha256 are satd extensions. Core requires a manual loadtxoutset against a file you fetched yourself.

Where to get a snapshot

satd hosts none, and does not name one for you. The anchors compiled into the binary decide which snapshots are loadable at all — currently mainnet heights 840,000, 880,000, 910,000 and 935,000, copied verbatim from Bitcoin Core's m_assumeutxo_data. Signet, testnet and regtest have no anchors, so fast-start is mainnet-only.

Several people publish the utxo-<height>.dat files Core's dumptxoutset produces; Jameson Lopp's mirror and https://bitcoin-snapshots.jaonoctus.dev/ are two that have been around a while. Any of them will do, because the host is trusted for availability only:

satd --fast-start=https://<host>/utxo-880000.dat \
     --fast-start-sha256=<sha256 of that file>

--fast-start-sha256 pins what you downloaded, so a truncated or swapped file fails before it is parsed. That check is a convenience; the one that matters is the anchor comparison above, which satd performs against a hash compiled into the binary and which no snapshot host can influence. A snapshot from a hostile mirror is rejected at load.

Pick the highest anchor height a published snapshot exists for: the higher the base, the less history the background validation has left to walk.

--fast-start-sha256 is the file's SHA-256, not the anchor hash. hash_serialized_3 in the anchor table is a hash over the UTXO set, not over the file; sha256sum utxo-880000.dat does not produce it. Take the file digest from the publisher, or compute it after downloading once.

Script-verification skip: assumevalid

-assumevalid controls how much script verification IBD performs. satd accepts three forms. The third is a satd extension.

ValueMeaningCompat
-assumevalid=<blockhash>Skip script verification at or below that block. The hash must already be in the block index. A per-network default ships in the binary (for example, mainnet height 840,000), as in Core.Core
-assumevalid=0Verify everything; no skipping.Core
-assumevalid=allSkip script verification for blocks older than a cutoff age; verify recent and new blocks in full. The cutoff is -assumevalidage (default 86400 s, 24 h).satd extension

Difference from Bitcoin Core. Core's -assumevalid takes a block hash or 0. satd adds the all keyword and -assumevalidage, which trust the deep chain and verify the last day without pinning a hash. This suits recurring fast re-syncs. assumevalid is independent of AssumeUTXO, which concerns the UTXO set rather than script verification; the two compose.

Consensus engine & shadow verification

satd ships two independent script-verification engines: the C++ libbitcoinconsensus FFI and a from-scratch Rust verifier. It can run both together and verify every script twice. Bitcoin Core has no equivalent.

Read a mode name as "which engine is the shadow". In <engine>-shadow, the named engine is the shadow, the non-authoritative one. The other engine is primary; its verdict is what the node acts on. The shadow re-verifies in the background and logs any disagreement. So:

  • rust-shadow: the Rust engine is the shadow; C++ is primary.
  • cpp-shadow: the C++ engine is the shadow; Rust is primary.

-consensus=<mode>:

ModePrimary (authoritative)Shadow
rust-shadow (default)C++ libbitcoinconsensusRust (logs mismatches)
cpp-shadowRustC++ (logs mismatches)
cppC++ libbitcoinconsensusnone (single engine)
rustRustnone (single engine)

The Rust engine passes Bitcoin Core's script test suite. Shadow verification against libbitcoinconsensus across the whole mainnet chain, genesis to about height 945,000, found zero divergence. The Rust engine is also usually faster than the C++ FFI: it avoids per-call FFI marshaling and uses a process-global, verification-only cached secp256k1 context. cpp-shadow (Rust primary, C++ shadow) is therefore the high-performance pairing.

rust-shadow (C++ primary) stays the default out of conservatism. Running two independently written engines against each other is satd's core safety property, and libbitcoinconsensus is the most widely deployed implementation. The plan is to promote the Rust engine to primary as it accumulates production mileage; cpp-shadow is that step. Treat the single-engine rust mode with care. The engine itself is proven, but either single-engine mode gives up the dual-engine cross-verification. satd prints a caution at startup when the single-engine rust mode is selected.

The shadow engine runs on a bounded background worker pool, so it consumes spare CPU without slowing block connection. Two flags tune it:

  • -shadowworkers=<n> (default 4): background shadow-verification threads.
  • -shadowqueuesize=<n> (default 4194304): shadow work-queue capacity. When the queue is full, shadow work is dropped, and an aggregated WARN is logged at most once per 5 s. The primary engine still verifies every script, so correctness is unaffected.

Difference from Bitcoin Core. Core has a single C++ engine and no shadow mode. satd's default runs both engines at once. -consensus, -shadowworkers, and -shadowqueuesize are satd-specific.

IBD performance & storage tuning

These flags bound or accelerate IBD. Full defaults and semantics are in the Configuration Flag Reference.

FlagDefaultNotes
-dbcache=<MB|auto>450Write-cache size. auto (satd) starts a controller that resizes the RocksDB block cache and CoinCache against /proc/meminfo pressure. Core's -dbcache is a static number only.
-par=<n>unsetScript-verification threads (Core name). satd's connect path manages its own parallelism, so -par does not size it directly. When -shadowworkers is unset, a positive -par value is used as the shadow-verification worker count; otherwise the default of 4 applies.
-prefetchworkers=<n>CPU cores(satd) IBD block-prefetch worker threads.
-maxahead=<n|N%|all>50000(satd) How many blocks IBD may stage ahead of the connect tip.
-storageprofile=<ssd|hdd>ssd(satd) RocksDB tuning class for the storage medium.
-maxopenfiles=<n>2048(satd) RocksDB max_open_files cap (-1 = unlimited).
-rocksdbbackgroundjobs / -rocksdbsubcompactions / -rocksdbwalmbfrom profile(satd) Advanced RocksDB overrides.
-compactionl0at=<n> / -ibdl0pauseat=<n>16 / 64(satd) Force chainstate compaction at N L0 SST files; pause the IBD connector at N L0 files so compaction can catch up.
-compactionintervalsecs / -compactiondiagintervalsecs1800 / 60(satd) Periodic forced compaction and pending-compaction diagnostics (0 disables).
-stallwatchdogsecs / -stallabortsecs300 / 300(satd) If the tip does not advance for N seconds, dump forensics, then abort after a further grace period. A silent IBD wedge becomes a loud, debuggable failure.

-dbcache, -prune, -txindex, -assumevalid, and -reindex keep Core's names and meanings. The rest of the table is satd-specific tuning with no Core equivalent.

Reindexing

  • -reindex rebuilds both the block index and the chainstate from the block files on disk (Core-compatible).
  • -reindex-chainstate rebuilds only the chainstate (the UTXO set) from the existing block files, and preserves the flat block files (Core-compatible). It is faster than a full -reindex when only the chainstate is suspect.

Both work against block files written by Bitcoin Core, including the XOR-obfuscated files Core v28.0+ produces by default. The key in blocks/xor.dat is picked up automatically (see blocksxor in the Configuration Flag Reference).

A reindex on a synced mainnet node runs for hours. The shipped systemd unit handles this without tripping the start timeout; see "Reindex resilience" in Packaging.

Driving a reorg by hand

invalidateblock and reconsiderblock work as in Bitcoin Core, and reach the node through sat-cli's raw-RPC passthrough:

sat-cli invalidateblock <blockhash>
sat-cli reconsiderblock <blockhash>

They are not listed in sat-cli --help — any method --help does not name is forwarded verbatim, which is how Core-compatible tooling keeps working. That makes them easy to miss when they are the tool you need.

Invalidating drives a reorg away from the named block and everything descended from it. reconsiderblock clears the mark and re-activates the best chain.

Startup integrity checks

Before serving RPC or connecting to peers, satd checks two things about the chain it is about to present.

The height→hash index is audited for gaps at or below the tip and rebuilt in place from the tip's ancestry. It is derived state, so this is a repair and startup continues. Heights whose rows disagree with the tip's ancestry are logged but never overwritten — correcting one means choosing between branches by chainwork.

The tip's ancestry is walked back one retarget period, and every block in it must be one this chainstate actually connected. This is not repaired. A block in the tip's ancestry that was never connected means the UTXO set is missing every output it created, and the only way to recover those is to replay the block. satd reports the affected heights and exits:

FATAL: this node's UTXO set does not agree with the chain its tip claims.

  * The tip stands on 8 block(s) that were never connected.
...
Refusing to start. Rebuild the UTXO set with -reindex-chainstate.

Serving in that state is worse than not starting: the tip is a real block on the real chain, the height is correct, and gettxout answers confidently and wrongly.

Two distinct faults can be reported, and they do not share a remedy. Blocks that were never connected mean the UTXO set is missing deltas, which -reindex-chainstate rebuilds. A broken parent pointer means the block index itself is wrong; -reindex-chainstate trusts that index, so only a full -reindex fixes it. Both can be present at once, and both are printed.

The exit status is 3, distinct from the 1 used for ordinary startup failures such as a bad config key, so supervision and alerting can tell "chainstate is damaged" from "the config file has a typo" without scraping stderr. Because this never heals by restarting, the shipped units set RestartPreventExitStatus=3 alongside Restart=always; without it the node would restart every few seconds forever and the unit would never settle into failed, so unit-state alerting would never fire. If you wrote your own unit, add that line.

After a -reindex or -reindex-chainstate, the audit runs again against what the replay actually rebuilt — a replay that stops short or reproduces the hole fails the same way, in the same run, rather than serving until the next restart.

On a pruned node none of these remedies can replay the missing blocks, because the block data is gone. satd says so rather than naming a remedy that cannot work: fetch the affected heights from a peer with getblockfrompeer and repair them, or resync.

On an AssumeUTXO node the history below the snapshot base is legitimately unvalidated until the background chainstate reaches it. That is recognised and logged at INFO, not treated as damage.

Auditing a suspect datadir offline

satd-chainstate-audit answers the question the startup checks cannot afford to: does the UTXO set actually agree with the blocks on the active chain? It walks the tip's ancestry, reads each block back from the flat files, and reports every disagreement — coins that should exist and do not, spent coins still present, height-index rows naming the wrong block, txindex rows pointing at the wrong block, cumulative transaction counts that do not follow from their parent.

satd-chainstate-audit --datadir /path/to/datadir
satd-chainstate-audit --datadir /path/to/datadir --window 20000 --verbose

It takes the RocksDB lock, so the node must be stopped.

It issues no writes of its own, but it is not non-mutating: opening the chainstate opens RocksDB read-write, so the WAL is replayed and truncated, memtables may flush and compact, the MANIFEST is rewritten, obsolete files are deleted, any missing column family is created, the legacy address-history column families are dropped, and the schema version is stamped — after which an older satd will no longer open that datadir. Opening the block files creates xor.dat if absent. If the datadir is evidence — which is the case this tool exists for — copy it and audit the copy. The tool prints this warning on every run.

Note also that it is not included in the release tarballs or the Docker image; build it from source (cargo build --release --bin satd-chainstate-audit).

Exit status is 0 when consistent, 1 when it could not run, 2 when it found inconsistencies, so it scripts cleanly.

It diagnoses and does not repair. A missing coin is recoverable only by replaying the block that created it: -reindex-chainstate, or satd-chainstate-repair for a single block's lost delta. A broken parent pointer is a block index fault and needs -reindex-reindex-chainstate trusts the same block index and cannot fix it.

--window bounds the walk, and its cost is not only one block read per height: every output the window creates and every outpoint it spends is held in memory until the end, so the default already runs to roughly a gigabyte on mainnet and tens of thousands of blocks runs to many. Start at the default and widen only as far as the search needs.

There is no --txindex flag: the tool reads the answer out of the datadir. An absent txindex row counts as a fault only when the chainstate's own completeness marker says the index was fully built, which rules out both shapes that would otherwise produce a false alarm — a node that does not run -txindex at all (satd's default), and one where -txindex=1 was switched on after the chain had already synced without it, leaving every historical block without a row it was never going to have. In that second case the audit says the rows went unchecked rather than counting them clean, because "not looked at" and "looked at and fine" are different answers.

This used to be a flag, and it was wrong in both directions. It defaulted to true while satd's -txindex defaults to off, so the invocation the node itself prints reported every transaction in the window as a missing row and exited 2 against a perfectly healthy node; and passing false silently disabled the txindex checks altogether, so a genuinely broken index came back consistent. An auditor cannot be expected to know a stranger's -txindex setting, and now does not have to.

Two states are reported but are not faults. Blocks the node pruned are counted separately from blocks that could not be read: pruning deletes block data deliberately, and treating that as damage would fail every healthy pruned node — at the default window, for most of the range — and then recommend -reindex-chainstate, which a pruned node refuses outright. On an AssumeUTXO node the snapshot base is read from the background chainstate's marker, so history below it is reported as not-yet-validated rather than as a hole.

Where a block could not be read, for either reason, the coin checks are skipped at and below that height — its spends are unknown, so a coin it spent would otherwise look missing. The tool prints a note when this applies. Verdicts above that height are unaffected: the walk runs newest-first, and a coin created at height H can only be spent at or above H.

Differences from Bitcoin Core at a glance

  • assumevalid=all with assumevalidage: verify-recent-only mode. Core takes a hash or 0.
  • Dual-engine shadow verification (-consensus, -shadowworkers, -shadowqueuesize): the default runs the C++ and Rust engines together. Core has one engine.
  • --fast-start / --fast-start-sha256: one-flag AssumeUTXO download-verify-load. Core requires a manual loadtxoutset.
  • -dbcache=auto: adaptive cache sizing. Core's is static.
  • satd-only IBD and storage options: -prefetchworkers, -maxahead, -storageprofile, -maxopenfiles, the -rocksdb* and -compaction* families, -ibdl0pauseat, and the stall watchdog.
  • -par is accepted for config compatibility. It does not size the connect path, but a positive value feeds -shadowworkers when that flag is unset.