Skip to main content

UR and CBOR primer

Every QR code BitTool Signer reads or writes, except plain addresses and PSBT text, is a Uniform Resource (BCR-2020-005). You do not need to implement UR yourself (use a library), but knowing the layers makes debugging much easier.

From CBOR to QR text​

CBOR bytes
→ append CRC-32 (4 bytes)
→ Bytewords "minimal": each byte becomes 2 letters (first + last letter of a 4-letter word)
→ "ur:" + type + "/" + bytewords single part

For example, a short EVM signature response:

ur:eth-signature (one part)
UR:ETH-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTADAOHDFPJNBYWYMUYLLTIMKSWDCFWKYTSPVWGYVEGMDNJNWMLSGAOLHGDPATJEGLIOTOQZFPJNQDYNMHQDESRSIHLEHHLGDRTDYLHHUREYGMWTLBMWBKLFCTIHRKJZRHCKMSLFOYADCWJNLUAY

The text is case-insensitive. Upper-case it before making the QR code: the characters then fit QR alphanumeric mode, which is much denser than byte mode.

Multi-part (animated) URs​

When a payload is too large for one QR code, the encoder splits it into fragments of equal length and produces parts:

ur:<type>/<seq>-<count>/<bytewords of CBOR [seq, count, message-length, checksum, fragment]>
  • Parts 1…count carry the fragments in order ("pure" parts).
  • Parts after count are fountain parts: the XOR of a pseudo-random selection of fragments, chosen from a seed derived from seq and the checksum. A decoder can rebuild missing fragments from them.
  • count, message-length and checksum (CRC-32 of the whole message) are the same in every part of one session. BitTool Signer rejects a scan that mixes two sessions.

Practical consequences:

  • Scan parts in any order; ignore repeats.
  • Display: keep generating parts with nextPart() in a loop rather than replaying the first count parts.
  • Fragment size is a trade-off between QR density and frame count. BitTool Signer uses 400 bytes for the wallet-level pairing and 180 bytes for its signing responses.

Frame 1 of the wallet-level pairing QR code:

ur:bytes, part 1 of 4
UR:BYTES/1-4/LPADAACFAMCECYJYHYKIWKHKADLTHKAMCFOLIEJNJLIEIHIEIYKPJZJZIOJTIHJYKTJLJPJEIOJNHSINJTJTIHJYIOKOIHJPJKINJLJTADISHSIAIAJLKPJTJYJKLTONIEKSJOKPIDKSJLKSJOKPIDENFXHSJYHGIEINHTINJLIEJNGOIHGHFYJOETGSGHECJLJPETJTJNIDGRGLIAKPKKKOKNEMHGKKJEJKHFFGJEGRFWEEGMFDKTFXFYEOHDKKKPKOGDFEIDKOJSFPGYHKEOJPFPGDJKISHGIAGTGSJLGDEYIYGTFGGTGRFDGDGEEEHTIHHTHDHKHFGOISGSKOEHHFGTJPIMGDFXEMGDHGENHFIHIAISHSINJTIAIDJYIAIYIEIHJPINKOIHOEIYIAISHSJTIOIHIAEHDLDRIOJPIHIAIHINKOIHIADYDLDRIYJKIAJPINJOJYIYJOEYKTJOJEISJEHSIAIAJLKPJTJYGDHSJYISJEJNDLETEEDIDLDYDIDLDYDIONIEKSJOKPIDKSJLKSJOKPIDENFWIOFWIOJKIHJKJOHGKOFEGMFGEOGSFDGYKPENFXJTJSIEKOIYFEKOJYGTIAGYIMHKJPIAGMKNKSECEOGYGEIMGUKSHSJPIMEYHSIYHKHGIAGSJYIHJLFLHFJEKKEMFYEOGOGRFYGDESGYKKJPGSJOJPGYEOHFFXFEFXJLHKEEESKKIYIEFYFEFDFLFXJYGTGTIMESEYJOGMIHGOJKGYIHIAISHSINJTIAIDJYIAIYIEIHJPINKOIHOEIYIAISHSJTIOIHUYSWLYTA

CBOR in these payloads​

Only a small subset of CBOR appears:

Major typeUsed for
0 unsigned intMap keys, data types, chain IDs, fingerprints, path indexes
2 byte stringSigning data, keys, chain codes, signatures, PSBTs
3 text stringOrigin, names, the pairing map's keys and values
4 arrayPath components, descriptor lists, pairing accounts
5 mapEvery request, response and key structure
6 tagTyped items (below)
7 simpletrue / false (is-private, hardened flags)

Requests must use definite lengths, no duplicate map keys and no trailing bytes. Floats, negative integers and indefinite-length items have no place in any of these structures.

Tags​

TagNameWraps
37uuidRequest ID bytes
303crypto-hdkeyHD key map
304crypto-keypathPath map {1: components, 2: source-fingerprint, 3: depth}
305crypto-coin-info{1: coin type, 2: network}
400script-hashsh(…)
401witness-script-hashwsh(…)
403public-key-hashpkh(…)
404witness-public-key-hashwpkh(…)
409taproottr(…)

Byte string vs. text matters: a request ID is bytes inside tag 37, not the UUID as text; an EVM address in key 6 is 20 raw bytes, not "0x…".

Reading a request by hand​

The CBOR of the EIP-1559 example, annotated:

a7 map, 7 entries
01 d8 25 50 5f1e9b2c…5a01 1: tag 37 (uuid), 16 bytes
02 58 31 02ef010c…80c0 2: 49 bytes: 0x02 ‖ rlp(unsigned EIP-1559 tx)
03 04 3: data-type 4
04 01 4: chain-id 1
05 d9 0130 a2 5: tag 304 (crypto-keypath), map 2
01 8a 18 2c f5 18 3c f5 00 f5 00 f4 00 f4 components [44,true,60,true,0,true,0,false,0,false]
02 1a 73c5da0a source-fingerprint 0x73c5da0a
06 54 9858effd…eda94 6: 20-byte address
07 6e "BitTool Wallet" 7: origin
The same bytes in full
a701d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0102583102ef010c843b9aca008506fc23ac0082520894000000000000000000000000000000000000dead872386f26fc1000080c00304040105d90130a2018a182cf5183cf500f500f400f4021a73c5da0a06549858effd232b4033e47d90003d41ec34ecaeda94076e426974546f6f6c2057616c6c6574

Libraries​

LanguageURCBOR
JavaScript / TypeScript@ngraveio/bc-ur; Keystone @keystonehq/bc-ur-registry* for typed classescborg
SwiftURKitURKit's DCBOR, SwiftCBOR
Kotlin / JavaHummingbirdHummingbird, com.upokecenter:cbor
Rusturminicbor, ciborium