Skip to content

docs: Restore the qr steps for offline signing - #93

Open
BenWestgate wants to merge 5 commits into
reviewability-v1from
claude/project-thread-4hyptt
Open

BenWestgate wants to merge 5 commits into
reviewability-v1from
claude/project-thread-4hyptt

Conversation

@BenWestgate

@BenWestgate BenWestgate commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Requested by Ben · project thread

Before: the user guide's "watch-only wallet and offline signer" section only linked to Bitcoin Core's offline-signing tutorial. The guide still referred to "the QR tools below" and had a QR troubleshooting section, but no step used qr.

After: the section follows the tutorial's offline_wallet and watch_only_wallet in four steps: restore the signer with ms32 wallet; send the gzipped public descriptors to a blank watch-only wallet by QR; get receiving addresses and labels from the watch-only wallet and check each one on the offline signer before giving it out; then carry the PSBT to the signer and the signed transaction back by QR. Every command uses only what Tails includes (python3, gzip, qr, zbarcam) besides Bitcoin Core and codex32, so it works in Bails, which can't install packages.

The tutorial moves an exportwatchonlywallet file, which doesn't fit in a QR even compressed (12,288 bytes on regtest; 2,365 gzipped, over qr's 2,331-byte default limit). The guide sends the listdescriptors entries instead and imports them with importdescriptors. gzip shrinks the 8 default descriptors to about 640 bytes. The online side reads it with zbarcam -Sbinary (ZBar 0.23.1 or newer) and gunzip. A short python3 command takes the descriptors, the PSBT and the signed hex out of Core's JSON in place of jq, which Tails lacks.

The receive check exists because a compromised online computer could show an address it controls. The address is scanned into the offline computer and looked up with getaddressinfo; it is given out only when the result shows "ismine": true. For your own phone wallet or a payer who is present, the offline screen shows the checked address as a QR, so nothing needs comparing. For an exchange withdrawal or a payer over the internet, the address passes through a networked device, so the guide says to compare the last screen before submitting with the address shown offline. The check works while offline_wallet is locked. Which receive flow the guide should use is still open in #112, which compares this one with making addresses on the offline wallet.

How: the commands were run end to end against Bitcoin Core 32.0rc2 on two separate regtest nodes, with zbarimg reading the generated QR images in place of a camera. All 8 descriptors imported from the gzipped QR, both wallets gave the same first address, a labelled online address checked "ismine": true on the locked signer while a foreign one checked false, the PSBT and the signed hex survived the QR round trip, and sendrawtransaction accepted the result. Signing with a wallet that can't sign stops with "The PSBT is not fully signed." and shows no QR. Without -Sbinary, ZBar rewrites the gzip bytes as text and gunzip fails. Past the signer's first 1,000 addresses a real address checks false until keypoolrefill, as the guide says. Docs-only.

Closes #92
Closes #111

🤖 Generated with Claude Code

https://claude.ai/code/session_01T233rKgZqE5wzDm3EVTHL1


Generated by Claude Code

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
@BenWestgate BenWestgate added the documentation Improvements or additions to documentation label Oct 1, 2026 — with Claude
@BenWestgate BenWestgate self-assigned this Oct 1, 2026
@BenWestgate BenWestgate added gate: adversarial review Resolve, merge, or explicitly defer before the next full adversarial review. area: wallet/core Wallet integration and Bitcoin Core boundaries. area: cli Command-line interface behavior. labels Oct 1, 2026
@BenWestgate
BenWestgate marked this pull request as ready for review October 1, 2026 02:27
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@BenWestgate BenWestgate left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI-generated release-gate review, posted at the maintainer’s request.

ACK d4641c4. I independently exercised the documented Bitcoin Core 32.0rc2 RPC sequence on regtest: exporting listdescriptors, importing the filtered descriptor array into a blank private-key-disabled wallet, comparing the next receiving address, creating a watch-only send PSBT, signing it with walletprocesspsbt, and broadcasting the resulting hex with sendrawtransaction. The descriptor QR payload was 1,976 bytes; address comparison succeeded; the watch-only send was incomplete with a PSBT; offline processing completed with hex; broadcast succeeded.

The shell boundaries are also appropriate: descriptor JSON is passed as an RPC stdin argument rather than shell-evaluated, and PSBT/raw transaction values are quoted. The existing rc1 documentation URL is a release-documentation follow-up, not a correctness blocker while stable v32.0 does not yet exist and the release monitor is active.

No correctness or security blocker found. This commit is Claude-authored, so it still needs responsible human review/rewrite as required by the project’s authorship policy before integration.

@BenWestgate

Copy link
Copy Markdown
Owner Author

Is the exportwatchonlywallet too large for qr even after it is compressed?

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
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
claude added 2 commits October 3, 2026 07:05
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
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

@BenWestgate BenWestgate left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex current-head re-review: ACK 6f6c86f.

I re-reviewed the four follow-ups added after the earlier d4641c4 ACK. The descriptor payload is compressed well under the QR limit; the receive path now verifies each online-generated address against the locked offline signer and gives out the checked address directly from the offline screen where possible; Tails/Bails-compatible python3 replaces the unavailable jq; and the signed-transaction path fails closed when the PSBT is incomplete. The guide keeps only public descriptors, PSBTs, checked addresses and signed transactions crossing the QR boundary. There are no inline review threads, and exact-head Python-package run 694 succeeded.

No correctness or security blocker found. These Claude-authored documentation commits still require responsible-human rewrite/squash before integration.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: cli Command-line interface behavior. area: wallet/core Wallet integration and Bitcoin Core boundaries. documentation Improvements or additions to documentation gate: adversarial review Resolve, merge, or explicitly defer before the next full adversarial review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants