From d4641c402c1e2719c6a38fab6a66ad50ed1a4a75 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 02:17:06 +0000 Subject: [PATCH 1/5] docs: Restore the qr steps for offline signing Commit 36d903d replaced the guide's offline-signing steps with a link to Bitcoin Core's tutorial, leaving "the QR tools below" and the QR troubleshooting section with no step that uses qr. Follow the tutorial's two wallets and use qr to carry the public descriptors to the online watch-only wallet, the PSBT to the offline signer, and the signed transaction back. The tutorial's exportwatchonlywallet file is too large for a QR, so the descriptors are imported into a blank watch-only wallet instead. Mention qrencode as the fallback. Closes #92 Claude-Session: https://claude.ai/code/session_01T233rKgZqE5wzDm3EVTHL1 --- docs/user/guide.md | 101 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 89 insertions(+), 12 deletions(-) diff --git a/docs/user/guide.md b/docs/user/guide.md index 7be6bcf..5cf73c2 100644 --- a/docs/user/guide.md +++ b/docs/user/guide.md @@ -82,7 +82,7 @@ port. Use a computer you believe is malware-free and whose other software you trust. Only codex32 and Bitcoin Core should perform recovery, derivation, wallet initialization, or signing. The QR tools below transport only public -descriptors or PSBTs. +descriptors, PSBTs, and signed transactions. Bitcoin Core wallet encryption is strongly recommended. Bitcoin Core owns the passphrase and its prompts; codex32 never asks for, reads, or forwards it. @@ -192,16 +192,91 @@ worth the extra steps. ## More protection: watch-only wallet and offline signer -On the offline signer, create an empty encrypted descriptor wallet with private -keys enabled and run `ms32 wallet`. Keep that computer disconnected from every -network while recovery text or signing keys are present. +This setup follows Bitcoin Core v32's +[offline-signing tutorial](https://github.com/bitcoin/bitcoin/blob/v32.0rc1/doc/offline-signing-tutorial.md) +and its two wallets: `offline_wallet` holds the signing keys on a computer that +never connects to a network, and `watch_only_wallet` runs on an ordinary online +node. Where the tutorial carries a file between the computers, this guide shows +the same public data as a QR on one screen and scans it with the other +computer's camera. -After the signer is restored, follow Bitcoin Core v32's maintained -[offline-signing tutorial](https://github.com/bitcoin/bitcoin/blob/v32.0rc1/doc/offline-signing-tutorial.md). -That workflow owns the watch-only export/import and PSBT transport steps. In -Bitcoin Core v32, `exportwatchonlywallet` creates the watch-only wallet file and -`restorewallet` loads it on the online node. Do not improvise a codex32-specific -descriptor-transfer procedure in place of that maintained workflow. +Before disconnecting the offline computer for good, install Bitcoin Core, +codex32, `jq`, `qr`, and ZBar's `zbarcam`. Tails and Debian ship `qr`. The +online computer needs `jq`, `qr`, and `zbarcam` too. Disable Ethernet, +internet, Tor, Wi-Fi, Bluetooth, cellular, and every other network path on the +offline computer. + +### 1. Restore the offline signer + +On the offline computer, create an empty encrypted descriptor wallet named +`offline_wallet` with private keys enabled and run `ms32 wallet`. Keep that +computer disconnected from every network while recovery text or signing keys +are present. + +### 2. Create the online watch-only wallet + +The tutorial moves a watch-only wallet file made by `exportwatchonlywallet`. +That file is far too large for a QR, so send the public descriptors instead. +On the offline computer: + +```bash +bitcoin-cli -rpcwallet=offline_wallet listdescriptors | jq -cj '[.descriptors[] | {desc,timestamp,active,internal,range,next_index}]' | qr +``` + +The QR holds about 2,000 characters; see [QR troubleshooting](#qr-troubleshooting) +if it does not fit. Public descriptors cannot spend, but they reveal wallet +activity. Do not use a website, cloud scanner, chat service, or synced +clipboard. + +On the online computer, create a blank watch-only wallet and scan the QR into +it: + +```bash +bitcoin-cli -named createwallet wallet_name=watch_only_wallet disable_private_keys=true blank=true +zbarcam --raw --oneshot -Sdisable -Sqrcode.enable | + bitcoin-cli -rpcwallet=watch_only_wallet -stdin importdescriptors +``` + +Every result must say `"success": true`. Core's descriptor export keeps the +stored timestamps, so the online node rescans from the same point. Run +`getnewaddress` in each wallet and compare the two addresses on the two +screens. Do not receive funds if they differ. + +### 3. Spend with a PSBT + +On the online computer, create the unsigned PSBT with your destination and +amount, and show it as a QR: + +```bash +bitcoin-cli -rpcwallet=watch_only_wallet send '{"DESTINATION_ADDRESS": AMOUNT}' | jq -rje .psbt > funded_psbt.txt && qr < funded_psbt.txt +``` + +On the offline computer, scan it, then check every destination, amount, and +fee before signing: + +```bash +zbarcam --raw --oneshot -Sdisable -Sqrcode.enable > funded_psbt.txt +bitcoin-cli decodepsbt "$(cat funded_psbt.txt)" +bitcoin-cli analyzepsbt "$(cat funded_psbt.txt)" +``` + +Unlock `offline_wallet` as the tutorial shows, then sign and show the signed +transaction as a QR: + +```bash +bitcoin-cli -rpcwallet=offline_wallet walletprocesspsbt "$(cat funded_psbt.txt)" | jq -rje 'if .complete then .hex else error("The PSBT is not fully signed.") end' > final_psbt.txt && qr < final_psbt.txt +``` + +On the online computer, scan it and broadcast: + +```bash +zbarcam --raw --oneshot -Sdisable -Sqrcode.enable > final_psbt.txt +bitcoin-cli sendrawtransaction "$(cat final_psbt.txt)" +``` + +If a PSBT is too large for a reliable QR, use a dedicated removable drive. The +drive crosses the security boundary: keep it for this purpose, treat every file +on it as untrusted, and still verify the transaction on the offline screen. ## Recover an existing or inherited wallet @@ -296,8 +371,10 @@ arbitrary-HRP format direction is not yet merged into that specification. ### QR troubleshooting Maximize the terminal and reduce its font size if a QR does not fit. Keep `qr` -connected to the terminal; redirecting its output creates an image file. Only -public descriptors, xpubs, and PSBTs may cross the offline boundary by QR. +connected to the terminal; redirecting its output creates an image file. If `qr` +is unavailable, `qrencode -t ANSIUTF8` shows the same QR. Only public +descriptors, xpubs, PSBTs, and signed transactions may cross the offline +boundary by QR. ## Technical references From ad2f234322f64db3d8fdee6eae902791ca969c6c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 06:03:24 +0000 Subject: [PATCH 2/5] docs: Compress the descriptor QR The watch-only wallet file from exportwatchonlywallet does not fit in a QR even compressed (12,288 bytes; 2,365 gzipped against qr's 2,331-byte limit), so the guide sends the public descriptors. gzip shrinks them from 1,975 to 619 bytes, which takes qr's code from version 37 to 19, about half the width, and makes the scan more reliable. The online side reads the QR with zbarcam -Sbinary and pipes it through gunzip. Without -Sbinary, ZBar rewrites the bytes as text and gunzip fails. qrencode needs -8 for the same data; without it the input stops at the first zero byte. Validation: on two separate Bitcoin Core 32.0rc2 regtest nodes with the BIP-93 test vector, the gzipped listdescriptors output went through qr and back through zbarimg with the guide's flags and gunzip, all 8 descriptors imported with success, and both wallets gave the same first address. qrencode -8 round-tripped the same bytes. Docs-only change. Refs #92 Claude-Session: https://claude.ai/code/session_01T233rKgZqE5wzDm3EVTHL1 --- docs/user/guide.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/user/guide.md b/docs/user/guide.md index 5cf73c2..75a6517 100644 --- a/docs/user/guide.md +++ b/docs/user/guide.md @@ -216,24 +216,24 @@ are present. ### 2. Create the online watch-only wallet The tutorial moves a watch-only wallet file made by `exportwatchonlywallet`. -That file is far too large for a QR, so send the public descriptors instead. -On the offline computer: +That file does not fit in a QR even compressed, so send the public descriptors +instead, compressed with `gzip`. On the offline computer: ```bash -bitcoin-cli -rpcwallet=offline_wallet listdescriptors | jq -cj '[.descriptors[] | {desc,timestamp,active,internal,range,next_index}]' | qr +bitcoin-cli -rpcwallet=offline_wallet listdescriptors | jq -cj '[.descriptors[] | {desc,timestamp,active,internal,range,next_index}]' | gzip -9 | qr ``` -The QR holds about 2,000 characters; see [QR troubleshooting](#qr-troubleshooting) -if it does not fit. Public descriptors cannot spend, but they reveal wallet -activity. Do not use a website, cloud scanner, chat service, or synced -clipboard. +The compressed descriptors take about 620 bytes; see +[QR troubleshooting](#qr-troubleshooting) if the QR does not fit. Public +descriptors cannot spend, but they reveal wallet activity. Do not use a +website, cloud scanner, chat service, or synced clipboard. -On the online computer, create a blank watch-only wallet and scan the QR into -it: +On the online computer, create a blank watch-only wallet, then scan and +decompress the QR into it: ```bash bitcoin-cli -named createwallet wallet_name=watch_only_wallet disable_private_keys=true blank=true -zbarcam --raw --oneshot -Sdisable -Sqrcode.enable | +zbarcam --raw --oneshot -Sdisable -Sqrcode.enable -Sbinary | gunzip | bitcoin-cli -rpcwallet=watch_only_wallet -stdin importdescriptors ``` @@ -372,7 +372,9 @@ arbitrary-HRP format direction is not yet merged into that specification. Maximize the terminal and reduce its font size if a QR does not fit. Keep `qr` connected to the terminal; redirecting its output creates an image file. If `qr` -is unavailable, `qrencode -t ANSIUTF8` shows the same QR. Only public +is unavailable, `qrencode -8 -t ANSIUTF8` shows the same QR. Reading the +compressed descriptors needs ZBar 0.23.1 or newer for `-Sbinary`; without it, +ZBar rewrites the bytes as text and `gunzip` fails. Only public descriptors, xpubs, PSBTs, and signed transactions may cross the offline boundary by QR. From 1ebc77ba727c18de7ccd4a22d1c711b8352febda Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 06:46:12 +0000 Subject: [PATCH 3/5] docs: Check receive addresses on the offline signer A compromised online computer can show an address it controls, and a deposit to it is lost even though the keys never left the offline signer. Bitcoin Core's offline-signing tutorial gets receiving addresses from the watch-only wallet without checking them. The new step keeps addresses and labels in watch_only_wallet, as the tutorial does, then scans each address into the offline computer and looks it up with getaddressinfo. The address is given out only when the result shows "ismine": true and matches what is sent. It needs only bitcoin-cli, qr and zbarcam, which Tails ships, and works while offline_wallet is locked. The spend step becomes step 4. Validation: on two separate Bitcoin Core 32.0rc2 regtest nodes with the BIP-93 test vector, an address from getnewaddress "LABEL" went through qr and zbarimg into a locked offline_wallet, which reported "ismine": true and the same address; the online wallet kept the label. A foreign address reported false. Address 1,002 reported false until keypoolrefill 2000 on the unlocked offline wallet, then true. Docs-only change. Refs #92 Claude-Session: https://claude.ai/code/session_01T233rKgZqE5wzDm3EVTHL1 --- docs/user/guide.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/user/guide.md b/docs/user/guide.md index 75a6517..22fd275 100644 --- a/docs/user/guide.md +++ b/docs/user/guide.md @@ -242,7 +242,32 @@ stored timestamps, so the online node rescans from the same point. Run `getnewaddress` in each wallet and compare the two addresses on the two screens. Do not receive funds if they differ. -### 3. Spend with a PSBT +### 3. Receive to a checked address + +Get receiving addresses and set labels in `watch_only_wallet`, as the tutorial +does, so one wallet tracks which addresses are used. Malware on the online +computer could show an address it controls, so check every address on the +offline computer before giving it out. On the online computer: + +```bash +bitcoin-cli -rpcwallet=watch_only_wallet getnewaddress "LABEL" | qr +``` + +On the offline computer, scan it and look it up in the signing wallet: + +```bash +address=$(zbarcam --raw --oneshot -Sdisable -Sqrcode.enable) +bitcoin-cli -rpcwallet=offline_wallet getaddressinfo "$address" +``` + +Give out the address only if the result shows `"ismine": true`, and compare +the address you send character by character with the `"address"` shown on the +offline screen. This works while `offline_wallet` is locked. The offline wallet +recognizes its first 1,000 addresses of each type; past that, a real address +shows `"ismine": false` until you unlock `offline_wallet` and run +`keypoolrefill` with a larger number. + +### 4. Spend with a PSBT On the online computer, create the unsigned PSBT with your destination and amount, and show it as a QR: From 1e6c7fa41452470a8bd578c958bb138a5a0d0a9f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 07:05:09 +0000 Subject: [PATCH 4/5] docs: Use python3 instead of jq for QR steps Tails doesn't ship jq, and Bails can't install packages because it doesn't run as an administrator. The offline-signing steps now pull the descriptors, the PSBT and the signed transaction out of Core's JSON with python3, which Tails includes. Importing the full listdescriptors entries works, so the descriptor step no longer filters fields; the gzipped QR grows from about 620 to 640 bytes. Drop the qrencode fallback, which also needs an install; Tails always has qr. Validated on Core 32.0rc2 regtest: 8 descriptors imported from a gzipped QR image read by zbarimg -Sbinary, addresses matched, a PSBT signed offline and broadcast; signing with a wallet that can't sign exits with "The PSBT is not fully signed." and shows no QR. Fixes #111 Refs #92 Claude-Session: https://claude.ai/code/session_01T233rKgZqE5wzDm3EVTHL1 --- docs/user/guide.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/user/guide.md b/docs/user/guide.md index 22fd275..e5facce 100644 --- a/docs/user/guide.md +++ b/docs/user/guide.md @@ -200,11 +200,10 @@ node. Where the tutorial carries a file between the computers, this guide shows the same public data as a QR on one screen and scans it with the other computer's camera. -Before disconnecting the offline computer for good, install Bitcoin Core, -codex32, `jq`, `qr`, and ZBar's `zbarcam`. Tails and Debian ship `qr`. The -online computer needs `jq`, `qr`, and `zbarcam` too. Disable Ethernet, -internet, Tor, Wi-Fi, Bluetooth, cellular, and every other network path on the -offline computer. +Before disconnecting the offline computer for good, install Bitcoin Core and +codex32. Both computers also use `python3`, `gzip`, `qr`, and ZBar's +`zbarcam`, which Tails already includes. Disable Ethernet, internet, Tor, Wi-Fi, +Bluetooth, cellular, and every other network path on the offline computer. ### 1. Restore the offline signer @@ -220,10 +219,12 @@ That file does not fit in a QR even compressed, so send the public descriptors instead, compressed with `gzip`. On the offline computer: ```bash -bitcoin-cli -rpcwallet=offline_wallet listdescriptors | jq -cj '[.descriptors[] | {desc,timestamp,active,internal,range,next_index}]' | gzip -9 | qr +bitcoin-cli -rpcwallet=offline_wallet listdescriptors | + python3 -c 'import json, sys; print(json.dumps(json.load(sys.stdin)["descriptors"]), end="")' | + gzip -9 | qr ``` -The compressed descriptors take about 620 bytes; see +The compressed descriptors take about 640 bytes; see [QR troubleshooting](#qr-troubleshooting) if the QR does not fit. Public descriptors cannot spend, but they reveal wallet activity. Do not use a website, cloud scanner, chat service, or synced clipboard. @@ -273,7 +274,9 @@ On the online computer, create the unsigned PSBT with your destination and amount, and show it as a QR: ```bash -bitcoin-cli -rpcwallet=watch_only_wallet send '{"DESTINATION_ADDRESS": AMOUNT}' | jq -rje .psbt > funded_psbt.txt && qr < funded_psbt.txt +bitcoin-cli -rpcwallet=watch_only_wallet send '{"DESTINATION_ADDRESS": AMOUNT}' | + python3 -c 'import json, sys; print(json.load(sys.stdin)["psbt"], end="")' > funded_psbt.txt && + qr < funded_psbt.txt ``` On the offline computer, scan it, then check every destination, amount, and @@ -289,7 +292,9 @@ Unlock `offline_wallet` as the tutorial shows, then sign and show the signed transaction as a QR: ```bash -bitcoin-cli -rpcwallet=offline_wallet walletprocesspsbt "$(cat funded_psbt.txt)" | jq -rje 'if .complete then .hex else error("The PSBT is not fully signed.") end' > final_psbt.txt && qr < final_psbt.txt +bitcoin-cli -rpcwallet=offline_wallet walletprocesspsbt "$(cat funded_psbt.txt)" | + python3 -c 'import json, sys; r = json.load(sys.stdin); print(r["hex"] if r["complete"] else sys.exit("The PSBT is not fully signed."), end="")' > final_psbt.txt && + qr < final_psbt.txt ``` On the online computer, scan it and broadcast: @@ -396,10 +401,9 @@ arbitrary-HRP format direction is not yet merged into that specification. ### QR troubleshooting Maximize the terminal and reduce its font size if a QR does not fit. Keep `qr` -connected to the terminal; redirecting its output creates an image file. If `qr` -is unavailable, `qrencode -8 -t ANSIUTF8` shows the same QR. Reading the -compressed descriptors needs ZBar 0.23.1 or newer for `-Sbinary`; without it, -ZBar rewrites the bytes as text and `gunzip` fails. Only public +connected to the terminal; redirecting its output creates an image file. +Reading the compressed descriptors needs ZBar 0.23.1 or newer for `-Sbinary`; +without it, ZBar rewrites the bytes as text and `gunzip` fails. Only public descriptors, xpubs, PSBTs, and signed transactions may cross the offline boundary by QR. From 6f6c86f3b8d7e3fed8f7379ba45277f09080c340 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 07:05:09 +0000 Subject: [PATCH 5/5] docs: Give out checked addresses from offline Malware on the online computer can swap an address after the offline getaddressinfo check. When the payer is your own phone wallet or is with you, showing the checked address as a QR on the offline screen lets them scan it without it passing through the online computer, so nothing needs comparing. An exchange withdrawal or a payer over the internet needs the address on a networked device, so the guide says to compare the last screen before submitting, such as the exchange's confirmation page, with the address shown offline. Refs #92 Claude-Session: https://claude.ai/code/session_01T233rKgZqE5wzDm3EVTHL1 --- docs/user/guide.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/user/guide.md b/docs/user/guide.md index e5facce..c648001 100644 --- a/docs/user/guide.md +++ b/docs/user/guide.md @@ -261,12 +261,24 @@ address=$(zbarcam --raw --oneshot -Sdisable -Sqrcode.enable) bitcoin-cli -rpcwallet=offline_wallet getaddressinfo "$address" ``` -Give out the address only if the result shows `"ismine": true`, and compare -the address you send character by character with the `"address"` shown on the -offline screen. This works while `offline_wallet` is locked. The offline wallet -recognizes its first 1,000 addresses of each type; past that, a real address -shows `"ismine": false` until you unlock `offline_wallet` and run -`keypoolrefill` with a larger number. +Give out the address only if the result shows `"ismine": true`. This works +while `offline_wallet` is locked. The offline wallet recognizes its first 1,000 +addresses of each type; past that, a real address shows `"ismine": false` until +you unlock `offline_wallet` and run `keypoolrefill` with a larger number. + +When you pay yourself from a phone wallet, or the payer is with you, show the +checked address as a QR on the offline screen and scan it there; nothing needs +comparing: + +```bash +printf %s "$address" | qr +``` + +For an exchange withdrawal or a payer over the internet, the address must pass +through a networked computer or phone, where malware could swap it after the +check. Paste it there, then compare the address on the last screen before you +submit or send, such as the exchange's confirmation page or your sent message, +character by character with the `"address"` shown offline. ### 4. Spend with a PSBT