Jason Dev
About
비트코인 스크립트 DeepDive (2)
published: 2024-02-25
p2tr
bitcoinjs-lib
control block
witness
psbt

비트코인은 프로그래밍이 가능한 돈이다. 비트코인을 소비할 수 있는 다양한 조건을 코드로 표현할 수 있다.

코드는 잠금/해제 스크립트(또는 Witness)에 포함되며 opcode라고 불리는 명령어들의 조합으로 구성된다.

P2PKH 같은 스크립트 유형은 정해진 명령어 조합 안에서만 사용할 수 있지만 P2SH, P2TR 등은 허용된 용량 안에서 자유롭게 코드를 작성할 수 있다.

이전 글에서는 P2PKH, P2SH에 대해 알아봤다. 이번 글에서는 P2TR에서 직접 스크립트를 작성하는 방법에 대해 알아보자.

사전 지식

프로젝트 환경 구축하기

이번 글에서는 bitcoinjs-lib를 활용한다. 아래 명령어를 실행해서 프로젝트 환경을 구축하자.

mkdir p2tr
cd p2tr
npm init -y
npm install @types/node bitcoinjs-lib ecpair tiny-secp256k1 varuint-bitcoin

typescript 코드 실행을 위해 bun을 이용한다. bun 사이트에서 설치 방법을 따라하자.

개인키 생성하기

P2TR 주소 생성을 위한 개인키를 만들자. 32 바이트 길이의 문자열이 필요한데, 직접 아무 값이나 입력해도 된다.

혹은 아래와 같이 코드를 이용해서 생성할 수도 있다.

import { initEccLib, networks } from "bitcoinjs-lib";
import { ECPairFactory } from "ecpair";
import * as tinysecp from "tiny-secp256k1";

initEccLib(tinysecp);
const ECPair = ECPairFactory(tinysecp);
const network = networks.regtest;

const keypair = ECPair.makeRandom({ network });
console.log(keypair.privateKey?.toString("hex"));

공개키/개인키 생성을 위해 ECPair 객체를 만들어서 사용했다.

코드 실행 결과는 다음과 같다.

bun index1.ts
1f6de67794fe0b09d2e18b3fb3bee41c2465360eddbc368c8de256618b0c4c72

P2TR 주소 생성하기

앞에서 생성한 개인키를 이용해서 P2TR 주소를 생성해보자.

직접 작성하는 스크립트 없이 키 경로만 사용하는 P2TR 주소를 만들 수도 있다. 이번 글에서는 스트립트 경로를 사용하는 P2TR에 대해서 다룬다.

P2TR 스크립트 준비하기

P2TR에서 사용할 두 개의 스크립트를 준비하자.

import { initEccLib, networks, payments, script } from "bitcoinjs-lib";
import { toXOnly } from "bitcoinjs-lib/src/psbt/bip371";
import { Taptree } from "bitcoinjs-lib/src/types";
import { ECPairFactory } from "ecpair";
import * as tinysecp from "tiny-secp256k1";

initEccLib(tinysecp);
const ECPair = ECPairFactory(tinysecp);
const network = networks.regtest;

const privateKey =
"1f6de67794fe0b09d2e18b3fb3bee41c2465360eddbc368c8de256618b0c4c72";
const keypair = ECPair.fromPrivateKey(Buffer.from(privateKey, "hex"), {
network,
});
const scriptAsm1 = `${toXOnly(keypair.publicKey).toString("hex")} OP_CHECKSIG`;
const scriptAsm2 = `OP_ADD OP_5 OP_EQUAL`;
const scriptBuffer1 = script.fromASM(scriptAsm1);
const scriptBuffer2 = script.fromASM(scriptAsm2);

첫 번째 스크립트(scriptAsm1)는 공개키를 이용한 일반적인 잠금 스크립트 형식이다. 두 번째 스크립트(scriptAsm2)는 더해서 5가 되는 두 숫자를 입력해야 풀리는 잠금 스크립트다. 두 스크립트 중에 원하는 하나의 스크립트를 선택해서 소비할 수 있는 P2TR 주소를 만들 예정이다.

여기서 toXOnly 함수가 다소 생소하다.

공개키는 33 바이트지만 Taproot에서 사용되는 공개키(internalPubkey)는 x 좌표만 포함하는 32 바이트다. 나머지 1 바이트는 y 좌표를 의미하는데 Taproot에서는 사용하지 않으므로 제거한다.

bitcoinjs-lib에서 toXOnly 함수의 구현은 아래와 같다.

const toXOnly = (pubKey) =>
pubKey.length === 32 ? pubKey : pubKey.slice(1, 33);

스크립트 트리와 함께 주소 생성하기

스크립트가 하나가 아니라면 여러 스크립트를 트리 형태로 구성해야 한다. 우리는 두 개의 리프(leaf) 노드를 갖는 트리로 구성해보자.

const scriptTree: Taptree = [
{ output: scriptBuffer1 },
{ output: scriptBuffer2 },
];

const p2tr = payments.p2tr({
internalPubkey: toXOnly(keypair.publicKey),
scriptTree,
network,
});

console.log(p2tr.address);

p2tr 객체를 만들 때 입력한 값이 실제로 P2TR 주소 생성을 위해 필요한 모든 값이다. 즉, P2TR 주소는 공개키와 스크립트로 생성할 수 있다.

코드를 실행해보자.

bun index2.ts
bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h

P2TR 주소로 비트코인 보내기

bitcoin-cli를 이용해서 앞에서 생성한 P2TR 주소로 비트코인을 보내자.

bc1 sendtoaddress bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h 0.1
0d2bd63bda12d22fb1edf33a0223faa787d236af61b627ccfd8004f2be234d78
bc1 -generate 1
{
"address": "bcrt1qcnaq3dscmjvtzdmtnyraxrmdpek54pzqqten80",
"blocks": [
"361b6e49b561e03441254d63e847fc494eaa0640cc293ba5bf022ebefa1e1345"
]
}

bitcoin-cli에서 P2TR 주소의 UTXO를 확인할 수 있도록 지갑을 생성하자.

bc1 -named createwallet wallet_name="p2tr" disable_private_keys=true blank=true
alias bc-tr="bitcoin-cli -rpcuser=w1 -rpcpassword=w1 -rpcport=1111 -rpcwallet=p2tr"

입력 편의를 위해 bc-tr 별칭을 만들었다.

생성한 지갑으로 P2TR 주소의 descriptor를 입력한다. 이때 checksum이 포함된 descriptor를 얻기 위해 getdescriptorinfo 명령어를 이용한다.

bc-tr getdescriptorinfo "addr(bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h)"
{
"descriptor": "addr(bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h)#t7wlhrr4",
"checksum": "t7wlhrr4",
"isrange": false,
"issolvable": false,
"hasprivatekeys": false
}
bc-tr importdescriptors '[{"desc": "addr(bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h)#t7wlhrr4", "timestamp": 0}]'

listunspent 명령어로 UTXO를 확인해보자.

bc-tr listunspent
[
{
"txid": "64d8a2ab7ca706ac759736829956195c44c69e73581cf827190ad86f6d9812a1",
"vout": 0,
"address": "bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h",
"label": "",
"scriptPubKey": "5120c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa5",
"amount": 0.10000000,
"confirmations": 1,
"spendable": true,
"solvable": true,
"desc": "rawtr(c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa5)#50gsyz4h",
"parent_descs": [
"addr(bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h)#t7wlhrr4"
],
"safe": true
}
]

0.1 비트코인이 잘 들어온 것을 확인할 수 있다.

P2TR 비트코인 사용하기

P2TR 비트코인을 사용하기 위해서는 준비된 스크립트 중에 하나를 선택해야 한다. 먼저 두 번째 스크립트인 OP_ADD OP_5 OP_EQUAL를 이용해보자.

import { LEAF_VERSION_TAPSCRIPT } from "bitcoinjs-lib/src/payments/bip341";

const redeem = {
output: scriptBuffer2,
redeemVersion: LEAF_VERSION_TAPSCRIPT,
};
const p2tr = payments.p2tr({
internalPubkey: toXOnly(keypair.publicKey),
scriptTree,
network,
redeem,
});

redeem 객체의 output에 우리가 선택한 두 번째 스크립트를 입력했다.

redeemVersion에는 BIP 표준에서 정의된 버전 정보를 입력한다. 현재는 초기 단계라서 별다른 옵션이 없고 LEAF_VERSION_TAPSCRIPT(0xC0)를 입력하면 된다. 0xC0tapscript spend를 의미한다.

p2tr 객체를 생성할 때 이전과는 다르게 redeem 정보를 입력했다. P2TR에서 스크립트 경로로 비트코인을 사용하기 위해서는 control block이라는 것이 필요한데, redeem 정보를 이용해서 control block 값을 얻을 수 있다.

const scriptPubkey = p2tr.output;
const controlBlock = p2tr.witness[p2tr.witness.length - 1];

p2tr.output는 잠금 스크립트다.

witness 데이터의 마지막에는 항상 control block이 들어간다는 사실을 이용해서 controlBlock 변수의 값을 입력했다.

현재 p2tr.witness는 아래 내용을 담고 있다.

[
[147, 85, 135],
[
192, 165, 188, 228, 188, 64, 142, 218, 30, 40, 201, 114, 61, 155, 150, 209,
173, 94, 120, 127, 2, 37, 112, 127, 93, 226, 30, 4, 118, 87, 121, 32, 2,
219, 40, 210, 1, 204, 138, 213, 26, 118, 211, 1, 156, 233, 178, 171, 167,
153, 172, 96, 247, 49, 216, 246, 254, 131, 122, 89, 124, 55, 255, 21, 43,
],
];

p2tr.witness[0]는 우리가 사용하는 두 번째 스크립트의 내용이다. p2tr.witness[1]는 앞에서 언급한대로 control block이다.

control block은 다음의 내용을 포함한다.

  • 1 바이트 redeemVersion (LEAF_VERSION_TAPSCRIPT = 0xC0 = 192)
  • 32 바이트 공개키(internalPubkey)
  • 32*m 바이트 경로
    • 루트로 올라가는 모든 경로의 해시값 (각 32 바이트)
    • 여기서는 루트까지 한 단계만 필요해서 32 바이트만 추가됐다
    • 만약 사용하려는 리프 노드가 루트까지 4 만큼 떨어져 있다면 32*4 바이트가 된다

우리 코드에서는 65(1+32+32) 바이트 크기의 control block이 만들어진다.

트랜잭션 생성하기

트랜잭션 생성을 위해서 PSBT를 이용한다. PSBT 객체를 만들고 트랜잭션의 입출력 값을 입력하자.

입력에는 앞에서 확인했던 UTXO 정보를 입력하자.

출력에는 다른 사람의 주소와 우리의 P2TR 주소를 각각 입력하자.

const psbt = new Psbt({ network });
const txid = "64d8a2ab7ca706ac759736829956195c44c69e73581cf827190ad86f6d9812a1";
const vout = 0;
const utxoValue = 1000e4;
const fee = 1000;
psbt.addInput({
hash: txid,
index: vout,
witnessUtxo: { value: utxoValue, script: scriptPubkey },
});

psbt.addOutput({
address: "bcrt1qxh0kt0ue3tgk93u8z02elmfgkx9m57rlwahd30",
value: (utxoValue - fee) / 2,
});
psbt.addOutput({
address: "bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h",
value: (utxoValue - fee) / 2,
});

witnessUtxo는 서명을 위해 필요한 정보다.

아직 psbt 객체는 서명이 안된 상태다. finalizeInput 메서드를 이용해서 서명을 해보자.

const customFinalizer = () => {
try {
const witness = [
Buffer.from([2]),
Buffer.from([3]),
redeem.output,
controlBlock,
];
return { finalScriptWitness: witnessStackToScriptWitness(witness) };
} catch (err) {
throw new Error(`Can not finalize taproot input: ${err}`);
}
};

psbt.finalizeInput(0, customFinalizer);

보통 witness 배열 앞쪽에는 서명 데이터가 들어간다. 우리가 사용하는 스크립트는 더해서 5가 되는 두 개의 숫자가 필요하므로 2와 3을 입력했다. 이어서 우리가 사용하는 스크립트의 내용과 control block을 입력했다. 앞에서 언급한대로 control block은 마지막에 입력해야 한다.

witnessStackToScriptWitness 함수는 witness 배열을 트랜잭션 형식에 맞게 변환해준다. witnessStackToScriptWitness 가 반환하는 Buffer에는 아래 값이 들어있다.

04010201030393558741c0a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e047657792002db28d201cc8ad51a76d3019ce9b2aba799ac60f731d8f6fe837a597c37ff152b

앞에 04는 4개의 데이터가 있다는 것을 의미하며 각 데이터는 아래와 같이 구분할 수 있다.

04
0102
0103
03935587
41c0a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e047657792002db28d201cc8ad51a76d3019ce9b2aba799ac60f731d8f6fe837a597c37ff152b
  • 첫 번째 데이터는 0102 이다. 앞에 01은 1 바이트 데이터가 있다는 것을 의미한다.
  • 두 번째 데이터는 0103 이다.
  • 세 번째 데이터는 03935587 이다. 03은 3 바이트 데이터가 있다는 것을 의미한다. 이것은 redeem.output 를 의미한다.
  • 네 번째 데이터는 controlBlock 이다. 41은 65 바이트 데이터가 있다는 것을 의미한다.

서명이 완료되었으므로 raw 트랜잭션 데이터를 추출하자.

const tx = psbt.extractTransaction();
console.log(tx.toHex());

코드를 실행해보자.

bun index3.ts
02000000000101a112986d6fd80a1927f81c58739ec6445c19569982369775ac06a77caba2d8640000000000ffffffff024c494c000000000016001435df65bf998ad162c78713d59fed28b18bba787f4c494c0000000000225120c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa504010201030393558741c0a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e047657792002db28d201cc8ad51a76d3019ce9b2aba799ac60f731d8f6fe837a597c37ff152b00000000

트랜잭션 전송하기

decoderawtransaction 명령어를 이용해서 트랜잭션 데이터를 검증해보자.

bc1 decoderawtransaction 02000000000101a112986d6fd80a1927f81c58739ec6445c19569982369775ac06a77caba2d8640000000000ffffffff024c494c000000000016001435df65bf998ad162c78713d59fed28b18bba787f4c494c0000000000225120c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa504010201030393558741c0a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e047657792002db28d201cc8ad51a76d3019ce9b2aba799ac60f731d8f6fe837a597c37ff152b00000000
{
"txid": "1faaf4b5f879c31c7a63d66a18e720f857e2fd60a6d42f79e93ffbaf4f01429d",
"hash": "355bd867cd4dd8e9af0de3ceb0818a31de75e1aaf9e9581ed6d8457f4b35b28e",
"version": 2,
"size": 202,
"vsize": 145,
"weight": 577,
"locktime": 0,
"vin": [
{
"txid": "64d8a2ab7ca706ac759736829956195c44c69e73581cf827190ad86f6d9812a1",
"vout": 0,
"scriptSig": {
"asm": "",
"hex": ""
},
"txinwitness": [
"02",
"03",
"935587",
"c0a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e047657792002db28d201cc8ad51a76d3019ce9b2aba799ac60f731d8f6fe837a597c37ff152b"
],
"sequence": 4294967295
}
],
"vout": [
{
"value": 0.04999500,
"n": 0,
"scriptPubKey": {
"asm": "0 35df65bf998ad162c78713d59fed28b18bba787f",
"desc": "addr(bcrt1qxh0kt0ue3tgk93u8z02elmfgkx9m57rlwahd30)#x62gacsz",
"hex": "001435df65bf998ad162c78713d59fed28b18bba787f",
"address": "bcrt1qxh0kt0ue3tgk93u8z02elmfgkx9m57rlwahd30",
"type": "witness_v0_keyhash"
}
},
{
"value": 0.04999500,
"n": 1,
"scriptPubKey": {
"asm": "1 c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa5",
"desc": "rawtr(c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa5)#50gsyz4h",
"hex": "5120c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa5",
"address": "bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h",
"type": "witness_v1_taproot"
}
}
]
}

사용하는 UTXO는 P2TR 주소이므로 해제 스크립트는 비어있고 대신 txinwitness 필드에 필요한 값이 채워져있다. 우리가 입력한 숫자 2와 3이 보인다.

출력 데이터도 우리가 입력한 대로 잘 들어가 있다.

생성된 트랜잭션을 전송하자.

bc1 sendrawtransaction 02000000000101a112986d6fd80a1927f81c58739ec6445c19569982369775ac06a77caba2d8640000000000ffffffff024c494c000000000016001435df65bf998ad162c78713d59fed28b18bba787f4c494c0000000000225120c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa504010201030393558741c0a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e047657792002db28d201cc8ad51a76d3019ce9b2aba799ac60f731d8f6fe837a597c37ff152b00000000
1faaf4b5f879c31c7a63d66a18e720f857e2fd60a6d42f79e93ffbaf4f01429d
bc1 -generate 1

UTXO를 확인해보자.

bc-tr listunspent
[
{
"txid": "1faaf4b5f879c31c7a63d66a18e720f857e2fd60a6d42f79e93ffbaf4f01429d",
"vout": 1,
"address": "bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h",
"label": "",
"scriptPubKey": "5120c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa5",
"amount": 0.04999500,
"confirmations": 1,
"spendable": true,
"solvable": true,
"desc": "rawtr(c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa5)#50gsyz4h",
"parent_descs": [
"addr(bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h)#t7wlhrr4"
],
"safe": true
}
]

0.1 비트코인에서 수수료를 제외한 나머지 절반이 잘 들어왔다.

OP_CHECKSIG 경로 사용하기

이번에는 OP_CHECKSIG를 사용하는 첫 번째 스크립트를 이용해보자.

첫 번째 스크립트는 두 번째 스크립트와는 다르게 공개키를 이용한 일반적인 잠금 스크립트를 이용하고 있다. 따라서 트랜잭션을 서명하는 과정이 이전 방식보다 훨씬 간단하다.

이전과 같이 P2TR 주소로 0.1 비트코인을 보내주자.

bc1 sendtoaddress bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h 0.1
9d22bc466124b755ad70420f2d84e1da3e1a914ba3bc76c553ef6d83a8c50e3c
bc1 -generate 1

UTXO 정보를 확인해보자.

bc-tr listunspent
[
{
"txid": "9d22bc466124b755ad70420f2d84e1da3e1a914ba3bc76c553ef6d83a8c50e3c",
"vout": 0,
"address": "bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h",
"label": "",
"scriptPubKey": "5120c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa5",
"amount": 0.10000000,
"confirmations": 1,
"spendable": true,
"solvable": true,
"desc": "rawtr(c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa5)#50gsyz4h",
"parent_descs": [
"addr(bcrt1pcmd8kqe47vcwvf43saanma4dv5tuz2xwywwnc3884zwy7ehql2jsemj47h)#t7wlhrr4"
],
"safe": true
},
...
]

앞에서 확인한 txid, vout값을 코드에 입력하자.

기존에 작성한 redeem 객체를 조금 수정해서 첫 번째 스크립트를 사용하도록 하자.

const redeem = {
output: scriptBuffer1,
redeemVersion: LEAF_VERSION_TAPSCRIPT,
};

트랜잭션 입력을 구성하는 코드도 추가 작업이 필요하다.

psbt.addInput({
hash: txid,
index: vout,
witnessUtxo: { value: utxoValue, script: scriptPubkey },
tapLeafScript: [
{
leafVersion: redeem.redeemVersion,
script: redeem.output,
controlBlock,
},
],
});

tapLeafScript 속성을 추가로 입력했다. 이전에는 수동으로 witness 데이터를 생성했지만 이제 tapLeafScript 덕분에 witness 데이터를 자동으로 생성할 수 있다. 이는 첫 번째 스크립트가 일반적인 잠금 스크립트 형태이기 때문이다.

이전에는 customFinalizer 함수를 작성했지만 이제는 필요 없다.

psbt.signInput(0, keypair);
psbt.finalizeAllInputs();

이렇게 서명이 완료됐다. 코드를 실행해보자.

bun index4.ts
020000000001013c0ec5a8836def53c576bca34b911a3edae1842d0f4270ad55b7246146bc229d0000000000ffffffff024c494c000000000016001435df65bf998ad162c78713d59fed28b18bba787f4c494c0000000000225120c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa503405b455041451940f57de89ebd96207672fca7cd04b7b4e9370bd202c8891eb27efc6c0265ca993143cdea3acefb38150704a51128e4d6954c3e2284ecfb6d357c2220a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e047657792002ac41c0a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e0476577920021eabacf84860c25ff473eb6c8c38fd52059c318c93a645e487eebaca5d7cf30a00000000

생성된 raw 트랜잭션에서 witness 데이터 부분은 아래와 같다.

03405b455041451940f57de89ebd96207672fca7cd04b7b4e9370bd202c8891eb27efc6c0265ca993143cdea3acefb38150704a51128e4d6954c3e2284ecfb6d357c2220a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e047657792002ac41c0a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e0476577920021eabacf84860c25ff473eb6c8c38fd52059c318c93a645e487eebaca5d7cf30a

그리고 이를 각 부분별로 구분해서 확인해보자.

03
405b455041451940f57de89ebd96207672fca7cd04b7b4e9370bd202c8891eb27efc6c0265ca993143cdea3acefb38150704a51128e4d6954c3e2284ecfb6d357c
2220a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e047657792002ac
41c0a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e0476577920021eabacf84860c25ff473eb6c8c38fd52059c318c93a645e487eebaca5d7cf30a
  • 03은 3개의 데이터가 있다는 것을 의미한다.
  • 40은 64 바이트 데이터가 있다는 것을 의미한다. 이것은 서명 데이터다.
  • 22는 34 바이트 데이터가 있다는 것을 의미한다. 이것은 우리가 사용하는 첫 번째 스크립트의 내용이다.
  • 41은 65 바이트 데이터가 있다는 것을 의미한다. 이것은 control block이다.

이제 트랜잭션을 전송해보자.

bc1 sendrawtransaction 020000000001013c0ec5a8836def53c576bca34b911a3edae1842d0f4270ad55b7246146bc229d0000000000ffffffff024c494c000000000016001435df65bf998ad162c78713d59fed28b18bba787f4c494c0000000000225120c6da7b0335f330e626b1877b3df6ad6517c128ce239d3c44e7a89c4f66e0faa503405b455041451940f57de89ebd96207672fca7cd04b7b4e9370bd202c8891eb27efc6c0265ca993143cdea3acefb38150704a51128e4d6954c3e2284ecfb6d357c2220a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e047657792002ac41c0a5bce4bc408eda1e28c9723d9b96d1ad5e787f0225707f5de21e0476577920021eabacf84860c25ff473eb6c8c38fd52059c318c93a645e487eebaca5d7cf30a00000000
98b822c7255f17c70501c34bcc694cd69795b4ac1f15c38cc146ff511e50988e

트랜잭션 ID가 출력됐으므로 서명이 잘 된 트랜잭션이라는 것을 알 수 있다.

2-depth tree

조금 더 복잡한 구조로 스크립트를 구성해보고 control block이 어떻게 달라지는지 확인해보자.

2-depth tree를 구성하기 위해 아래처럼 3개의 스크립트를 만들자.

const scriptAsm1 = `OP_ADD OP_2 OP_EQUAL`;
const scriptAsm2 = `OP_ADD OP_3 OP_EQUAL`;
const scriptAsm3 = `OP_ADD OP_4 OP_EQUAL`;
const scriptBuffer1 = script.fromASM(scriptAsm1);
const scriptBuffer2 = script.fromASM(scriptAsm2);
const scriptBuffer3 = script.fromASM(scriptAsm3);

const scriptTree: Taptree = [
{
output: scriptBuffer1,
},
[
{
output: scriptBuffer2,
},
{
output: scriptBuffer3,
},
],
];

세 번째 스크립트를 사용하는 control block을 출력하자.

const redeem = {
output: scriptBuffer3,
redeemVersion: LEAF_VERSION_TAPSCRIPT,
};
const p2tr = payments.p2tr({
internalPubkey: toXOnly(keypair.publicKey),
scriptTree,
network,
redeem,
});

const controlBlock = p2tr.witness[p2tr.witness.length - 1];
console.log(controlBlock.toString("hex").length / 2);

코드를 실행해보자.

bun index5.ts
97

이전 control block의 크기는 65 바이트였는데, 32 바이트가 늘어났다. 이는 1 depth가 늘어난 결과다.

지금까지 작성한 코드는 여기에서 확인할 수 있다.

댓글 삭제 시 메일 주소가 필요합니다