본문으로 건너뛰기

Bitcoin (PSBT)

ur:crypto-psbtur:psbtBBQrPSBT v0 / v2

Bitcoin 요청은 PSBT입니다. 지갑이 PSBT를 만들면, BitTool Signer가 서명을 추가해 PSBT를 돌려주고, 지갑이 이를 최종화해 브로드캐스트합니다.

전송 방식​

요청응답참고
ur:crypto-psbtur:crypto-psbt권장. UR 본문 = PSBT를 담은 CBOR 바이트 문자열.
ur:psbtur:psbt레지스트리의 새 이름으로, 내용은 같습니다.
BBQr (B$…, 인코딩 H, 2, Z)BBQr (2 또는 H)Coinkite의 형식입니다. Z(deflate)도 받을 수 있으며, 응답은 2(Base32)를 사용하고 요청이 H였다면 H를 사용합니다.
QR 코드 하나에 담은 Base64(cHNidP…) 또는 hex(70736274ff…) 텍스트1,800자 이하이면 같은 텍스트, 아니면 애니메이션 ur:crypto-psbt작은 트랜잭션용입니다.
.psbt 파일(Base64 또는 hex, 줄바꿈 포함 가능)같음서명 기기 iPhone의 Files/AirDrop을 통해 전달합니다.

응답은 (위에 명시한 경우를 제외하면) 항상 요청과 같은 형식을 사용하므로, 이미 가진 디코더를 그대로 재사용할 수 있습니다.

PSBT 필수 항목​

요구 사항세부 내용
버전PSBT v0 또는 v2(BIP 370). v0이 가장 안전한 선택입니다.
크기입력 1–100개, 출력 1–100개.
파생 정보서명 기기가 서명해야 하는 모든 입력에는 해당 키의 bip32Derivation(마스터 지문 + 전체 경로)이 있어야 합니다. Taproot 입력은 tapBip32Derivation, tapInternalKey를 사용합니다.
이전 트랜잭션Legacy 및 SegWit v0 입력(P2PKH, P2WPKH, P2SH-P2WPKH, P2WSH …)에는 이전 트랜잭션 전체(nonWitnessUtxo)가 필요합니다. witnessUtxo만 있는 경우는 모든 입력이 Taproot일 때만 허용됩니다.
SighashSIGHASH_ALL, 또는 Taproot의 경우 SIGHASH_DEFAULT. 그 외는 거부됩니다.
잔돈잔돈 출력에도 bip32Derivation을 넣으세요. BitTool Signer는 자신의 키로 출력의 스크립트를 다시 파생해 확인한 경우에만 그 출력을 "잔돈"으로 표시합니다.
출력표준 스크립트와 83바이트 이하의 OP_RETURN.
네트워크메인넷 전용.
nonWitnessUtxo가 필요한 이유

SegWit v0 서명은 사용하는 입력의 금액에는 커밋하지만 다른 입력의 금액에는 커밋하지 않습니다. 악의적인 코디네이터는 두 차례의 서명 과정에서 입력 금액을 속여 서명 기기가 막대한 수수료를 지불하게 만들 수 있습니다(2020년에 공개된 SegWit 수수료 공격). 이전 트랜잭션 전체가 있으면 서명 기기가 모든 금액을 직접 확인할 수 있습니다. Taproot 서명은 모든 입력 금액에 커밋하므로, 입력이 모두 Taproot인 트랜잭션은 예외입니다.

지원 스크립트​

스크립트서명 여부
P2PKH, P2WPKH, P2SH-P2WPKH예(단일 키)
P2TR 키 경로(BIP86)예
멀티시그 P2SH, P2WSH, P2SH-P2WSH (multi/sortedmulti)예, 로컬 키마다 부분 서명 하나
P2TR 스크립트 경로, CHECKSIG/CHECKSIGVERIFY 및 CHECKSIGADD 임계값 리프(리프 버전 0xc0)예
MuSig2 및 기타 스크립트아니요

응답​

응답은 서명이 추가된 PSBT입니다. ECDSA 입력에는 partialSig, Taproot에는 tapKeySig 또는 tapScriptSig가 추가됩니다. 최종화되지 않으며 그 밖의 내용은 바뀌지 않습니다. 최종화하기 전에 다음을 수행하세요.

  1. 서명되지 않은 트랜잭션이 보낸 것과 바이트 단위로 같은지 확인합니다(PSBT 전송 방식에는 요청 ID가 없습니다).
  2. 새로 추가된 모든 서명을 해당 입력에 대해 검증합니다.
  3. 멀티시그라면 다른 공동 서명자의 PSBT와 합칩니다.
  4. 최종화하고, 트랜잭션을 추출해 브로드캐스트합니다.
1.0.0의 알려진 문제
  • 잔돈이 있는 소각(R3-01, 낮음, iPhone만). 금액이 0이 아닌 OP_RETURN 출력으로 코인을 소각하면서 잔돈도 돌려받는 트랜잭션의 경우, BitTool Signer 결과 화면과 활동 기록의 한 줄 요약이 "Every output returns to your wallet"(모든 출력이 지갑으로 돌아옵니다)로 표시됩니다. 서명 화면 자체는 소각, 그 금액, 높은 위험을 올바르게 보여 주며 PSBT에도 영향이 없습니다. Android용 BitTool Signer 1.0.0은 요약에도 소각을 표시합니다.
  • 어떤 인덱스든 잔돈으로 간주(BTS-08, 중간, 두 앱 모두). 출력의 스크립트가 지갑 자체의 단일 서명 구성 중 하나와 일치하면 어떤 인덱스든 잔돈으로 간주됩니다. 보기 전용 지갑이 절대 스캔하지 않는 인덱스도 포함됩니다. 잔돈은 일반적인 인덱스에 두고, 모든 자금을 사용자 지갑의 이례적인 경로로 옮기는 요청은 의심하세요.

보안 검토를 참고하세요.

코드​

bitcoinjs-lib으로 PSBT를 만들고 Keystone의 @keystonehq/bc-ur-registry로 ur:crypto-psbt에 감쌉니다.

examples/bitcoin.mjs
import * as bitcoin from 'bitcoinjs-lib';
import { CryptoPSBT } from '@keystonehq/bc-ur-registry';
import { URDecoder } from '@ngraveio/bc-ur';
import { secp256k1 } from '@noble/curves/secp256k1.js';

// utxos: [{ txid, vout, value, prevTxHex, pubkey, path }] path like "m/84'/0'/0'/0/3"
export function buildPsbt({ utxos, to, amount, change, masterFingerprint }) {
const fingerprint = Buffer.from(masterFingerprint, 'hex');
const psbt = new bitcoin.Psbt(); // mainnet
for (const u of utxos) {
const prevTx = bitcoin.Transaction.fromHex(u.prevTxHex);
psbt.addInput({
hash: u.txid,
index: u.vout,
nonWitnessUtxo: Buffer.from(u.prevTxHex, 'hex'), // required for SegWit v0 and legacy inputs
witnessUtxo: { script: prevTx.outs[u.vout].script, value: u.value },
bip32Derivation: [{ masterFingerprint: fingerprint, path: u.path, pubkey: u.pubkey }],
});
}
psbt.addOutput({ address: to, value: amount });
psbt.addOutput({ // change: give the path so the signer can recognize it
address: change.address, value: change.value,
bip32Derivation: [{ masterFingerprint: fingerprint, path: change.path, pubkey: change.pubkey }],
});
const encoder = new CryptoPSBT(psbt.toBuffer()).toUREncoder(400);
const frames = [];
for (let i = 0; i < encoder.fragmentsLength; i++) frames.push(encoder.nextPart().toUpperCase());
return { psbt, frames };
}

서명된 PSBT를 읽어 확인하고 최종화합니다.

examples/bitcoin.mjs
const validator = (pubkey, msghash, signature) => secp256k1.verify(signature, msghash, pubkey, { prehash: false });

export function finalizeSignedPsbt(frames, original) {
const decoder = new URDecoder();
for (const frame of frames) decoder.receivePart(frame);
if (!decoder.isSuccess()) throw new Error('Incomplete or invalid QR');
const ur = decoder.resultUR();
if (ur.type !== 'crypto-psbt') throw new Error(`Expected crypto-psbt, got ${ur.type}`);

const signed = bitcoin.Psbt.fromBuffer(CryptoPSBT.fromCBOR(ur.cbor).getPSBT());
// The signer only adds signatures: the transaction itself must be unchanged.
if (!signed.data.globalMap.unsignedTx.toBuffer().equals(original.data.globalMap.unsignedTx.toBuffer()))
throw new Error('The signed PSBT is for a different transaction');
if (!signed.validateSignaturesOfAllInputs(validator)) throw new Error('Invalid signature');

signed.finalizeAllInputs();
return signed.extractTransaction().toHex(); // ready to broadcast
}

테스트 벡터​

m/84'/0'/0'/0/0의 입력 100,000 sats에서 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4로 60,000 sats를 보내고, 39,000 sats는 m/84'/0'/0'/1/0으로 잔돈, 1,000 sats는 수수료입니다. 이전 트랜잭션은 합성 데이터입니다.

요청 (내 지갑 → BitTool Signer)

crypto-psbt3 프레임
UR:CRYPTO-PSBT/1-3/LPADAXCFADJLCYRFDSIHPRHDKGHKADJZJOJKIDJYZMADAEJSAOAEAEAEADWLBSWTPKCNUTLOFSDRVDOYSGFNCFPKFZCMLSVLNSYKJPCEWKSPNEYKTDRLPMGESWAEAEAEAEAEZMZMZMZMAOHNWDAEAEAEAEAEAECMAEBBKPCKKOVSCFMEMTTYGHMWCEFETTQDOTCNWNFXFRTBHDMKAEAEAEAEAEAECMAEBBFMEEMKHLSGJLUTSOZOENNLFZVESTTPVOLTFHGMNSAEAEAEQZMKTNSA
1 / 3
서명되지 않은 PSBT (Base64)
cHNidP8BAHECAAAAAekP8Koj3Yg9KuehyjwZqkAWg+Oc9XIc9Mif9dK3rUrGAAAAAAD/////AmDqAAAAAAAAFgAUdR526BmRltRUlBxF0bOjI/FDO9ZYmAAAAAAAABYAFD40mF3Kb93J+zaZQOTH2OKHP1KcAAAAAAABAFICAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAD/////AaCGAQAAAAAAFgAUwM681sPTyox13F7GLr5VMw75EOIAAAAAAQEfoIYBAAAAAAAWABTAzrzWw9PKjHXcXsYuvlUzDvkQ4iIGAzDVT9DdQgpuX402JPXzSCyuNQ951fB1O/W+75wtka88GHPF2gpUAACAAAAAgAAAAIAAAAAAAAAAAAAAIgIDAlMkiI5Cmrjj268feAJki5zQHptBhIXF+kwbm1cA4aYYc8XaClQAAIAAAACAAAAAgAEAAAAAAAAAAA==
QR 텍스트와 원본 CBOR 보기
QR 텍스트 · ur:crypto-psbt
UR:CRYPTO-PSBT/1-3/LPADAXCFADJLCYRFDSIHPRHDKGHKADJZJOJKIDJYZMADAEJSAOAEAEAEADWLBSWTPKCNUTLOFSDRVDOYSGFNCFPKFZCMLSVLNSYKJPCEWKSPNEYKTDRLPMGESWAEAEAEAEAEZMZMZMZMAOHNWDAEAEAEAEAEAECMAEBBKPCKKOVSCFMEMTTYGHMWCEFETTQDOTCNWNFXFRTBHDMKAEAEAEAEAEAECMAEBBFMEEMKHLSGJLUTSOZOENNLFZVESTTPVOLTFHGMNSAEAEAEQZMKTNSA
UR:CRYPTO-PSBT/2-3/LPAOAXCFADJLCYRFDSIHPRHDKGAEAEADAEGMAOAEAEAEADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADAEAEAEAEAEZMZMZMZMADNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOAEAEAEAEADADCTNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOCPAMGWBSRYCW
UR:CRYPTO-PSBT/3-3/LPAXAXCFADJLCYRFDSIHPRHDKGAXDYTLGWTIUTFWBKJTHELGENDKYKWFFDDWPLECBSKKTLWTKPFRYKRNWSNSDPMEPEFNCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAAEAEAEAEAEAEAEAEAEAECPAOAXAOGUDKLOMNFWNYROVLUYPECTKSAOIELUNSTICKNDFPLRLPSKZSGSCWNDHGAEVYOLCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAADAEAEAEAEAEAEAEAEAEAEGWCEHDPR
UR CBOR (hex)
59016c70736274ff0100710200000001e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c0000000000010052020000000101010101010101010101010101010101010101010101010101010101010101010000000000ffffffff01a086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e20000000001011fa086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e222060330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c1873c5da0a54000080000000800000008000000000000000000000220203025324888e429ab8e3dbaf1f7802648b9cd01e9b418485c5fa4c1b9b5700e1a61873c5da0a540000800000008000000080010000000000000000

응답 (BitTool Signer → 내 지갑)

서명되어 돌아온 PSBT (Base64)
cHNidP8BAHECAAAAAekP8Koj3Yg9KuehyjwZqkAWg+Oc9XIc9Mif9dK3rUrGAAAAAAD/////AmDqAAAAAAAAFgAUdR526BmRltRUlBxF0bOjI/FDO9ZYmAAAAAAAABYAFD40mF3Kb93J+zaZQOTH2OKHP1KcAAAAAAABAFICAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAD/////AaCGAQAAAAAAFgAUwM681sPTyox13F7GLr5VMw75EOIAAAAAAQEfoIYBAAAAAAAWABTAzrzWw9PKjHXcXsYuvlUzDvkQ4iICAzDVT9DdQgpuX402JPXzSCyuNQ951fB1O/W+75wtka88SDBFAiEAnxTBvsytWOKIqCL+jVnOZoU8wKKB3sg9FqBU6YKagoUCIC4lC/U2vuJKimL4vEacD8f6CnzzwQjDbBZOBoNWSJMgASIGAzDVT9DdQgpuX402JPXzSCyuNQ951fB1O/W+75wtka88GHPF2gpUAACAAAAAgAAAAIAAAAAAAAAAAAAAIgIDAlMkiI5Cmrjj268feAJki5zQHptBhIXF+kwbm1cA4aYYc8XaClQAAIAAAACAAAAAgAEAAAAAAAAAAA==
QR 텍스트와 원본 CBOR 보기
QR 텍스트 · ur:crypto-psbt
UR:CRYPTO-PSBT/1-3/LPADAXCFADUYCYVWCKDTWYHDNEHKADTPJOJKIDJYZMADAEJSAOAEAEAEADWLBSWTPKCNUTLOFSDRVDOYSGFNCFPKFZCMLSVLNSYKJPCEWKSPNEYKTDRLPMGESWAEAEAEAEAEZMZMZMZMAOHNWDAEAEAEAEAEAECMAEBBKPCKKOVSCFMEMTTYGHMWCEFETTQDOTCNWNFXFRTBHDMKAEAEAEAEAEAECMAEBBFMEEMKHLSGJLUTSOZOENNLFZVESTTPVOLTFHGMNSAEAEAEAEAEADAEGMAOAEAEAEADADADADADADADADADADADADADADADADADADADADADADADADADADADCLBBLDOY
UR:CRYPTO-PSBT/2-3/LPAOAXCFADUYCYVWCKDTWYHDNEADADADADADADAEAEAEAEAEZMZMZMZMADNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOAEAEAEAEADADCTNBLNADAEAEAEAEAECMAEBBRTTORFTBSRTESGLKKPUOHYSWDMRNGOEOBAYTBEVOCPAOAXDYTLGWTIUTFWBKJTHELGENDKYKWFFDDWPLECBSKKTLWTKPFRYKRNWSNSDPMEPEFNFDDYFEAOCLAENEBBSERNSFPMHDVOLOPDCPZELGHKTOIYLPFNRTOELYUESPFSCMNBGHWLLFNYLFLPAONLJTAAPK
UR:CRYPTO-PSBT/3-3/LPAXAXCFADUYCYVWCKDTWYHDNECXDMDABDYKENRNVOGELEIDYARFFGNSBSSTZSBKKEWFSEAYSRJZCMGLAMLSHFFDMUCXADCPAMAXDYTLGWTIUTFWBKJTHELGENDKYKWFFDDWPLECBSKKTLWTKPFRYKRNWSNSDPMEPEFNCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAAEAEAEAEAEAEAEAEAEAECPAOAXAOGUDKLOMNFWNYROVLUYPECTKSAOIELUNSTICKNDFPLRLPSKZSGSCWNDHGAEVYOLCSJKSKTNBKGHAEAELAAEAEAELAAEAEAELAADAEAEAEAEAEAEAEAEAEAEHHDLWYGS
UR CBOR (hex)
5901d870736274ff0100710200000001e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c0000000000010052020000000101010101010101010101010101010101010101010101010101010101010101010000000000ffffffff01a086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e20000000001011fa086010000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e222020330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c4830450221009f14c1beccad58e288a822fe8d59ce66853cc0a281dec83d16a054e9829a828502202e250bf536bee24a8a62f8bc469c0fc7fa0a7cf3c108c36c164e0683564893200122060330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c1873c5da0a54000080000000800000008000000000000000000000220203025324888e429ab8e3dbaf1f7802648b9cd01e9b418485c5fa4c1b9b5700e1a61873c5da0a540000800000008000000080010000000000000000

지갑이 조립한 결과

finalTxHex
02000000000101e90ff0aa23dd883d2ae7a1ca3c19aa401683e39cf5721cf4c89ff5d2b7ad4ac60000000000ffffffff0260ea000000000000160014751e76e8199196d454941c45d1b3a323f1433bd658980000000000001600143e34985dca6fddc9fb369940e4c7d8e2873f529c024830450221009f14c1beccad58e288a822fe8d59ce66853cc0a281dec83d16a054e9829a828502202e250bf536bee24a8a62f8bc469c0fc7fa0a7cf3c108c36c164e06835648932001210330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c00000000
txid
6f492df497b5e455fb3305d0272fe9c6dc3d8cc9d0054be3aaefee77daca82b7

멀티시그​

  1. 각 공동 서명자의 키를 가져옵니다. BitTool Signer는 BIP48 공동 서명자 키를 코인 레벨 페이지에서 설명한 방식으로 내보냅니다(P2WSH는 m/48'/0'/0'/2').
  2. 각 입력에 witnessScript(P2SH-P2WSH라면 redeemScript도)와 모든 공동 서명자 키의 bip32Derivation 항목을 넣어 PSBT를 만듭니다.
  3. 각 서명 기기에 차례로 보내거나, 병렬로 보낸 뒤 합칩니다. BitTool Signer는 자신이 보유한 키에 대해서만 서명을 추가합니다. iPhone 하나가 서로 다른 지갑에 공동 서명자 키 두 개를 갖고 있다면, 지갑마다 비밀번호를 따로 요청합니다.
  4. 기존 서명은 검증된 뒤 유지되므로, PSBT는 어떤 순서로 돌려도 됩니다.

BitTool Signer는 서명을 요청받은 스크립트를 검토하지만, 그 정책이 사용자가 의도한 것인지는 알 수 없습니다. 멀티시그 지갑을 만들 때 지갑에서 전체 정책(임계값, 모든 지문)을 보여 주세요.