Getting things on and off
The camera is the main way in. All four screens that take a file can scan it: a transaction, a quorum descriptor or coordinator setup file, an encrypted backup, and a label file. The camera names what it is being pointed at, so you can see when you are holding up the wrong card.
Everything the device produces is shown as a QR code and, under a disclosure, as text to copy onto a card. Large payloads split across several codes automatically. See what the air gap does and does not do for what it stops.
The two transports
QR codes, and only QR codes today. The device reads them with a USB (UVC) webcam; a Raspberry Pi camera module on the CSI connector has no driver in the pinned kernel. No camera has read a code on a Pi yet.
SD card: planned, not built. Nothing in the image mounts removable media
(see provisioning/HARDENING.md), so no screen can read a card yet. Until then,
a device with no camera can show codes for what leaves it but cannot receive a
transaction.
Codes too large for one frame
A single QR code at the density this device draws holds a few hundred bytes: enough for an xpub or an address, usually not a signed transaction. That is shown as an animated sequence in one of two formats:
| Format | Read by | On this device |
|---|---|---|
| BBQr | Sparrow, Coldcard, Nunchuk | The default for a signed PSBT |
UR (crypto-psbt) |
SeedSigner, Jade, Keystone, Sparrow | Pick UR under the code |
The scanner reads either without being told which. Both use QR alphanumeric mode under the same density cap.
UR is written as crypto-psbt, not psbt, because SeedSigner and Jade read
only crypto-psbt. The scanner accepts both. Later UR frames mix several pieces,
so a missed frame can be recovered from the ones after it; progress counts
pieces known, not frames seen. UR carries a checksum of the whole transaction,
so two UR transfers are always told apart.
BBQr frames can arrive in any order, and a missed one is picked up on the next pass. The scanner lists by number the frames it still needs.
- Each frame decodes on its own, so a misread frame is named instead of the transfer failing at the end.
- The device never writes compressed frames. It reads them, because BBQr requires a receiver to, but writing them would put a compressor in the path that produces signed transactions.
- Frames from two transfers are not merged when their headers differ: a second sequence with a different frame count, file type or encoding stops the scanner. BBQr has no transfer id and no whole-payload checksum, so identical headers cannot be told apart. For a PSBT or transaction, a full set that does not join into exactly one is refused and the count restarts. That checks the result is well formed, not that it came from one screen, so show one sequence at a time. The review screen is the backstop: it shows what was assembled.
The decoder is the one dependency that reads
The code that draws QR codes is in this repository, covered by MANIFEST.lock,
and checked against an independent decoder at every version and error correction
level, so verifying the manifest hash verifies it.
Reading uses zxing-wasm, a long-established decoder. Its WebAssembly is served
from the device itself, never the internet, and the code refuses to load it from
any other origin. Nothing it returns is trusted: the output is a candidate for
the ordinary parser and review screen, and a BBQr header's file type tells the
device what to try first, never what to accept.
When something else reads a code wrong
Here a camera reads this panel into software on a networked machine. No checksum is printed under the codes, on purpose: no coordinator would check one this device invented. What catches a misread is what the receiving software already does:
| Code | What a misread does |
|---|---|
| Encrypted backup | AES-256-GCM: a wrong byte fails the tag and restore refuses |
| Signed transaction | Fails to parse in the coordinator. One that somehow parsed still has to be broadcast, and the transaction it names was reviewed here first |
| Cosigner bundle | Produces a descriptor this device holds no key in, which registration refuses (INV-MULTI-6) |
| Address | The characters are on screen beside the code, grouped in fours, under a warning to compare them against the payer's screen |
| Descriptor | Carries its own BIP-380 checksum, which the reading software shows |
| Message proof | A corrupted signature fails verification |
| Labels | BIP-329 is one JSON document per line, so a corrupted read stops parsing. A label also decides nothing |
make qr-readback keeps that set closed: each new code must say what makes a
misread loud before it ships, and entries no screen renders are removed.
Rendered from docs/USING.md, the same bytes that ship with the code. SHA-256 b735aa55a6ae5cc7682c2b90fe2c551b3a396a02240f45936d50e6540d65a989