This guide covers installing, configuring, running, upgrading and
troubleshooting the bmcbitcoind daemon of Bitcoin Machine Code on a Linux
x86-64 host under systemd. It describes the system as it is; what the node
does and does not implement is in README.md and docs/FEATURE_GAPS.md.
One binary, asm/daemon/bmcbitcoind (named bitcoind until 2026-09-07; the sources are still bitcoind.asm), runs the node as a fork-model daemon:
- a parent accepts inbound peers (one forked serve child per connection) and runs the embedded JSON-RPC server on its own thread;
- a forked download worker owns the outbound peer legs, block download and keep-up, UTXO application and reorg handling;
- a background UTXO compaction forks a short-lived child per merge; it
appears as a second
bmcbitcoindprocess with the same command line.
Everything the node persists lives in one datadir, and every chain gets its
own subdirectory of it: <datadir>/main, <datadir>/signet,
<datadir>/testnet4, <datadir>/regtest. bitcoin.conf is shared at the
datadir root (or ../config/bitcoin.conf); all other state is per chain, so
two chains never share block, UTXO or wallet state.
The companion client is asm/daemon/bmc_cli. With -datadir=<dir> it
reads the chain, RPC port and credentials from that datadir's bitcoin.conf
and cookie, so no other flags are usually needed.
- Linux x86-64 (NASM ELF64, System V ABI).
nasm,gcc,make,python3(build, test suite, build audits).- Disk: the mainnet block archive is about 0.7 TB at the current height
(713 GB across 5,733
blk*.datfiles) and grows with the chain; the UTXO store is about 13–15 GB; the optional indexes add about 27 GB (the txid index), 91 GB (the txo-spender index), ~200 GB (the address history) and 13 GB (the block filters), each as sorted run files the daemon builds during the sync (2026-09-16,docs/devlog/INDEX_RUNS.md). The archive re-layout tool needs archive-sized scratch space (about 1.1 TB). Pruned mode fits in a few GB. - RAM: several GB; the initial-sync memtable is sized from
dbcache(default 450 MiB, Core's; the benchmarks use 8192). As in Core,dbcacheis the total: with the async flush the live memtable and the flush writer's frozen copy get half each (2026-10-08, plan M2). - Optional:
tor(SOCKS 9050, control 9051),i2pd(SAM 7656),cjdroute.
cd /path/to/repo/asm
make runtime # the daemon, bmc_cli, and every helper the daemon execs
make daemon/bmcbitcoind # the daemon alone (about 31 MB)
make daemon/bmc_cli # the RPC client
make tests/tool_archive_relayout # archive maintenance tool (optional)The daemon runs its index builders (bmc_build_tx_index,
bmc_build_txospender_index, bmc_build_addr_hist,
bmc_build_coinstats_hist, bmc_merge_index_runs) from its own directory.
Without them it keeps running and logs builder ... not executable, and the
configured indexes are never folded into runs; make runtime builds the whole
set (make print-runtime-helpers lists it).
asm/build.sh is an equivalent wrapper. The build uses gcc -no-pie -O2
and nasm -f elf64.
The reference deployment runs the service as an unprivileged user with:
| path | purpose |
|---|---|
/path/to/repo/asm |
service working directory; binaries under asm/daemon/ |
/path/to/repo/data |
the datadir (root of all chains) |
/path/to/repo/config/bitcoin.conf |
configuration (git-ignored) |
/path/to/repo/logs/<chain>/bitcoin.<chain>.log |
console (stdout/stderr) log per chain |
/etc/bmc/wallet.pass |
wallet passphrase file, outside the datadir (optional) |
Create the datadir and logs/<chain> before the first start. The datadir
may be a symlink or mount point; the daemon resolves it with realpath and
changes directory into <datadir>/<chain>.
A first start on an empty datadir bootstraps itself: DNS-seed discovery,
headers-first sync, parallel block download, then a from-genesis UTXO build
with full script verification. Expect days, CPU-bound on signature
verification. The pulse is [dl] heartbeat: tip=<h> peers=<live>/<wanted> txouts=<utxo count> uptime=<d:hh:mm:ss>.
One kernel setting, applied on the reference host 2026-09-10 and kept in
/etc/sysctl.d/90-bmc-tcp.conf:
net.ipv4.tcp_slow_start_after_idle = 0
By default Linux resets a TCP connection's send window to its initial 10 segments after one round trip of quiet. A block connection is quiet for ten minutes between blocks, so every block this node serves to an inbound peer (and every blocktxn reply) started from the smallest window: a 40 KB compact block took three round trips and an 800 KB blocktxn seven on a 0.8 s peer, measured on a passive capture. With the setting off, a connection keeps the window it earned. It governs what THIS host sends; a remote peer's kernel decides how fast it sends to us. It applies to every socket on the box, the Core oracle and a benchmark run included.
bmcbitcoind serve <datadir> resolves its configuration in this order:
a -conf=<path> argument (an unreadable file is fatal, never a fallback);
$BITCOIN_CONF; <datadir>/bitcoin.conf if readable;
<datadir>/../config/bitcoin.conf. The path used is echoed at boot as
[config] loaded <path>: N setting(s) applied, followed by a [config]
block with every resolved value (conns, utxo, pool, mpol, net,
chain). Check it after any change; values are read once at start-up.
Unknown keys are ignored with a message; a Core option the node does not
implement is named at boot as having no effect.
config/bitcoin.sample.conf lists every key with its default. Keys are
Bitcoin Core's names and defaults unless the comment says otherwise.
chain=main # main | testnet4 | signet | regtest (testnet4=1 / signet=1 / regtest=1 also accepted)
# signetchallenge=<hex> # CUSTOM signet; also determines the network magic
port=8333 # P2P; chain default if omitted (see Reference)
rpcport=8332 # RPC; chain default if omitted
listen=1 # IPv4 and IPv6 listeners on `port`chain=test/testnet (testnet3) is refused. A custom signet gets no
chain-work floor and no DNS seeds. rpcport must differ from port: the
P2P listener binds first and the RPC server then loses its bind, leaving the
node without RPC. A fresh non-main chain directory self-seeds its genesis.
- Cookie (default).
<datadir>/<chain>/.cookieis written mode 0600 at start (__cookie__:<64 hex>) and deleted on shutdown.rpccookiefile=relocates it. rpcauth=user:salt$hash(repeatable, up to 8) for a fixed credential; a malformed entry is ignored with a log line.rpcuser=/rpcpassword=work too but keep a plaintext secret on disk.- The server starts if any credential source is available and refuses to start when none is.
- Bind and ACL. The listener binds
127.0.0.1.rpcbind=is honoured only with at least onerpcallowip=(up to 16 subnets;127.0.0.0/8and::1are always allowed); otherwise the log saysOption -rpcbind was ignored because -rpcallowip was not specified. A malformedrpcallowipis fatal. - Peer permissions (2026-09-01).
whitelist=[perms@]<subnet>andwhitebind=[perms@]addr:portgrant Core'snoban,relay,forcerelay,mempool,download,addr(andin); a bare entry gets Core's implicit set (noban, mempool, relay perwhitelistrelay, forcerelay perwhitelistforcerelay).bloomfilterandoutare refused at start-up with the reason.blocksonly=1refuses transactions from every peer withoutrelay(disconnect, not scored, Core's log words), sends fRelay=0 and no feefilter, and reportslocalrelay:false;inboundrelaypercentcaps the inbound peers that negotiate tx relay (only relaying peers count).getpeerinfonow itemizes inbound peers (slots 64..127 of the shared table) withpermissionsand a truthfulrelaytxes.
An encrypted wallet (bmcwallet.enc) is adopted locked at boot. To unlock it
automatically, provide a passphrase source, in order of precedence:
$BMC_WALLET_PASS(visible insystemctl showand/proc/<pid>/environ);walletpassfile=<absolute path>inbitcoin.conf.
The file is refused, with a [wallet] walletpassfile ... not usable: <why>
line, if it is not absolute, is inside the datadir, is world-accessible
(any 007 bit), is group-writable, is not a regular file, or is empty. The
first line is the passphrase. The intended shape:
sudo install -d -m 0755 /etc/bmc
sudo install -o root -g <service-group> -m 0640 secret /etc/bmc/wallet.passA legacy <store>.pass beside the wallet is not read and is warned about.
proxy=127.0.0.1:9050 # SOCKS5 for every network
onion=127.0.0.1:9050 # SOCKS5 for .onion only (defaults to proxy; "0" disables)
onlynet=onion # restrict networks (repeatable): ipv4 ipv6 onion i2p cjdns
discover=0 # do not learn/announce our own address
torcontrol=127.0.0.1:9051 # tor control port: creates OUR onion service
torpassword= # only if tor uses HashedControlPassword
i2psam=127.0.0.1:7656 # SAM bridge; empty = i2p off
cjdnsreachable=1 # the cjdns tun (fc00::/8) is present
v2transport=1 # BIP324 encrypted transport, default onproxy=(unlikeonion=alone) also stops resolver lookups for peer names, skips the DNS seeds, randomises SOCKS5 credentials per connection (one Tor circuit each), and never advertises a clearnet address to onion or I2P peers. Seed the address book or useaddnode=/connect=.- Onion service. With
torcontrol=(andlisten=1,listenonion=1) the node authenticates on the control port (cookie file named by PROTOCOLINFO, ortorpassword), issuesADD_ONIONwith a key persisted in<chaindir>/onion_v3_private_key, and listens on a loopback socket at the chain default P2P port + 1 (127.0.0.1:8334on mainnet). The service is ephemeral and exists only while the daemon holds the control connection. It takes a few minutes after start; the confirming line is[tor] onion service <addr>.onion:<port> -> 127.0.0.1:<port+1> (key onion_v3_private_key). The service account needs read access to tor's control cookie: under systemd,SupplementaryGroups=debian-tor(group membership in the user database is not applied to a service). Outbound onion needs onlyonion=/proxy=. - I2P.
i2psam=opens a SAM session with a destination persisted in<chaindir>/i2p_private_key; boot line[dial] i2p session up via SAM <ip:port>, our address <b32>.b32.i2p. Withlisten=1an acceptor thread sits in SAMSTREAM ACCEPTon that session and inbound I2P streams reach the serve loop like any other inbound peer; boot line[i2p] accepting inbound streams on <b32>.b32.i2p, per-peer line[serve] inbound over i2p from <b32>.i2pacceptincoming=0turns that off. A router that reports itself firewalled cannot publish a leaseset, so no inbound will arrive until it can. - CJDNS. Needs a running
cjdroute(a systemd unit in the reference deployment; the node's unit is ordered after it) and IPv6 on the host; boot line[dial] cjdns reachable (fc00::/8 over IPv6). Inbound cjdns peers arrive on the IPv6 listener. Without IPv6:[dial] no IPv6 on this host: ipv6 and cjdns peers are unreachable. - BIP324. Inbound v2 accepted, v2 dialled to peers advertising
NODE_P2P_V2, v1 detected in band. Boot line[net] BIP324 v2 transport enabled (services=0x...); N of M known peers advertise v2.
getnetworkinfo reports per-network reachability, the onion hostname and the
I2P destination.
-
zmqpubhashblock,zmqpubhashtx,zmqpubrawblock,zmqpubrawtx,zmqpubsequencetaketcp://<interface>:<port>;tcp://*:PORTis refused, name an interface (127.0.0.1for local subscribers). A publisher has no authentication.zmqpubsequence(Core's A/R/C/D stream with the mempool sequence number) was refused until 2026-09-19. -
Address history (
addrindex=1, 2026-09-16): the daemon builds it DURING the sync. The live journal (addrindex.tail) records every funding and spend from genesis; everybmc.indexrunblocksheights (default 20,000, kept 144 below the applied height) the trailing builder folds the next range into a sorted runaddr_hist.r<from>-<to>.dat— its spends read from undo, so a run is self-contained — rotates the journal to drop what the run now covers, and merges runs at six. The Esplora/addressroutes,getaddressbalanceandgetaddresstxidsread the runs plus the journal.addrindex=1must still be set before the sync (or within the undo window of the tip): a journal that starts late cannot reconstruct historic spends. A node with the old singleaddr_hist.datkeeps it: it is simply the run that starts at 0. The whole-chain builder (daemon/bmc_build_addr_hist <chaindir> [to], ~700 GB of temp, hours) still exists for a one-off rebuild; nothing on a fresh sync needs it./address/:addr/utxoneeds the txospender index too.getaddresstxidstakes an optional inclusive height window, following the addrindex patch set's convention (Core has no address index, so that lineage is the only convention there is):getaddresstxids {"addresses": ["bc1..."], "start": 800000, "end": 810000}Both bounds are optional and the bare
getaddresstxids "<addr>"form is unchanged. The window is applied before a run event's txid is resolved, and resolving one costs a block read, so a windowed query reads only that range's blocks. Without a window the reply is capped at 100,000 txids — page withstart/endrather than relying on that cap, which truncates silently.getaddressbalancetakes no window: a balance is a property of the whole address, and it is summed over every event regardless. -
coinstatsindex=1keeps the coin statistics index live (coinstats.dat, the running MuHash and totals at the applied tip) and, since 2026-09-08, one row per committed height incoinstats_hist.dat(1 KB each: the set's counters, both MuHash accumulators, the cumulative per-block amounts), sogettxoutsetinfo <hash_type> <hash_or_height>answers at any height the rows cover, in Core's shape,block_infoincluded. A boot with no valid persisted state seeds the index from a walk of the set (minutes); rows exist from that height on in the TAIL (coinstats_hist.dat), and a query below it is refused by name until the BASE exists:coinstats_hist_base.dat, rows 0..to_height, written bydaemon/bmc_build_coinstats_hist <chaindir> [to_height] [workers]from the archive alone (about two hours and ~500 GB of scratch undercsh_tmp/on mainnet with eight workers; the daemon may keep running). The reader prefers the base for every height it covers. Its numbers are Core's coinstatsindex's: verified byte for byte at height 963,967 and at the tip. mempool.space readstxoutsandblock_info.prevout_spentper indexed block oncegetindexinforeports the index synced at that height. Off (0) the index is not maintained and the RPC refuses. The base repairs itself (2026-09-08, after a build died in an OOM and its half-written rows and 209 GB of leftovers went unnoticed): once a heartbeat the fold worker checks the base (header every time; every row, the header's hash and the seam with the tail the first time) and, when it is absent, spawns the builder beside its own executable, niced, never during initial block download, at most three attempts per boot with six hours between them;bmc.coinstatshistrepair=0turns that off. A node synced from block 0 skips it (2026-10-04): its tail starts at genesis, so after the download the fold worker checks every tail row once (hash and one generation, no re-seed in between) and answers from the tail; a torn row or a re-seed falls back to the build. A base that fails the check is renamed*.broken-<epoch>(delete those when you have looked) and rebuilt. The builder resumes at the pass after its lastcsh_tmp/passN.donemarker, discards scratch without a marker (and the old layout'scsh_*.tmp), sizes pass 3 from free RAM, holdscsh_tmp/lockso two builders never share a scratch, and writes the base to a temp name that is renamed into place with the complete flag set last.gettxoutsetinfobelow the base names the state ("being rebuilt", "waits for initial block download", ...);coinstats_hist.statusin the chain directory carries the same line. -
bmc.esploraport=<port>(2026-09-08) opens the Esplora facade for mempool.space'sBACKEND: "esplora": a second, unauthenticated listener (bmc.esplorabind, default 127.0.0.1) answering Esplora's REST routes from the node's own RPC handlers in process. Keep it on loopback or behind a proxy. Address routes need the address history index (above). Seereleases/2026-09-08-esplora-facade.md. -
txindex=1(Core's meaning since 2026-09-16): the daemon builds the txid index during the sync as sorted runs (txindex.datif a base exists, thentxindex.r<from>-<to>.dat) trailing the applied height by 144, withtxindex.tailfor the heights above the highest run; the tail is rotated after every run and the runs merged at six (bmc_merge_index_runs).getrawtransaction <txid>asks every run, then the tail. Nothing waits for initial block download, and the same builders keep trailing after it. -
txospenderindex=1(2026-09-16): the same shape for Core's txo-spender index (txospender.dat/txospender.r*.dat/txospender.tail);gettxspendingprevoutanswers confirmed spends from the first run on. The whole-chain builders (daemon/bmc_build_tx_index,daemon/bmc_build_txospender_index <datadir> [from] [to] [out]) remain for a one-off rebuild; a run is the same file under a run name. -
bmc.indexrunblocks=<n>(default 20000, 1000..200000): heights per run for the three trailing builders above. The unsorted tail a lookup scans linearly is never longer than this; a run costs one archive walk over its range, at nice 10, one builder per index at a time. -
blockfilterindex=1: the daemon createsbfilters.dat/bfilters.idxitself and builds every filter from genesis at the new-block choke point (prevouts from undo), closing any gap in slices of 256 heights per block so a catch-up never stalls the apply path. The offline backfill (daemon/bmc_build_block_filters) is no longer part of the path.coinstatsindex=1is opt-in as in Core (default 0 since 2026-09-06) and, since 2026-09-16, survives the two pre-BIP34 duplicate coinbases on a fresh sync (the overwrite is a remove + add, as Core's coinstatsindex does; it used to invalidate the index at 91,842 and re-seed only at the next boot). -
assumevalid=<hash>skips script evaluation for blocks at and below that block (PoW, merkle, structure and every UTXO check still run); the height is resolved from the archive at boot ([utxo_live] assumevalid: block found at height N). Unset = the chain's built-in block (Core v31: mainnet 938343, testnet4 123613, signet 293175; regtest none).assumevalid=0evaluates every script of every block. -
pid=<file>writes the pid once the RPC port is bound.blocknotify,alertnotify,startupnotify,shutdownnotifyrun a shell command with%ssanitised to[A-Za-z0-9._:/-]. -
bmc.utxocompactthreshold(default 12) is the run count that triggers a background compaction;bmc.bootcatchup=0skips the boot-time parallel download. -
bmc.dlcchunk=<n>(default 16, 4..64; 2026-09-30, default 40 until 2026-10-01) is the blocks per request of the parallel download: each worker asks one peer for a chunk of this many blocks in one getdata. Core keeps 16 in flight per peer, refilled as each lands. The[dlc] Core's shape:line prints the value in use and the staged-chunk count at which the committer asks a worker to fetch the chunk the apply is waiting on. -
debuglogfile=<path>relocates the daemon's own leveled log (relative to the chain directory or absolute;0disables it);disablewallet=1loads no wallet;bytespersigop(default 20) sets the sigop-adjusted size used for fee-rate checks.
Reference unit, /etc/systemd/system/bmcbitcoind.service:
[Unit]
Description=Bitcoin Machine Code Daemon (experimental AI-authored asm node)
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=600
StartLimitBurst=5
[Service]
Type=simple
User=<service-user>
Group=<service-group>
WorkingDirectory=/path/to/repo/asm
ExecStart=/path/to/repo/asm/daemon/bmcbitcoind.live serve /path/to/repo/data
StandardOutput=append:/path/to/repo/logs/main/bitcoin.main.log
StandardError=append:/path/to/repo/logs/main/bitcoin.main.log
Restart=on-failure
RestartSec=10
TimeoutStopSec=90
KillSignal=SIGTERM
# Hardening (audit 2026-09-02 N5). This process auto-unlocks the wallet at
# boot and holds the decrypted seed for its lifetime: no core dumps (a dump is
# the debugger-backtrace leak of 2026-08-30 in file form), no privilege gain,
# no /home, a private /tmp. ProtectSystem=full leaves /etc read-only, which is
# all the daemon needs from it.
LimitCORE=0
NoNewPrivileges=yes
ProtectSystem=full
ProtectHome=read-only
PrivateTmp=yes
[Install]
WantedBy=multi-user.targetDrop-ins in bmcbitcoind.service.d/ in the reference deployment raise
TimeoutStopSec=900 (headroom for a compaction in progress at stop time),
add SupplementaryGroups=debian-tor (tor control cookie access), and carry
the hardening block above as 50-hardening.conf. The block is reproduced
inline here so that a unit built from this page is hardened without the
drop-in; the reference deployment's unit itself is a local artifact and is
deliberately not vendored into this repository (operator decision,
2026-09-05 — see releases/2026-09-05-audits-closed.md).
- The reference deployment's
data/bitcoin.conf(2026-09-08) has five keys:port=8332andrpcport=8331(the operator's choice: the router forwards 8332 to this node's P2P side, and the ports the 09-07 build happened to use are the ones peers and tooling know);bind=<host address>because a Core oracle on the same host listens on127.0.0.1-16:8333for the benchmark's loopback replay and Linux refuses a wildcard bind against a listening specific address on the same port;debuglogfile=0andprinttoconsole=1so the log keeps flowing through the unit's capture below rather than<chaindir>/debug.log. ExecStartrunsbmcbitcoind.live, a symlink to a dated snapshotbmcbitcoind.deploy-<YYYYMMDD><letter>, not the tree binary. A rebuild changes nothing until the symlink moves;systemctl restartboots whatever it points at.- The datadir argument is the root;
chain=selects<datadir>/<chain>. - stdout/stderr append to
logs/<chain>/bitcoin.<chain>.log. - SIGTERM is honoured (the parent forwards it to the download worker);
TimeoutStopSecis the wait before systemd SIGKILLs. Restart=on-failure,RestartSec=10, at most 5 starts per 600 s.- No
LimitNOFILEis set, so the process gets the default soft limit of 1,024 descriptors (Max open filesin/proc/<pid>/limits). A healthy production process holds about 66. The Core oracle's unit sets 65,536. On 2026-10-07 a leak used all 1,024 and RPC stopped accepting for 2 h 17 m (devlog/INCIDENT_2026-10-07_reader_lane_fd_leak.md). Raising the limit is an open decision. It would only buy time against a leak, so keep the descriptor check below either way.
sudo systemctl daemon-reload
sudo systemctl enable --now bmcbitcoind
systemctl is-active bmcbitcoindconfig/logrotate-bmc.conf rotates logs/*/*.log when a file exceeds
size 2M (the knob), keeps 60 compressed rotations, and uses
copytruncate so systemd's append descriptor stays valid. It is driven by
bmc-logrotate.timer (5 min after boot, then every 15 min).
The timer reads /etc/bmc/logrotate-bmc.conf, not the repo copy.
logrotate 3.21 refuses a config that is group-writable or not root-owned, so
a file inside the service-user-owned checkout can never be the live config --
pointing the timer at the repo copy is why rotation silently did nothing for
a week (audit 2026-09-02, N6).
The repo copy is the SOURCE. Its su line carries a __SVCUSER__
placeholder rather than a hard-coded account name, because the account name
is host configuration and committing it publishes who runs the node.
Substitute and install in one deliberate step, run AS the service user so
id -un resolves to the right account:
sudo sed "s/__SVCUSER__/$(id -un)/g" config/logrotate-bmc.conf \
| sudo install -m 0644 -o root -g root /dev/stdin /etc/bmc/logrotate-bmc.conf
A leftover __SVCUSER__ makes logrotate fail loudly on the next tick, which
is the safe direction -- a wrong-but-plausible username would quietly rotate
as the wrong account.
An upgrade is build, audit, gate, snapshot, relink, restart. Snapshots are host-local build products, not in git; keep the last few.
cd /path/to/repo/asm
git pull
make daemon/bmcbitcoind # safe while the old binary runs: ld unlinks first
# the three build audits (each is also a prerequisite of `make test`)
make prereq-check # every file a recipe uses is a declared prerequisite
make link-check # every rule links the files defining the symbols it needs
make header-check # every rule that compiles daemon/main.c lists the headers it includes
make runlist-check # every test is gated or declared manual with a reason
make abi-check # SysV stack-alignment audit of asm->C call sites
# the gate: record make's OWN exit status, then audit the finished log
make -k test > gate.log 2>&1; echo MAKE_EXIT=$? >> gate.log
make gate-log-check LOG=gate.log
echo GATE_CHECK_EXIT=$? # act on THIS status onlygate-log-check (scripts/gate_log_audit.py) exits 0 only if make exited
0, every test named in the test: recipe actually ran, and no failure or
crash marker appears; it prints GATE LOG AUDIT OK or GATE LOG AUDIT FAILED with the tests that never ran. Read its exit status directly, never
through a pipe (a pipeline reports the last command's status), and do not
grep the log for failure words: a gate whose test: recipe never ran because
a prerequisite failed to link contains no FAIL lines.
Deploy:
cp -a daemon/bmcbitcoind daemon/bmcbitcoind.deploy-$(date +%Y%m%d)a # next free letter
ln -sfn bmcbitcoind.deploy-$(date +%Y%m%d)a daemon/bmcbitcoind.live
sudo systemctl restart bmcbitcoindLetters advance per deploy on the same day (a..z, aa, ...). Never
cp onto the path of a running binary (ETXTBSY); write a new name and
relink. Copy the wallet files (see Backup) before a deploy that changes
wallet or index formats.
Rollback:
ln -sfn bmcbitcoind.deploy-<previous> daemon/bmcbitcoind.live
sudo systemctl restart bmcbitcoindNot every earlier snapshot is a safe rollback target (as of 2026-10-07):
- Nothing before
deploy-20261006d. B3's async flush (#396, first live in 06d) retires the UTXO WAL's flushed generations: it records the first live byte inutxo.idxand punches a hole over the bytes before it. Earlier builds replay the WAL from byte 0 and would drop tombstones from a datadir that 06d or later has written. That includesdeploy-20261006c, the snapshot immediately before. Do not roll back past 06d without working out the datadir first. deploy-20261006dleaks three descriptors per Esplora facade request (the getblock reader lane, fixed by #402, as does every build fromdeploy-20261005a). It is the only rollback target fordeploy-20261007a. It runs, but RPC stops accepting within hours to a day under mempool.space, so use it only as a bridge and watch the descriptor count.
The current production snapshot and its predecessors, with what each one
carried, are in devlog/DEPLOYMENT_HISTORY.md.
A deploy is verified only when all four hold:
- a NEW block arrives and is applied: its hash equals the oracle's, and the
heartbeat shows
tip == stored; - at least 10 minutes in,
systemctl show -p NRestarts,ActiveState bmcbitcoindstill reads0/active; - the RPC clients (mempool.space, the facade) have reconnected meanwhile;
- the descriptor count stays flat across that client traffic:
ls /proc/$(systemctl show -p MainPID --value bmcbitcoind)/fd | wc -l(about 66 ondeploy-20261007a), well underMax open files.
The second, third and fourth are not optional. On 2026-10-02 a build applied its first block correctly, then crash-looped ~40 s after every start once mempool.space re-synced through the facade, until systemd gave up. One good block proves only the apply path. On 2026-10-07 a build that had passed the first three for two days ran out of descriptors: every facade request leaked three, and only a count taken over time shows that.
One Bitcoin Core node is the reference everything here is measured against: the IBD harness's capstone, the RPC and REST differentials, the mempool differential, the next-block check after a deploy. Since 2026-10-01 it is the v31.1 release, the version this node targets. A v31.99 development build used to sit on these paths; it was retired because diffing against it invented work (docs/PARITY_RPC_FIELDS.md).
| unit | bitcoin-oracle (config/core-oracle/bitcoin-oracle.service, __SVCUSER__ substituted at install as for logrotate): SIGTERM only, TimeoutStopSec=900, never SIGKILL -- Core flushes a large chainstate on the way down |
| binaries | /storage/bitcoin-core-v31.1/bin (bitcoind, bitcoin-cli); source tree /storage/bitcoin-core-v31.1/source |
| datadir | /storage/core-oracle, config config/core-oracle/bitcoin.conf: txindex, coinstatsindex, blockfilterindex, dbcache=8192 |
| network | P2P 8333 on 127.0.0.1-16 (the loopback replay benchmarks dial them), RPC 8335, ZMQ 28432 |
| query | /storage/bitcoin-core-v31.1/bin/bitcoin-cli -conf=/storage/core-oracle/bitcoin.conf -datadir=/storage/core-oracle getblockcount |
It lives on /storage, not on the benchmark NVMe, so it does not compete with
a timed run for I/O. Manage it with sudo systemctl {status,restart,stop} bitcoin-oracle; never start a second copy by hand.
The RPC server comes up about 40 s after start on the reference host; allow about a minute. Check, in order:
systemctl is-active bmcbitcoind
ss -ltnp | grep bmcbitcoind # P2P, RPC, onion target, ZMQ endpoints
daemon/bmc_cli -datadir=/path/to/repo/data getblockcountExpected lines in logs/<chain>/bitcoin.<chain>.log, in order:
===== bmcbitcoind LOG START: ... pid N vX.Y.Z built ... mode=serve
[config] loaded <path>: N setting(s) applied
[net] BIP324 v2 transport enabled (services=0x...); N of M known peers advertise v2
[boot] chain=<chain> datadir=<datadir>/<chain> port=<p> dnsseed=<0|1>
[boot] tx-validation snapshot ready (NN.NNs) -- inbound peers inherit it
[boot] chain archive loaded: tip=<h> (N.NNs)
[boot] archive check clean (N.NNs) # or "found N problem(s) -- see [check] lines above"
[boot] catch-up check done: N block(s) written
[boot] boot phase complete (NN.NNs total)
[rpc] no rpcuser/rpcpassword -- using cookie authentication
[rpc] encrypted wallet adopted and unlocked from the configured passphrase source
# or "adopted (locked -- use walletpassphrase)"
[rpc] JSON-RPC server on 127.0.0.1:<rpcport> (live-node + chain, user=...)
[rpc] cookie authentication enabled (.cookie, mode 0600)
[utxo_live] init dir=... reload applied_height=<h> manifest_n=N live=<count>
[mempool] loaded mempool.dat: N accepted, M rejected of T (...)
[tor] onion service <addr>.onion:<port> -> 127.0.0.1:<port+1> (key onion_v3_private_key)
serving on port <p> (N outbound peer(s))...
applied_height equals the archive tip once caught up, and the heartbeat's
tip tracks the network within a block. The mempool reload runs after the
RPC server is listening, so getrawmempool is briefly partial. The onion
service line appears a few minutes after start.
Two lines every 10 s (since 2026-09-08; before that the tick was eight lines):
[dlc] == elapsed 2:32:32 | eta 00:10:31:52 | overall: 442051/965954 stored (45.76% of real tip) | in flight 232 of window 4096 through 442282 (oldest gap 0s at 441270, 99.95% landed) | applied=440754 lag=515 ==
[dlc] -- recv 11.0MB/s (avg 9.8MB/s) | write 10.9MB/s (avg 10.1MB/s) | floor 32.0 KB/s (median 812.3) | banned 1/126 | events 0 rot 0 wait 0 help 0 fail 0 abandon (run 780/545/64/68/0) --
etaisDD:HH:MM:SSat the last ten minutes' block rate; blocks grow toward the tip, so on the tail it reads optimistic.--:--:--:--until a rate exists.in flight N of window Wis the claimed-not-yet-landed set. With the defaults since 2026-10-01 (10 workers,bmc.catchupworkers, on 16-block chunks,bmc.dlcchunk) it is at most a few hundred. It is bounded by the window: Core's 1,024 blocks above the connected tip (since 2026-09-29), or workers × chunk if that is larger. The archive consolidates behind it.oldest gap Ns at his how long heighthhas been the lowest unfilled height. A gap that stays oldest for 60 s prints asSTRANDED: height h has been the oldest gap for Ns— the one line on this subject that deserves a look.appliedis the UTXO connect's height;lagis how far behind the contiguous prefix it is.- The dashed line carries this tick's network receive and disk write rates
with their averages since the start, the dead-weight floor with the pool
median it derives from, how many peers are banned right now, and the
event counts since the last tick with the run totals in brackets:
rotations (a worker left a peer under half the pool median at a chunk
boundary, nothing discarded), window waits (a worker paused rather than
claim past the window), helps (an idle worker fetched the blocking chunk),
failed attempts (a chunk fetch that failed on a peer; the worker backs
off and the peer's standing halves), abandons (a chunk that went to the
retry ring after 400 attempts). Before the event counts,
staged N commit M(2026-09-08): chunks waiting for the in-order committer right now, and chunks it has appended.stagedabove a handful for more than a tick means the chunk at the first hole has not arrived. While the window is full, the stall rule below judges whoever holds it. - The NODE_WITNESS drop count prints only when it changes.
- The 16-line peer table prints every five minutes.
[dl] new blockand[dl] announced tipare silent while the tip is older thanmaxtipage(Core's definition of initial block download, during which Core relays no blocks); one line says so, and at the tip both return, one per block. A compaction is one line, at completion.
Lines that still print one per event, because each one is worth reading:
chunk [lo,hi] ABANDONED -> retry ring, dead weight (...) (the parent's
pool-relative verdict, with the measured rate and the bar; since
2026-10-08, #409, the rate covers only the part of the 10 s tick the
worker was free to fetch, and a tick it spent mostly waiting at the full
window is not judged at all), stalled: no block for 120s, attempt N failed after M ms: <reason> (only the third in
a row on one chunk and every hundredth), and REJECT h=.
The download's other one-per-event lines (2026-10-05 to 10-08):
| line | meaning |
|---|---|
[dlc] header probe: the first page from N candidate(s) in Xs: <peer> R KB/s (...), ... -- <peer> leads, the rest are fallbacks |
B5: the first header page was asked of four candidates. The fastest leads the header download. |
[dlc] headers from X fell to N KB/s over its last 4 page(s), under half of Y's probed M KB/s -- switching at height H (+S stored, P page(s) held and carried); Y continues from there |
B12 (#404): the header leader slowed. The next candidate continues from H, with nothing re-downloaded. Never printed under bmc.dlshape=core. |
[dlc] header ranges: N range(s) from height A to B (anchors every S) across P peer(s), the probe's fastest |
B12 ranges (#410): the probe's answering peers (up to 4) fetch the chain in parallel, one range between two anchor hashes at a time. The anchors are mainnet block hashes every 50,000 in chainparams (see CORE_DIVERGENCES.md). Not printed under bmc.dlshape=core, with fewer than two answering peers, or on a chain without anchors. |
[dlc] header ranges: K of N range(s) in Xs (MB, rate): <peer> r range(s)/p page(s), ...; heights a..b are contiguous[ -- the sequential fetch continues from there] |
the end of the parallel fetch. Each range was checked for linkage and proof of work and ends on its anchor's hash. Only the contiguous prefix from our tip is used. |
[dlc] header ranges: range j (heights a..b) was given up | was not finished after N attempt(s) |
the first gap. A range that failed 3 times is given up; the sequential fetch takes it and everything after. |
[dlc] header ranges: range j held by P for Xs against a median of Ys -- taken back for a waiting peer | and given up |
a straggler: held over 3x the finished ranges' median (at least 5 s) with nothing left to hand out. Its peer is stopped and the range handed back. |
[dlc] header ranges: +N stored through the checks (total T)[, the rest held below -minimumchainwork through height H] |
the prefix stored by the same code as a peer's pages, every check and the -minimumchainwork hold included. A hold left over is carried into the sequential fetch. the replay refused them -- nothing from them stored means a check failed, and the sequential fetch starts from the tip as before. |
[dlc] headers from X stopped answering after +N (chain at H) -- the next candidate takes over |
the leader went silent. What it served is rolled back and the next candidate starts over. |
[dlc] wN <peer> is stalling the window: chunk [lo,hi] is the oldest missing and the window (W above A) is full -- dropped after N s (next timeout T s; peer <verdict>) | holder <phase> for N s, ... |
Core's stall rule. The verdict is disconnected, a first stall (banned on a second) or BANNED for the run (its second stall) (B13), disconnected, not banned (Core) under bmc.dlshape=core, or one of the guards: manual, kept selectable, at the usable floor, kept selectable, already banned. |
[dlc] chunk [lo,hi] goes to the retry ring for an idle worker as well (Core: ...) |
the stalled chunk was handed to an idle worker at once, as Core re-requests a disconnected staller's blocks. |
[dlc] connected N block(s) during the download; connected tip T (...); idle waits cut short N (B9) |
end of the download: blocks the applier connected while downloading, and how often the idle tick was cut short by a new tip (B9 part 2). |
[dl] peers announce N but the pool's median claim was M Ns ago -- not believed (...); using K |
B10: an announced height the chain could not have reached is clamped. |
[ready] ... |
the finish line: every index at the tip (bmc.benchlog). |
- The log:
<datadir>/<chain>/debug.log, Core's file in Core's place. Since 2026-09-08 everything the daemon prints goes there by default, every[boot],[config],[rpc],[dl],[dlc],[utxo_live],[tx_accept],[tor],[zmq]line, from every forked child. Before that stderr was the log and only the assembly logger's few lines reacheddebug.log.printtoconsole=1sends the same lines to the console as well (Core's semantics);debuglogfile=0turns the file off.shrinkdebugfile=1(default) keeps the last 200 KB of a file over 10 MB at start-up; between restarts the file grows, as Core's does, so rotate it (config/logrotate-bmc.confcoversdata/*/debug.logwithcopytruncate). - What still reaches the launcher's stderr: the lines printed before
the chain directory is entered, i.e. the config echo, chain selection
and a datadir-lock failure, and a
[boot] logging to ...line naming the file. Under systemd those land inlogs/<chain>/bitcoin.<chain>.log. A deployment that wants the old arrangement, everything on stderr and nothing in the chain directory, setsprinttoconsole=1anddebuglogfile=0. journalctl -u bmcbitcoindholds only systemd's lines (start, stop,Killingon a stop timeout).
| line | meaning |
|---|---|
[dl] heartbeat: tip=.. peers=a/b txouts=.. uptime=.. |
the pulse; tip should track the network |
[tx_accept] last 30s: +N accepted (mempool M) | rejected: N missing-inputs, N invalid, N policy | N already confirmed |
relay summary; missing-inputs rejects are normal churn |
[txrelay] last 60s ..., [txrelay] orphans: ... |
relay and orphan-pool counters |
[pool] N peer(s) sampled from the book: ipv4 a, ipv6 b, onion c, i2p d, cjdns e (...) |
the dial pool's per-network composition at boot; anonymity networks appear only when their transport is configured |
[mempool] block <h>: removed N pool tx (confirmed/conflicted) |
a block connected |
compaction of N run(s) ... started in background pid P / background compaction done in Xs |
UTXO maintenance |
[utxo_live] merge of N run(s) deferred: the apply is X blocks behind the archive (waits under M runs) |
during a sync, a merge waits while the apply is 256 or more blocks behind, until twice the count threshold (48 → 96 runs). Since #408 it starts at once when the run files pass the run budget (35% of RAM), at any count. A deferral that lasts more than a few minutes with the apply falling further behind is run 42's 1 h 35 m stall. Look for the compaction done line. |
RECOVERY: rolled back N ghost block(s), rolled back ghost application, init: swept N orphan file(s) |
healthy crash recovery at boot |
[utxo_live] flush: async (...), [bench] freeze ... |
B3: the memtable is frozen and a forked writer flushes it. The writer is a short-lived child that is adopted between blocks. |
[rpc] accept: out of file descriptors (...) -- backing off; the RPC listener is degraded |
printed once per process. RPC and the facade are no longer accepting. See Troubleshooting. |
REJECT, FATAL, DEGRADED, ghost-rollback FAILED, INCONSISTENT |
alert on these |
daemon/bmc_cli -datadir=/path/to/repo/data getblockchaininfo
daemon/bmc_cli -datadir=/path/to/repo/data -chain=signet getblockcount
daemon/bmc_cli -rpcport=<n> -rpcuser=<u> -rpcpassword=<p> <method> [params...]gettxoutsetinfo and scantxoutset refuse while the UTXO set is being
written; call again between blocks or stop the daemon for an authoritative
value. gettxout retries briefly while the worker is busy.
Wallet files live in the chain directory by default; walletdir=<dir>
(absolute, or relative to the chain directory) moves them -- the default
wallet's store and journal, the key and scan records, and every named
wallet under <walletdir>/wallets/<name>/. The directory is created 0700 if
absent. Keeping it outside the datadir keeps wallet keys out of chain
backups; the passphrase file must still live elsewhere again.
With bmcwallet.enc present the wallet boots locked unless a passphrase
source is configured. walletpassphrase "<pass>" <seconds> unlocks it for
that long, walletlock re-locks, walletpassphrasechange re-wraps in place.
encryptwallet on a plaintext bmcwallet.dat seals it and writes
bmcwallet.enc. There is no recovery without the passphrase.
systemctl stop bmcbitcoind sends SIGTERM. The parent logs [serve] shutting down (signal 15): tip=..., saves the mempool ([mempool] saved N transaction(s) to mempool.dat), forwards SIGTERM to the download worker
([dl] shutting down (signal 15): ...), runs shutdownnotify if set,
removes .cookie and the pidfile, and exits. A normal stop takes seconds.
A signal during the UTXO reload exits immediately, leaving the state the
previous clean shutdown left.
If a stop stalls: after TimeoutStopSec systemd sends SIGKILL (journalctl
shows Killing) and boot recovery repairs the interrupted write. To
intervene by hand, identify the process by PID and readlink /proc/<pid>/exe (it points at the deployed snapshot) and signal that PID.
Do not use a broad pkill -f bitcoind: other Bitcoin daemons on the host
match it. A compaction child shares the daemon's command line; killing it is
harmless (the merge is redone).
| item | files in <datadir>/<chain>/ |
notes |
|---|---|---|
| wallet | bmcwallet.enc (or bmcwallet.dat + bmcwallet.dat.txlog), walletkeys.dat, walletscan.dat |
copy with the daemon stopped |
| wallet passphrase | /etc/bmc/wallet.pass (outside the datadir) |
store separately from the wallet backup |
| network identity (optional) | onion_v3_private_key, i2p_private_key |
private keys; keep to preserve the .onion/.b32.i2p address |
| address book (optional) | peers2.dat |
faster re-bootstrap |
| configuration | config/bitcoin.conf, unit file and drop-ins |
holds any rpcauth/rpcuser credential |
The block archive, headers, UTXO store, indexes and mempool.dat are
re-downloadable or rebuildable. Never keep the passphrase file with the
wallet files: a backup carrying both is the failure the walletpassfile
rules prevent, which is why the daemon refuses a passphrase file inside the
datadir. Do not copy UTXO files or point tools at a datadir while the daemon
writes it; stop it first. daemon/bmc_utxo_setinfo <datadir> --muhash on a
stopped datadir is the instrument for comparing the set with a trusted
node's gettxoutsetinfo muhash. Live, ask both nodes for the same height
(gettxoutsetinfo muhash <height>): the no-height answer was printed
byte-reversed on an indexed node from 2026-09-25 to 2026-09-27 (#321), and
Core's no-height call forces a UTXO flush on the oracle.
A coin the node calls spent that a trusted node calls unspent (a block
rejected for "input references a missing/already-spent UTXO" that Core
accepted): daemon/bmc_utxo_probe_one <datadir> <txid> <vout> resolves the
outpoint through both read paths, the point lookup and the full walk. Both
missing: the set lacks the coin. Walk found, lookup missed: a run file is at
fault -- symlink the run files into a scratch directory with a hand-written
one-entry manifest (UMN2, n, total_live, then (gen, run_no) pairs) and
probe each run alone. Bench run 18 (2026-09-09) found a run holding a key
twice, a PUSH then a DEL, from two defects fixed that day
(docs/releases/2026-09-09-one-record-per-key.md); a run written before
the fix is repaired by its next compaction.
Recovery:
- Unclean stop or crash. Start the service. Boot rolls back any ghost
block application from
undo_<h>.datbefore anything reads the set, sweeps orphan run files and re-applies. No operator action. - Missing archive blocks. The node fills gaps and the tail itself
(
[boot] checking for archive gaps / missing blocks...). - Corrupt or lost block index. Set
reindex=1inbitcoin.confand start. Before opening the archive the node rebuildsindex.dat,headers.datandchainwork.datfrom theblk*.datframes alone -- every block on disk is re-linked from genesis by prev-hash, the best chain by cumulative work wins, and duplicates, orphans, stale forks and frames that fail their own proof of work are left out -- then drops the UTXO set and the height-positional indexes so they rebuild. The previous index files stay as*.pre-reindex. The key is one-shot (reindex.donemarks it done); remove it afterwards. The rebuild itself takes seconds (an archive of 150,000 blocks in 93 files rebuilds in about five); the UTXO rebuild that follows is the long part. The log line to expect is[reindex] rebuilt: tip=<h> from <n> frame(s) in <f> file(s); .... - Reorg rehearsal.
asm/tests/reorg_drill <datadir-COPY> --depth Ndisconnects and reconnects the last N blocks on a copy and requires the UTXO walk and tip hash to return to their prior values. It refuses the production datadir by path.
Archives filled by the parallel downloader BEFORE 2026-09-08 are not in
height order, and every boot reports [check] block data is NOT laid out monotonically (first break at height H) -- truncation and pruning will refuse to run. This is informational; only in-place pruning and physical
truncation refuse (the whole-file pruner and the index-only truncation run
instead). Since 2026-09-08 the download's in-order committer appends the
archive in height order from a single process
(docs/releases/2026-09-08-in-order-committer.md), so a fresh sync passes
the check; the tool below is for archives built earlier.
tests/tool_archive_relayout <archive-dir> <out-dir> reads index.dat,
copies every block frame into new blk*.dat files in height order (128 MiB
rotation), writes a matching index.dat, verifies every block hash and
leaves the result in <out-dir>. It reads only index-committed frames, so it
can run against a live daemon; blocks appended meanwhile are not captured and
are re-fetched at the next boot. The swap is done stopped:
tests/tool_archive_relayout /path/to/data/main /path/to/scratch/main-relayout
sudo systemctl stop bmcbitcoind
mkdir /path/to/backup && mv /path/to/data/main/blk*.dat /path/to/data/main/index.dat /path/to/backup/
mv /path/to/scratch/main-relayout/* /path/to/data/main/
sudo systemctl start bmcbitcoindKeep the old files until the new boot's archive check is clean; they are the rollback. The scratch copy needs as much space as the archive.
| symptom | cause and action |
|---|---|
bmc_cli connection refused right after start |
RPC is not up yet. Boot runs config, the tx-validation snapshot (tens of seconds), archive load and check, catch-up check and hash index before starting RPC (about a minute). Wait for [rpc] JSON-RPC server on .... |
| active service, but no RPC ever | rpcport equals port (the RPC bind is lost), or no credential source exists. Read the [rpc] lines. |
| bind failure at start / node exits | The port is in use. ss -ltnp | grep :<port> shows the PID holding it: a stale daemon from a previous run or another node. Stop that PID. |
systemctl stop stalls |
See Shutdown. After TimeoutStopSec systemd SIGKILLs; boot recovery repairs the interrupted write. |
[zmq] notification ring overrun: N transaction(s) not published (total T) |
Producers lapped the 64-slot ring (typical during the mempool reload). Harmless unless a subscriber needs every event; a subscriber must resync via RPC on a per-topic sequence gap, as with Core. Logged at most once a minute; the total is cumulative. |
missing-inputs rejects in the [tx_accept] last 30s summary |
Normal relay churn (parents not yet seen). The summary is the record; there is no per-event line. |
[tor] no onion service: ... |
Control-port authentication failed or the port is unreachable. Check torcontrol=, cookie readability (SupplementaryGroups=debian-tor) or set torpassword=. Outbound onion is unaffected. |
[dial] no IPv6 on this host: ipv6 and cjdns peers are unreachable |
Enable host IPv6 (and run cjdroute) for cjdnsreachable=1. |
[wallet] walletpassfile "..." not usable: <why> / is inside the datadir -- refusing |
Fix path and mode (absolute, outside the datadir, 0640 or stricter, not group-writable). The wallet stays locked until then. |
[boot] archive check found N problem(s) |
Read the [check] lines above it, and do NOT assume the non-monotonic layout notice is benign. It was expected before the in-order committer (2026-09-08) and is a DEFECT after it: a fresh sync on this build must pass the check. On run 26 it meant six layout breaks, one per restart during the download, each scattering ~33 blocks into the tail gaps of older blk files — which disables truncation and pruning. Fixed by the append-frontier guard (2026-09-17); an archive built before that keeps its breaks until tests/tool_archive_relayout rewrites it. Other findings name the height. |
systemctl status: failed (Result: core-dump), "Start request repeated too quickly" |
A crash loop: systemd stopped restarting. Roll back first (above), then sudo systemctl reset-failed bmcbitcoind and start. The cause is in the log's [crash] lines (next row). |
[crash] SIGSEGV (11) at address ... |
A fatal signal (2026-10-03, daemon/crash_trace.c). The lines give the fault address, RIP/RSP/RBP, a backtrace, and the stack's return addresses; resolve each with addr2line -fe asm/daemon/bmcbitcoind.deploy-<x> <addr> (the binary is non-PIE). There is no core file, by design: LimitCORE=0 keeps the decrypted wallet seed off disk, and the report prints addresses only. |
[rpc] getmininginfo took N ms: refresh ..., networkhashps ..., ... |
One call held the exclusive RPC lock over 1 s; every other RPC waited behind it. The split names the slow step. Seen once after a restart into catch-up (149.5 s on 2026-10-03), cause still open. |
a second bmcbitcoind with the same command line |
A child of the running daemon: the serve worker, a compaction, the B3 flush writer (seconds per freeze), the B4 index worker or the coinstats builder. Check /proc/<pid>/exe and the parent PID before assuming a duplicate daemon; never run two daemons on one chain directory. A <defunct> flush writer that lasts longer than a block interval was the pre-#400 shape (fixed 2026-10-07). |
RPC and the facade time out, but is-active is fine and the tip advances; mempool.space stalls |
Check descriptors first: ls /proc/<pid>/fd | wc -l against Max open files in /proc/<pid>/limits, and grep the log for accept: out of file descriptors. At the limit, P2P keeps working because its sockets are already open, while accept() fails. Roll forward to a build without the leak, or restart as a stopgap. 2026-10-07: devlog/INCIDENT_2026-10-07_reader_lane_fd_leak.md. |
One bitcoin.conf selects one chain, so a second chain needs its own
configuration:
- Separate datadir. Give the second chain its own datadir root with its
own
bitcoin.conf(chain=signet, distinctport=/rpcport=) and start a second daemon on it, by hand or as a copy of the unit with a different name,ExecStartdatadir and log path (logs/signet/bitcoin.signet.log, already covered by the logrotate glob). - Wrapper datadir with a symlinked chain directory. The wrapper
<wrapper>/bitcoin.confselects the chain and<wrapper>/<chain>/holds the state, while<maindatadir>/<chain>is a symlink to<wrapper>/<chain>. The main datadir then shows every chain under one root, and each daemon reads its own configuration.
Ports must not collide between daemons. Each daemon writes its own .cookie
in its chain directory, so bmc_cli -datadir=<that datadir> resolves
the right one. Regtest and testnet4 configurations usually pin peers with
connect=<ip:port>, which disables discovery and DNS seeding.
| chain | P2P | RPC | magic (wire) |
|---|---|---|---|
| main | 8333 | 8332 | f9 be b4 d9 |
| testnet4 | 48333 | 48332 | 1c 16 3f 28 |
| signet | 38333 | 38332 | 0a 03 cf 40 (derived from the challenge) |
| regtest | 18444 | 18443 | fa bf b5 da |
Extra listeners: onion service target at chain default P2P port + 1 (loopback); ZMQ endpoints as configured (commonly 28332/28333).
| file | contents |
|---|---|
blk*.dat, index.dat |
block archive (framed blocks, 128 MiB rotation) and positional height index (48-byte records) |
headers.dat, chainwork.dat |
header chain and cumulative work |
utxo.dat, utxo.idx, utxo_run_*.dat, utxo_manifest.dat, utxo_applied_height.dat, utxo_lsm_*.map |
LSM UTXO store: the WAL; its checkpoint (utxo.idx, the first byte of the generations not yet retired; B3, 2026-10-06; retired bytes are hole-punched); sorted runs; manifest; applied height; and the memtable files. Since B11 (2026-10-07) the memtable lives in anonymous memory, and the .map files are only created at their full size for the offline tools, which read their sizes. Only the table's first page (the live count) is shared through the file. bmc.memtableanon=0 restores the file mappings. |
undo_<h>.dat |
per-block undo data for the recent tail (reorg and crash rollback) |
append.lock |
archive append lock |
mempool.dat |
mempool saved at shutdown, reloaded after RPC start |
peers2.dat (peers.dat legacy) |
address book |
.cookie |
RPC cookie, mode 0600, removed at shutdown |
bmcwallet.enc / bmcwallet.dat (+ .txlog), walletkeys.dat, walletscan.dat |
wallet container / plaintext store and journal, HD keys, rescan records |
onion_v3_private_key, i2p_private_key |
persisted onion service key and I2P destination |
rev%05u.dat + undo.idx |
undo data for every block (2026-09-08; Core's rev files). Pruned only with the block store. A node upgraded from the per-height undo_<h>.dat files folds them in at start; older history needs -reindex-chainstate |
txindex.dat / txindex.r*-*.dat + txindex.tail, txospender.dat / txospender.r*-*.dat + txospender.tail, addr_hist.dat / addr_hist.r*-*.dat + addrindex.tail, bfilters.dat + bfilters.idx, coinstats.dat + coinstats_hist.dat |
optional indexes: sorted runs the daemon builds during the sync plus a bounded tail each (2026-09-16); addr_index.dat is the retired UTXO-by-address snapshot |
debug.log |
the daemon's log, everything, as Core (since 2026-09-08; before that only the leveled logger's lines; logs/bitcoind.log before 2026-09-06) |
Outside the chain directory: <datadir>/bitcoin.conf or
<repo>/config/bitcoin.conf; <repo>/logs/<chain>/bitcoin.<chain>.log;
<repo>/asm/daemon/bmcbitcoind.live and bmcbitcoind.deploy-*;
<repo>/config/logrotate-bmc.conf.
config/bitcoin.sample.conf documents every key. Of note for operators:
uacomment=<text>(repeatable) appears in the user agent on the wire and ingetnetworkinfo;( ) / \ ; :and control characters are refused.peerblockfilters=1is now required to serve BIP157 filters and to advertiseNODE_COMPACT_FILTERS— Core's default is 0 and this node follows it.blockfilterindex=still controls whether the index is maintained.rpcwhitelist=<user>:<m1>,<m2>locks an RPC user to a method list (HTTP 403 otherwise); once any whitelist exists, users without one may call nothing unlessrpcwhitelistdefault=0.rpcthreads/rpcworkqueuesize the HTTP server (503 "Work queue depth exceeded" past the queue);rpcservertimeoutbounds a slow client.rpccookieperms=group|allwidens the cookie file's mode (0640 / 0644).- Wallet:
addresstype/changetypepick whatgetnewaddressand change use (a type must be activated withcreatewalletdescriptorfirst);fallbackfeeis 0 by default, so with no fee estimate a send fails with Core's "Fee estimation failed. Fallbackfee is disabled..." — set it (BTC/kvB) on a fresh node.walletbroadcast=0returns txids without broadcasting.walletnotify=<cmd>runs with%s= txid on mempool arrival and on confirmation.wallet=<name>loads a named wallet at start-up (one active wallet at a time; further names are logged as skipped). - Mining:
blockmaxweight,blockreservedweight,blockmintxfee(BTC/kvB),blockversion,printpriorityshapegetblocktemplate. - Logging:
logtimestamps=0,logtimemicros=1,logthreadnames=1,logsourcelocations=1change the stderr prefix from the point the config is read;shrinkdebugfile(default 1) truncates adebuglogfileover 10 MB to its last 200 KB at start-up.[rpc] exec lock:lines (2026-09-29): every RPC but the lock-free few runs under one execution lock, and four times in three days the whole surface stalled for over 90 s right after a block with nothing to say who held it. A wait or a hold ofBMC_RPC_EXEC_LOG_MSor longer (an environment variable on the unit; default 2000 ms, 0 switches it off) now logs one line naming the JSON-RPC method, or the facade / REST route and the method it dispatched:... getmininginfo (excl) waited 90123 ms; the last exclusive holder was esplora GET /internal/block/<hash>/txs -> getrawtransaction (held 90101 ms); 3 still waiting, and the holder's own... held 90101 ms (waited 0 ms); 3 waiting behind it. A waiter names the last exclusive holder because that is what the writer-preferring lock made it wait for; a hold shorter than the wait means the wait was behind readers.[mempool] pool lock:lines (2026-09-30): the mempool's own lock is shared by the download worker, every inbound serve child and the RPC process, and the seconds the lines above attributed togetrawmempool,getmempoolinfoandgetrawtransactionwere spent waiting for it while the worker held it at a new block. A wait or a hold ofBMC_MEMPOOL_LOCK_LOG_MSor longer (environment; default 1000 ms, 0 switches it off) logs one line from the process that waited or held, naming the site (the worker's or child's function, or the RPC method) and the pid:... getrawtransaction (pid 12602) waited 3467 ms; the holder was tx_accept_block_connect_h/rm/reindex (pid 12606, held 3450 ms); 2 still waiting, and the holder's own... tx_accept_block_connect_h (pid 12606) held 3450 ms (waited 0 ms): fest_begin 0 ms, mark 118 ms, remove_marked 0 ms, rm/seq 12 ms, ..., rm/reindex 2900 ms, seq_C 1 ms; 2 waiting behind it-- the steps are block connect's passes, so the line says which one. Each line is written by the process that waited or held, on its stderr, which every process of the node shares (the worker's, a serve child's and the RPC process's lines land in the same log, interleaved). includeconf=<file>(relative to the main file's directory) is read after it; an included file may not include another.- Core options that are accepted WITHOUT effect are each named at start-up
with the reason (
[config] <key>= is a Bitcoin Core option that has NO EFFECT here: ...); the full classification of all 181 Core options is indocs/FEATURE_GAPS.md.
Nothing needs a .pass file or BMC_WALLET_PASS any more; both still work
and take precedence when present.
wallet_cli getaddress|getprivkey|load [...]on an encrypted wallet with no passphrase supplied asks for it on the terminal with echo off (via/dev/tty, so redirecting stdout is fine). With stdin not a terminal it reads one line from stdin instead:printf '%s\n' "$PASS" | wallet_cli getaddress.wallet_cli initwith no passphrase argument asks twice; a passphrase entered this way is not written to a.passfile. Enter nothing for a plaintext wallet, as before.- The RPC client follows Core:
bmc_cli -stdinwalletpassphrase walletpassphrase 60reads the passphrase as the first line of stdin (echo off on a terminal) and sends it as the first parameter, so it never appears inpsor the shell history;-stdinreads the remaining parameters one per line.