Setting up bitcoind (on-chain)
Use a bitcoind dedicated to the hub and SatBooth, with no wallet that holds keys loaded on it. (Each bitcoind fact
in this section is marked source, read in the Bitcoin Core 32.1 source, or observed, seen in qualification on
regtest.) SatBooth’s RPC user is confined by rpcwhitelist, but the whitelist is per method, not per wallet (source:
src/httprpc.cpp:107-118; observed: getnewaddress as satbooth worked on another wallet), and listdescriptors
with private true returns the private descriptors of any loaded wallet that holds keys and is not locked
(source: src/wallet/rpc/backup.cpp:479-519; not observed). So a node that also serves a spending wallet lets a
stolen SatBooth password read that wallet’s keys, unless the wallet is encrypted and stays locked. The hub needs the
same node as its chain source, and SatBooth refuses to start without that node (with the onchain block, bitcoind down at
boot stops the whole service, Lightning included; see A supervisor). The hub host’s Compose bundle runs such a
bitcoind (deploy/README.md, “The hub host”).
SatBooth needs a Bitcoin Core node it can reach over the private link (qualified against Core 32.1, qualify/REPORT.md
“On-chain end to end”). The operator makes the wallet and imports the descriptor with bitcoin-cli; SatBooth’s own RPC
user cannot. The examples are on testnet, like the example configuration in Configuration: the
bitcoin-cli lines carry -testnet. Drop it on mainnet, use -signet or -regtest on those networks, or
-conf=<file> when the node’s configuration (with its chain=) is elsewhere; without the right flag bitcoin-cli
talks to the mainnet port and wallet directory.
-
A dedicated account. In the merchant’s wallet, make a new account (its own extended public key) for SatBooth, and export that account’s extended public key with its key origin: on testnet a
tpubwith BIP-84 coin type1h, for examplewpkh([d34db33f/84h/1h/1h]tpub.../0/*); on mainnet anxpubwith coin type0h, for examplewpkh([d34db33f/84h/0h/1h]xpub.../0/*). No other wallet may issue addresses from it: a wallet that hands out addresses from the same<key>/0/*chain reuses SatBooth’s addresses, and its receipts count toward SatBooth’s orders. -
SatBooth’s RPC user, in
bitcoin.conf:rpcauth=satbooth:<salt>$<hash>rpcwhitelist=satbooth:getnewaddress,getaddressinfo,deriveaddresses,listsinceblock,gettransaction,getmempoolentry,getblockchaininfo,getwalletinfo,listdescriptors,getdescriptorinfo,getmempoolinfo,gettxspendingprevoutrpcwhitelistdefault=0share/rpcauth/rpcauth.py satbooth, in the Bitcoin Core source, prints therpcauthline and a new password (source:src/httprpc.cpp:278names it). Put the password inonchain.rpc_password_file, mode 0600. Anrpcauthline joins anyrpcuser/rpcpasswordpair in the same credential list, so both users work (source:src/httprpc.cpp:248-308; observed).- The whitelist is exactly these twelve methods. SatBooth’s client allows the same twelve and refuses any other
before sending it. Upgrading from a build with ten: add
getmempoolinfoto the line (SatBooth asks it each pass whether bitcoind has finished loading its mempool after a restart, and marks no payment gone until it has), andgettxspendingprevout(SatBooth asks it before it marks a payment gone, to tell a payment that bitcoind’s wallet has not yet seen replaced from one that left the mempool); from a build with eleven: addgettxspendingprevout. Without either, the startup check fails with the reasononchain. SatBooth’s on-chain rail needs Bitcoin Core 31 or newer:gettxspendingprevouttakes itsmempool_onlyoption from Core 31, and the startup check refuses an older node through this call. For this user bitcoind answers any other method with HTTP 403 (observed:sendtoaddress,createwallet,importdescriptorsandstop). A secondrpcwhitelistline for the same user narrows the list to the methods both lines name (source:src/httprpc.cpp:315-332). rpcwhitelistdefault=0keeps every user without a whitelist (your own, and the cookiebitcoin-cliuses) unrestricted. Without it, a singlerpcwhitelistline locks out every other user (source:src/httprpc.cpp:314; observed with it: the full user was unaffected).- The hub bundle confines the hub’s own user too, to the eleven calls its chain source makes, a different list
from SatBooth’s (
deploy/hub/config-example/bitcoin.conf; the source lines are indocs/dependencies.md§8). WithLDK_GOSSIP_SOURCEleft empty, as in the bundle, the hub checks channel announcements withgetblockhashandgettxout, both in that list. - The whitelist reaches every wallet the node has loaded: see the first paragraph of this section.
-
The watch-only wallet, with the full user:
bitcoin-cli -testnet -named createwallet wallet_name=satbooth disable_private_keys=true blank=true descriptors=true load_on_startup=trueIt has no keys and no descriptor until the import below (observed: after the import it holds exactly one).
load_on_startupmatters: SatBooth’s user cannot load a wallet, so after a bitcoind restart the wallet must come back by itself (observed: the test’sminerwallet, made without it, had to be loaded again after each bitcoind kill,scripts/qualify.sh; the watch-only wallets made with it came back). -
The checksum.
importdescriptorsrequires the descriptor’s#checksum(source:src/wallet/rpc/backup.cpp:164). Get it withbitcoin-cli -testnet getdescriptorinfo "wpkh([d34db33f/84h/1h/1h]tpub.../0/*)"and use the
descriptorfield of the answer, which carries it. -
The import, once:
bitcoin-cli -testnet -named -rpcwallet=satbooth importdescriptors requests='[{"desc":"<the descriptor with its #checksum>","active":true,"internal":false,"timestamp":"now"}]'"now"scans no history, which a new account does not need (source:src/wallet/rpc/backup.cpp:135-144). With norange, the import covers indexes 0 to 999 and starts atnext_index0 (source:src/wallet/rpc/backup.cpp:178-191); bitcoind extends the range itself as SatBooth asks for addresses (observed:listdescriptorsshows the range[0, next_index + 998]after each new address; source:src/wallet/scriptpubkeyman.cpp:948,:1121, as cited inqualify/REPORT.md).Import exactly one receive descriptor. SatBooth’s startup check counts the wallet’s active descriptors that are not
internal: it needs exactly one, and it must be the configured one, so a second active receive descriptor (atrbeside thewpkh, or another account) stops SatBooth at startup (onchain/check.go). A change descriptor ("internal":true) or an inactive one is ignored; the watch-only wallet never spends, so it needs none. -
Never re-import without
next_index. A wallet restored from an older backup, or the descriptor imported again withoutnext_index, moves the wallet’snext_indexback (observed: to just past the last paid address, not to 0), and bitcoind would hand out addresses SatBooth already showed. SatBooth refuses to start then (ALERT: wallet next_index is behind SatBooth's record; addresses are not issued). To recover, import the same descriptor again with"next_index"set to SatBooth’s highest recorded index plus 1 (SELECT max(idx) FROM addressesinstore.db), and a"range"that contains it and is not smaller than the wallet’s current one (observed: a smaller range is refused;next_indexmust lie inside the range, source:src/wallet/rpc/backup.cpp:194-198). A rebuilt wallet knows no transactions until it is rescanned, and step 5’s"timestamp":"now"scans none: import with a"timestamp"(Unix seconds) at or before the creation of the oldest order that can still be paid (its expiry plus 7 days not yet passed) and of every output the store still tracks, so that bitcoind rescans the payments the restored store will look for; when in doubt, the time the descriptor was first imported.After SatBooth’s own store was restored from a backup (see Backup and restore), do not use that plus 1. The restored store’s highest index is the backup’s: addresses of orders made after the backup were shown to customers and the store no longer knows them. Set
"next_index"to the restored store’s highest index plus a margin that is generously above the number of on-chain addresses that could have been issued since the backup (an address is issued when a customer first presses “Pay with bitcoin”: take the most on-chain orders you ever had in a day, times the days since the backup’screated_at, times ten), and make the"range"at least[0, next_index + 1000]so that it contains it. Why: an address already shown to the customer of an order the restore lost must never be handed to a new order, because a late payment to it would mark the new order paid; SatBooth cannot know that address, and refuses only an index that is not above the store’s own highest. The skipped indexes stay unused in the merchant’s own wallet: raise its gap limit above the margin, or it will not find later payments (see “The merchant’s wallet” below).
At every start SatBooth checks, in this order: bitcoind’s chain is the configured network; the wallet is a
descriptor wallet with private keys disabled; getdescriptorinfo of the configured descriptor has no private keys
and is ranged; the wallet has exactly one active receive descriptor, and it is that one (so a wpkh and a tr
descriptor need two wallets and two services); and its next_index is above every index SatBooth recorded. Any
failure stops the start. It logs ALERT: bitcoind is in initial block download or behind its headers when the
node is syncing or more than 3 blocks behind its headers, and goes on.
The merchant’s wallet. SatBooth never reuses an address: an order gets the next index when the customer first presses
“Pay” on its page, and orders that are never paid leave unused addresses behind. A wallet restored from the account’s seed
stops looking after a run of unused addresses (its gap limit, often 20). Raise the gap limit of the merchant’s
wallet above the longest run SatBooth may leave, or it will not find later payments. SatBooth logs ALERT: unused on-chain addresses exceed gap_alert; raise the merchant wallet's gap limit, with the count, when the issued addresses
above the highest one that received a payment first number gap_alert or more, then once a day while they still do
(and again on the first pass after a restart).
Documentation from SatBooth v0.1.0-rc.1, Apache-2.0