Running a node

What pip install btclib-node puts on PATH, run against each of the four chains; pointing it at a peer of your own; reading its progress while it syncs; the RPC methods it answers; what it validates and what it does not; and what a mainnet sync has actually been measured to cost. README.md and SECURITY.md are the other two pages a new reader wants first — this one is what comes after pip install.

Starting a node

pip install btclib-node
btclib-node -h

btclib-node -h lists every flag, spelled the way Bitcoin Core spells its own — a single dash, -datadir rather than --datadir, though the double-dash spelling is accepted too. One flag selects the chain:

btclib-node                # mainnet, the default
btclib-node -testnet
btclib-node -signet
btclib-node -regtest

Data lives under -datadir=<dir> (~/.btclib if it is not given), inside a subdirectory named for the chain — <dir>/mainnet, <dir>/testnet, <dir>/signet, <dir>/regtest — so the four chains never share one store and switching between them on the same -datadir loses nothing.

An operator’s existing bitcoin.conf is read from the data directory without being told to, -conf=<file> naming another one; a [section] per chain, main included, plus one default section that applies to every chain, is the same shape Core’s own reader uses, and the command line always wins over the file. -prune is accepted, and ## Pruning below is what any nonzero value actually does.

Pruning

-prune=<n> matches Core’s own three-way split. <n> of 1 is manual pruning: nothing is deleted on its own, and the pruneblockchain RPC is what deletes block and undo data up to a height or a timestamp, both answering the height of the last block actually pruned. <n> at or above 550 is automatic pruning to roughly <n> MiB on disk, tracked against actual bytes under blocks/ and re-checked after every block this node connects. Neither ever reaches within the last 288 blocks (about two days) of the tip – getblockchaininfo’s own pruned, pruneheight, automatic_pruning and prune_target_size answer for what a caller wants to check programmatically, pruneheight being the first height still on disk. <n> from 2 to 549 refuses to start, Core’s own wording – too small a target to actually run a node on. A negative <n> refuses to start too, rather than silently pruning.

A pruned node tells a peer so: NODE_NETWORK_LIMITED on its own version, NODE_NETWORK dropped, matching what Core’s own pruned node advertises. getrawtransaction’s block-hash lookup and a peer’s own getdata both answer for a block this node has since deleted the way Core’s peer does – "Block not available (pruned data)" over RPC, no answer at all, or the connection dropped outright, over the wire. -connect/-addnode still dial whatever is named; a pruned node serving another pruned node is between the two operators to arrange, same as it is for Core.

Pointing it at a peer of your own

-connect=<ip>[:port], repeatable, dials only the peers named this way. Naming one turns off DNS seeding and every automatically-drawn outbound connection — Core’s own -connect interaction — and defaults -listen off too, unless -listen/-nolisten overrides it explicitly. -addnode=<ip>[:port] dials a peer alongside the ordinary draw rather than instead of it. Only a literal IP address is accepted in either; a hostname is refused rather than resolved (#573).

This is the answer to “I already have the chain on another node, can this one validate it against that copy instead of the internet”:

btclib-node -regtest -datadir=<dir> -rpcport=<port> \
  -connect=127.0.0.1:<the peer node p2p port>

Reading progress

getblockchaininfo answers headers (the height of the best header chain this node knows, downloaded or not) and blocks (the height of the chain it has fully validated) as two separate members, plus initialblockdownload, a boolean matching Core’s own definition — chain work against a minimum and the tip’s own age against a staleness bound, not merely “out of candidates to try right now.” getblockcount answers blocks alone, so it does not move during header sync: headers is what to watch while the chain of headers extends ahead of the blocks arriving behind it. verificationprogress is deliberately absent rather than answered wrong — Core estimates it from a per-chain assumed transaction rate, and neither that assumption nor the per-block count it is checked against exists in this tree yet (#575, which added the other members above).

The listener authenticates nothing (SECURITY.md), so a call needs no credential:

curl -s http://127.0.0.1:<rpc port> \
  -d '{"jsonrpc":"2.0","id":"1","method":"getblockchaininfo"}'

RPC methods

Fourteen, each mirroring the Core method of the same name: getbestblockhash, getblockcount, getblockchaininfo, getblockhash, getblockheader, getpeerinfo, getconnectioncount, getmempoolinfo, getrawmempool, getrawtransaction, testmempoolaccept, sendrawtransaction, ping, stop.

What is validated, and what is not

Every header’s proof of work, and its retarget and median-time-past against its ancestors; a block’s own structure against its difficulty bound, on receipt; every script and every signature in it; a coinbase that pays no more than subsidy plus fees and commits to its own height under BIP34 (#568 and #571); a spend of a coinbase not yet COINBASE_MATURITY deep (#569); the same coinbase landing twice, BIP30 (#570); and a transaction’s own nLockTime together with its BIP68 relative lock (#572). All five of those were open questions this tracker carried and are now closed.

Pruning is accepted as a flag and refused rather than honoured — see Starting a node above (#601). The UTXO set carries no commitment a caller can audit against (#639), and this node answers no gettxoutsetinfo of its own.

What a sync costs

The first initial block download this tree has measured (#576). Machine throughout: darwin 25.6.0, ten cores, CPython 3.14.6 (GIL build), btclib-secp256k1 installed. The peer for the header sync and the block-validation figures was a local, fully-synced bitcoind dialled over loopback as the only peer, -connect, DNS seeds off — none of it crossed the internet.

Header sync

Measured 2026-08-27 at origin/main 8dfd9d82b5520547cf8df3e4c8b9c229264a7889, load 3.7 and 5.3: 964,357 headers — the peer’s own height plus genesis — in 40.1 seconds, twice, to the tenth of a second, every header’s proof of work and retarget arithmetic checked, no checkpoint and nothing assumed valid.

Block validation

Measured the same day, origin/main 7da40ebcd606a7d3ef09fe2a49a2d6d63017c8b1. Six populated blocks up to mainnet height 964,000, their prevouts pulled from the peer over RPC and handed to the same call update_chain itself makes (interpreter.check_transactions, no download and nothing written): roughly 40,000 to 65,000 inputs validated a second, at a load around 3.0 — about 200 microseconds per input per core, one libsecp256k1 verification and the script engine around it. A control against the same block with one prevout’s script swapped for another’s was refused. A second range — the emptiest 136,000 blocks from genesis, downloaded and validated for 750 seconds and stopped by hand — showed a blocks-per-second rate falling threefold, but it correlates with the machine’s own rising load at -0.68 across that same run and is not offered here as an independent cost figure; the store measurement below, taken under a load held level, is what that falling rate was actually showing.

The store

Measured 2026-08-28 at origin/main 0f33f69d711169cab8a2431a1cce5a83c65fc65a, against this node’s KeyValueStore filled to a range of sizes and then given one mainnet block’s own reads, deletes and writes — on the sqlite3 store this tree used at the time; the store has been RocksDB since #641, and this figure has not been re-measured against it, there being no committed script in this tree to rerun. Below about four million rows the store is write-bound and the cost per operation is nearly flat; a cliff sits between eight and sixteen million rows, where reads and writes both jump because the working set stops fitting the page cache — measured at 2.9 to 16.2 microseconds an operation at eight million rows, and 29.3 to 55.0 microseconds at sixteen million. The peer’s own gettxoutsetinfo put the real UTXO set at 165,770,908 outputs, about ten times the largest size measured here, so every figure below is a floor and not a forecast. Spends were drawn uniformly across the whole key space, the worst case a chain can present; drawing 90% of them from the newest 1% of keys — the locality a real chain has and this measurement otherwise ignores — bought 8 to 15%, not the gap between this floor and a real cost.

What the whole chain would cost

A sampled input count — 193 samples of getblockstats, mean 3,655.9 inputs a block — puts the chain at roughly 3.5 billion inputs and 7.2 billion store deletes-and-writes. At the input rate above, script validation alone is on the order of a day, spread across ten cores. The store is the larger half and it is single-threaded: at the eight-million-row cost above it comes to about 1.5 days, and at the sixteen-million-row cost, about 5.8 days — both measured on a store a tenth the size a real sync’s store reaches, so both are floors on the true figure rather than an estimate of it.