Skip to the content
nullroute
Browse the docs

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