본문으로 건너뛰기

UR과 CBOR 입문

일반 주소와 PSBT 텍스트를 제외하면, BitTool Signer가 읽거나 쓰는 모든 QR 코드는 Uniform Resource(BCR-2020-005)입니다. UR을 직접 구현할 필요는 없지만(라이브러리를 사용하세요), 계층 구조를 알면 디버깅이 훨씬 쉬워집니다.

CBOR에서 QR 텍스트까지​

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

예를 들어 짧은 EVM 서명 응답은 다음과 같습니다.

ur:eth-signature (단일 파트)
UR:ETH-SIGNATURE/OEADTPDAGDHECKNDDWKIFTGLJLLEBDCEDPFMGWHTADAOHDFPJNBYWYMUYLLTIMKSWDCFWKYTSPVWGYVEGMDNJNWMLSGAOLHGDPATJEGLIOTOQZFPJNQDYNMHQDESRSIHLEHHLGDRTDYLHHUREYGMWTLBMWBKLFCTIHRKJZRHCKMSLFOYADCWJNLUAY

텍스트는 대소문자를 구분하지 않습니다. QR 코드를 만들기 전에 대문자로 바꾸세요. 그러면 문자가 QR 영숫자(alphanumeric) 모드에 맞게 되어, 바이트 모드보다 훨씬 밀도가 높아집니다.

멀티파트(애니메이션) UR​

페이로드가 QR 코드 하나에 담기지 않으면, 인코더는 페이로드를 같은 길이의 프래그먼트로 나누어 파트를 생성합니다.

ur:<type>/<seq>-<count>/<bytewords of CBOR [seq, count, message-length, checksum, fragment]>
  • 파트 1…count는 프래그먼트를 순서대로 전달합니다("순수" 파트).
  • count 이후의 파트는 파운틴(fountain) 파트입니다. seq와 체크섬에서 파생한 시드로 의사 난수 방식으로 고른 프래그먼트들을 XOR한 값이며, 디코더는 이를 이용해 누락된 프래그먼트를 복원할 수 있습니다.
  • count, message-length, checksum(메시지 전체의 CRC-32)은 한 세션의 모든 파트에서 같습니다. BitTool Signer는 두 세션이 섞인 스캔을 거부합니다.

실무상 의미:

  • 파트는 어떤 순서로 스캔해도 되며, 반복된 파트는 무시합니다.
  • 표시할 때는 처음 count개 파트를 반복 재생하지 말고, 루프에서 nextPart()로 파트를 계속 생성하세요.
  • 프래그먼트 크기는 QR 밀도와 프레임 수 사이의 절충입니다. BitTool Signer는 지갑 레벨 페어링에 400바이트, 서명 응답에 180바이트를 사용합니다.

지갑 레벨 페어링 QR 코드의 1번 프레임:

ur:bytes, 4개 중 1번 파트
UR:BYTES/1-4/LPADAACFAMCECYJYHYKIWKHKADLTHKAMCFOLIEJNJLIEIHIEIYKPJZJZIOJTIHJYKTJLJPJEIOJNHSINJTJTIHJYIOKOIHJPJKINJLJTADISHSIAIAJLKPJTJYJKLTONIEKSJOKPIDKSJLKSJOKPIDENFXHSJYHGIEINHTINJLIEJNGOIHGHFYJOETGSGHECJLJPETJTJNIDGRGLIAKPKKKOKNEMHGKKJEJKHFFGJEGRFWEEGMFDKTFXFYEOHDKKKPKOGDFEIDKOJSFPGYHKEOJPFPGDJKISHGIAGTGSJLGDEYIYGTFGGTGRFDGDGEEEHTIHHTHDHKHFGOISGSKOEHHFGTJPIMGDFXEMGDHGENHFIHIAISHSINJTIAIDJYIAIYIEIHJPINKOIHOEIYIAISHSJTIOIHIAEHDLDRIOJPIHIAIHINKOIHIADYDLDRIYJKIAJPINJOJYIYJOEYKTJOJEISJEHSIAIAJLKPJTJYGDHSJYISJEJNDLETEEDIDLDYDIDLDYDIONIEKSJOKPIDKSJLKSJOKPIDENFWIOFWIOJKIHJKJOHGKOFEGMFGEOGSFDGYKPENFXJTJSIEKOIYFEKOJYGTIAGYIMHKJPIAGMKNKSECEOGYGEIMGUKSHSJPIMEYHSIYHKHGIAGSJYIHJLFLHFJEKKEMFYEOGOGRFYGDESGYKKJPGSJOJPGYEOHFFXFEFXJLHKEEESKKIYIEFYFEFDFLFXJYGTGTIMESEYJOGMIHGOJKGYIHIAISHSINJTIAIDJYIAIYIEIHJPINKOIHOEIYIAISHSJTIOIHUYSWLYTA

이 페이로드에 쓰이는 CBOR​

CBOR의 작은 부분집합만 사용합니다.

메이저 타입용도
0 unsigned int맵 키, 데이터 타입, 체인 ID, 지문, 경로 인덱스
2 byte string서명 데이터, 키, 체인 코드, 서명, PSBT
3 text stringorigin, 이름, 페어링 맵의 키와 값
4 array경로 구성 요소, 디스크립터 목록, 페어링 계정
5 map모든 요청, 응답, 키 구조
6 tag타입이 지정된 항목(아래 참고)
7 simpletrue / false (is-private, 강화 플래그)

요청은 확정 길이를 사용해야 하며, 중복 맵 키와 뒤따르는 잉여 바이트가 없어야 합니다. 부동소수점, 음의 정수, 비확정 길이 항목은 이 구조 어디에도 쓰이지 않습니다.

태그​

태그이름감싸는 대상
37uuid요청 ID 바이트
303crypto-hdkeyHD 키 맵
304crypto-keypath경로 맵 {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(…)

바이트 문자열과 텍스트의 구분은 중요합니다. 요청 ID는 UUID 텍스트가 아니라 태그 37 안의 바이트이며, 키 6의 EVM 주소는 "0x…"가 아니라 20바이트 원시 데이터입니다.

요청 직접 읽어 보기​

EIP-1559 예제의 CBOR에 주석을 단 것입니다.

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
같은 바이트 전체
a701d825505f1e9b2c7d3a4e6f8a0b1c2d3e4f5a0102583102ef010c843b9aca008506fc23ac0082520894000000000000000000000000000000000000dead872386f26fc1000080c00304040105d90130a2018a182cf5183cf500f500f400f4021a73c5da0a06549858effd232b4033e47d90003d41ec34ecaeda94076e426974546f6f6c2057616c6c6574

라이브러리​

언어URCBOR
JavaScript / TypeScript@ngraveio/bc-ur, 타입 클래스가 필요하면 Keystone @keystonehq/bc-ur-registry*cborg
SwiftURKitURKit의 DCBOR, SwiftCBOR
Kotlin / JavaHummingbirdHummingbird, com.upokecenter:cbor
Rusturminicbor, ciborium