Jason Dev
About
비트코인 스크립트 DeepDive (1)
published: 2024-02-18
btcdeb
btcc
p2pkh
p2sh
redeemScript

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

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

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

이번 시리즈에서는 P2PKH, P2SH에 대해 알아보자. 특히 P2SH에서 직접 코드를 작성하는 방법에 대해 자세히 알아본다.

이어지는 글에서는 P2TR에 대해 자세히 알아볼 예정이다.

사전 지식

btcdeb 사용해보기

우리가 비트코인 스크립트를 작성했을 때 이를 검증해볼 수단으로 btcdeb를 활용할 수 있다. btcdeb는 우리가 작성한 스크립트가 순차적으로 실행되는 과정을 시뮬레이션 해볼 수 있고, 매 과정마다 스택의 상태도 확인할 수 있다.

btcdeb 설치하기

설치 가이드를 따라하면서 btcdeb 프로그램을 설치해보자.

which 명령어를 통해 btcdeb가 제대로 설치되었는지 확인해보자.

which btcdeb
/usr/local/bin/btcdeb

btcdeb를 설치하면 btcc라는 프로그램도 같이 설치된다. btcc는 스크립트를 hex로 변환할 때 유용하다.

btcdeb 사용해보기

간단한 스크립트를 입력해서 btcdeb 동작을 확인해보자.

btcdeb '[5 2 OP_ADD 7 OP_SUB]'
btcdeb 5.0.24 -- type `btcdeb -h` for start up options
LOG: signing segwit taproot
notice: btcdeb has gotten quieter; use --verbose if necessary (this message is temporary)
warning: ambiguous input 5 is interpreted as a numeric value; use OP_5 to force into opcode
warning: ambiguous input 2 is interpreted as a numeric value; use OP_2 to force into opcode
warning: ambiguous input 7 is interpreted as a numeric value; use OP_7 to force into opcode
5 op script loaded. type `help` for usage information
script | stack
--------+--------
5 |
2 |
OP_ADD |
7 |
OP_SUB |
#0000 5
btcdeb> step
<> PUSH stack 05
script | stack
--------+--------
2 | 05
OP_ADD |
7 |
OP_SUB |
#0001 2
btcdeb>
<> PUSH stack 02
script | stack
--------+--------
OP_ADD | 02
7 | 05
OP_SUB |
#0002 OP_ADD
btcdeb>
<> POP stack
<> POP stack
<> PUSH stack 07
script | stack
--------+--------
7 | 07
OP_SUB |
#0003 7
btcdeb>
<> PUSH stack 07
script | stack
--------+--------
OP_SUB | 07
| 07
#0004 OP_SUB
btcdeb>
<> POP stack
<> POP stack
<> PUSH stack
script | stack
--------+--------
| 0x
btcdeb>

처음에 한 번만 step 명령어를 입력해주고 이후에는 Enter 키만 입력하면 된다.

step 명령어가 실행될 때마다 스크립트가 앞에서부터 하나씩 실행된다. 그 과정에서 변화하는 스택의 상황도 확인할 수 있다.

비트코인은 스크립트가 실행된 후 스택 꼭대기 값이 0이라면 실패로 간주한다. 따라서 앞의 예제 스크립트는 실패하는 스크립트가 된다.

실행 초기에 경고 메세지가 출력됐다. 이는 숫자를 표현할 때 10진수인지 16진수인지 헷갈려서 실수할 수 있기 때문이다. 실수를 방지하기 위해서는 다음과 같이 작성하는 게 좋다.

btcdeb '[OP_5 OP_2 OP_ADD OP_7 OP_SUB]'

P2PKH 스크립트 실행해보기

테스트를 위해 P2PKH 주소를 하나 생성하고 해당 주소로 비트코인을 보내자.

bc1 -named getnewaddress address_type=legacy
n1xBm7E3dTTN3HoTj2mcALjp3eU8g9awUY
bc1 sendtoaddress n1xBm7E3dTTN3HoTj2mcALjp3eU8g9awUY 0.001
e984e142c34704afecee21728b1fcf697ddf77e0eb0d07cd8d97ceeb12be86b2

address_type에 legacy를 입력하면 P2PKH 주소가 반환된다.

앞에서 생성된 트랜잭션의 정보를 확인해보자.

bc1 getrawtransaction e984e142c34704afecee21728b1fcf697ddf77e0eb0d07cd8d97ceeb12be86b2 true
{
"txid": "e984e142c34704afecee21728b1fcf697ddf77e0eb0d07cd8d97ceeb12be86b2",
"hash": "08d6508b791c007a33f3435d075acebb48391ddea31636cbc3888462adeb9b21",
"version": 2,
"size": 228,
"vsize": 147,
"weight": 585,
"locktime": 0,
"vin": [
{
"txid": "d61e352d078361fd5fbc12d03dd2d4f68b4c2be6bae0254b66289a08d57565b5",
"vout": 0,
"scriptSig": {
"asm": "",
"hex": ""
},
"txinwitness": [
"304402203a85d6d3051d35bbc5de01163d2b7f0dabb625c3e45920c6b46434427e511b1f02201d563403f767bd4e9169b22cd79b9eeb8bb8b81edac425e3b36fce1f6d695ff101",
"0278575d57afe6ee7bcb68718597855ed55de1cff5f29f9c668f85d8ed1b8ea2ea"
],
"sequence": 4294967293
}
],
"vout": [
{
"value": 0.89894240,
"n": 0,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 7599dd57de51b79e2990c71a0ba4e23caa509198 OP_EQUALVERIFY OP_CHECKSIG",
"desc": "addr(mrEmgCXVzW4SSW8om5yjymFfTpFHu3X9YR)#6khek5jj",
"hex": "76a9147599dd57de51b79e2990c71a0ba4e23caa50919888ac",
"address": "mrEmgCXVzW4SSW8om5yjymFfTpFHu3X9YR",
"type": "pubkeyhash"
}
},
{
"value": 0.00100000,
"n": 1,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 e02827ec23bb951f04217416399fbb63d8daaba6 OP_EQUALVERIFY OP_CHECKSIG",
"desc": "addr(n1xBm7E3dTTN3HoTj2mcALjp3eU8g9awUY)#69xeskrs",
"hex": "76a914e02827ec23bb951f04217416399fbb63d8daaba688ac",
"address": "n1xBm7E3dTTN3HoTj2mcALjp3eU8g9awUY",
"type": "pubkeyhash"
}
}
],
"hex": "02000000000101b56575d5089a28664b25e0bae62b4c8bf6d4d23dd012bc5ffd6183072d351ed60000000000fdffffff0260ad5b05000000001976a9147599dd57de51b79e2990c71a0ba4e23caa50919888aca0860100000000001976a914e02827ec23bb951f04217416399fbb63d8daaba688ac0247304402203a85d6d3051d35bbc5de01163d2b7f0dabb625c3e45920c6b46434427e511b1f02201d563403f767bd4e9169b22cd79b9eeb8bb8b81edac425e3b36fce1f6d695ff101210278575d57afe6ee7bcb68718597855ed55de1cff5f29f9c668f85d8ed1b8ea2ea00000000"
}

vout 두 번째 항목을 살펴보자. type 필드의 값 pubkeyhash을 통해 이 주소가 P2PKH라는 것을 알 수 있다. asm 필드를 통해 P2PKH 잠금 스크립트의 내용을 확인할 수 있다.

이제 해당 주소의 해제 스크립트를 확보해보자. 해제 스크립트는 해당 출력에 묶인 비트코인을 사용할 때 노출된다. vout 두 번째 항목을 사용하는 새로운 트랜잭션을 만들어보자.

새로운 트랜잭션의 출력에 사용할 주소를 생성하자.

bc1 getnewaddress
bcrt1q2jpf3sk7pst4eut3gdqjvxeexzdealxc7uasz0

이전 트랜잭션의 vout 두 번째 항목을 입력으로 사용하는 트랜잭션을 생성하고 서명한다.

bc1 -named createrawtransaction inputs='[{"txid":"e984e142c34704afecee21728b1fcf697ddf77e0eb0d07cd8d97ceeb12be86b2","vout":1}]' outputs='[{"bcrt1q2jpf3sk7pst4eut3gdqjvxeexzdealxc7uasz0":0.00995}]'
0200000001b286be12ebce978dcd070debe077df7d69cf1f8b7221eeecaf0447c342e184e90100000000fdffffff01b82e0f0000000000160014548298c2de0c175cf1714341261b39309b9efcd800000000
bc1 signrawtransactionwithwallet 0200000001b286be12ebce978dcd070debe077df7d69cf1f8b7221eeecaf0447c342e184e90100000000fdffffff01b82e0f0000000000160014548298c2de0c175cf1714341261b39309b9efcd800000000
{
"hex": "0200000001b286be12ebce978dcd070debe077df7d69cf1f8b7221eeecaf0447c342e184e9010000006a47304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb80da91542022074ab9f2483b240604c86a2702b25502a14ff57537f9066c9a3885f973a03f83801210359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926fdffffff01b82e0f0000000000160014548298c2de0c175cf1714341261b39309b9efcd800000000",
"complete": true
}

decoderawtransaction 명령어를 이용해서 서명된 트랜잭션의 내용을 살펴보자.

bc1 ; 0200000001b286be12ebce978dcd070debe077df7d69cf1f8b7221eeecaf0447c342e184e9010000006a47304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb80da91542022074ab9f2483b240604c86a2702b25502a14ff57537f9066c9a3885f973a03f83801210359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926fdffffff01b82e0f0000000000160014548298c2de0c175cf1714341261b39309b9efcd800000000
{
"txid": "c3bed081b4b0c75292e8df0d21b8514a9e72151c0c3b43cf54cfa5b5c09da7ae",
"hash": "c3bed081b4b0c75292e8df0d21b8514a9e72151c0c3b43cf54cfa5b5c09da7ae",
"version": 2,
"size": 188,
"vsize": 188,
"weight": 752,
"locktime": 0,
"vin": [
{
"txid": "e984e142c34704afecee21728b1fcf697ddf77e0eb0d07cd8d97ceeb12be86b2",
"vout": 1,
"scriptSig": {
"asm": "304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb80da91542022074ab9f2483b240604c86a2702b25502a14ff57537f9066c9a3885f973a03f838[ALL] 0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926",
"hex": "47304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb80da91542022074ab9f2483b240604c86a2702b25502a14ff57537f9066c9a3885f973a03f83801210359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926"
},
"sequence": 4294967293
}
],
"vout": [
{
"value": 0.00995000,
"n": 0,
"scriptPubKey": {
"asm": "0 548298c2de0c175cf1714341261b39309b9efcd8",
"desc": "addr(bcrt1q2jpf3sk7pst4eut3gdqjvxeexzdealxc7uasz0)#yyzazj0w",
"hex": "0014548298c2de0c175cf1714341261b39309b9efcd8",
"address": "bcrt1q2jpf3sk7pst4eut3gdqjvxeexzdealxc7uasz0",
"type": "witness_v0_keyhash"
}
}
]
}

vin 필드의 scriptSig.asm에서 해제 스크립트를 확인할 수 있다. [ALL] 문자열 왼쪽과 오른쪽으로 구분해서 살펴보자.

[ALL] 문자는 모든 입출력에 대해 서명했다는 의미이며 hex 코드로는 0x01을 의미한다. 따라서 왼쪽 부분에서 [ALL] 문자를 01로 변경하면 서명 데이터가 된다. 오른쪽은 공개키이다.

서명과 공개키를 쉘 변수에 저장하자.

sig=304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb80da91542022074ab9f2483b240604c86a2702b25502a14ff57537f9066c9a3885f973a03f83801
pubkey=0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926

이제 잠금/해제 스크립트를 모두 얻었다. 비트코인 노드는 검증을 위해 <sig> <pubkey> <scriptPubkey> 순으로 배열해서 실행한다.

btcdeb 프로그램을 이용해서 실행해보자.

btcdeb "[${sig} ${pubkey} OP_DUP OP_HASH160 e02827ec23bb951f04217416399fbb63d8daaba6 OP_EQUALVERIFY OP_CHECKSIG]"
btcdeb 5.0.24 -- type `btcdeb -h` for start up options
LOG: signing segwit taproot
notice: btcdeb has gotten quieter; use --verbose if necessary (this message is temporary)
7 op script loaded. type `help` for usage information
script | stack
-------------------------------------------------------------------+--------
304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb... |
0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926 |
OP_DUP |
OP_HASH160 |
e02827ec23bb951f04217416399fbb63d8daaba6 |
OP_EQUALVERIFY |
OP_CHECKSIG |
#0000 304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb80da91542022074ab9f2483b240604c86a2702b25502a14ff57537f9066c9a3885f973a03f83801
btcdeb> step
<> PUSH stack 304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb80da91542022074ab9f2483b240604c86a2702b25502a14ff57537f9066c9a3885f973a03f83801
script | stack
-------------------------------------------------------------------+-------------------------------------------------------------------
0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926 | 304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb...
OP_DUP |
OP_HASH160 |
e02827ec23bb951f04217416399fbb63d8daaba6 |
OP_EQUALVERIFY |
OP_CHECKSIG |
#0001 0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926
btcdeb>
<> PUSH stack 0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926
script | stack
-------------------------------------------------------------------+-------------------------------------------------------------------
OP_DUP | 0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926
OP_HASH160 | 304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb...
e02827ec23bb951f04217416399fbb63d8daaba6 |
OP_EQUALVERIFY |
OP_CHECKSIG |
#0002 OP_DUP
btcdeb>
<> PUSH stack 0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926
script | stack
-------------------------------------------------------------------+-------------------------------------------------------------------
OP_HASH160 | 0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926
e02827ec23bb951f04217416399fbb63d8daaba6 | 0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926
OP_EQUALVERIFY | 304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb...
OP_CHECKSIG |
#0003 OP_HASH160
btcdeb>
<> POP stack
<> PUSH stack e02827ec23bb951f04217416399fbb63d8daaba6
script | stack
-------------------------------------------------------------------+-------------------------------------------------------------------
e02827ec23bb951f04217416399fbb63d8daaba6 | e02827ec23bb951f04217416399fbb63d8daaba6
OP_EQUALVERIFY | 0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926
OP_CHECKSIG | 304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb...
#0004 e02827ec23bb951f04217416399fbb63d8daaba6
btcdeb>

OP_HASH160 연산까지 실행한 모습이다. 스택의 최상단에 있는 값과 스크립트 최상단에 있는 값이 같다. 따라서 이후 OP_EQUALVERIFY 연산을 통해 유효한 공개키라는 것이 확인될 것이다.

계속 실행해보자.

<> PUSH stack e02827ec23bb951f04217416399fbb63d8daaba6
script | stack
-------------------------------------------------------------------+-------------------------------------------------------------------
OP_EQUALVERIFY | e02827ec23bb951f04217416399fbb63d8daaba6
OP_CHECKSIG | e02827ec23bb951f04217416399fbb63d8daaba6
| 0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926
| 304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb...
#0005 OP_EQUALVERIFY
btcdeb>
<> POP stack
<> POP stack
<> PUSH stack 01
<> POP stack
script | stack
-------------------------------------------------------------------+-------------------------------------------------------------------
OP_CHECKSIG | 0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926
| 304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb...
#0006 OP_CHECKSIG
btcdeb>
EvalChecksig() sigversion=0
Eval Checksig Pre-Tapscript
error: Signature is found in scriptCode
btcdeb>
script | stack
-------------------------------------------------------------------+-------------------------------------------------------------------
| 0359a40a9aab447fb2af40ca9ddade7ad43efbd0562604737774e5cc77720e7926
| 304402207bce151b72a6f379fac0b09b7ba0ec6d0396b1c55c4e8c3831e03bb...
#0006 OP_CHECKSIG
btcdeb>
at end of script

예상대로 OP_EQUALVERIFY 연산을 잘 통과했다. 그런데 OP_CHECKSIG를 실행하는 과정에서 에러가 났다. btcdeb이 OP_CHECKSIG 연산에 대해서는 불안정한 상태인 것 같다.

이렇게 P2PKH는 정해진 코드 구조 안에 필요한 데이터만 채워 넣으면 된다. 따라서 우리가 새롭게 작성할 코드는 없다.

P2SH

P2SH에는 P2PKH처럼 몇 가지 정해진 코드 구조가 존재하지만 우리가 원하는 코드도 직접 넣을 수 있다. 이 번 글에서는 후자에 대해서 자세히 살펴보자.

redeemScript 만들기

P2SH에서 우리가 작성하는 스크립트를 redeemScript라고 부른다. P2SH 잠금 스크립트에는 redeemScript의 해시값이 들어가고 해제 스크립트에는 redeemScript의 실제 스크립트 내용이 들어간다.

간단한 redeemScript를 만들어보자.

OP_ADD OP_7 OP_SUB

매우 간단한 redeemScript를 만들었다. 앞에 두 개의 숫자 입력이 있다고 가정한다. 그리고 숫자의 합은 7이 아니어야 한다.

btcc 프로그램을 이용해서 스크립트를 hex로 변경하자.

btcc OP_ADD 7 OP_SUB
935794

decodescript 명령어를 이용해서 스크립트의 정보를 확인해보자.

bc1 decodescript 935794
{
"asm": "OP_ADD 7 OP_SUB",
"desc": "raw(935794)#ndstqtka",
"type": "nonstandard",
"p2sh": "2N9HrmafJeoq7kUhsvCWR3bD8oYTZxPoDJW",
"segwit": {
"asm": "0 26b295617a76fb142dabb61d161d0f6d64841a374ff5f95b913d378e7668b591",
"desc": "wsh(raw(935794))#7gr7jj09",
"hex": "002026b295617a76fb142dabb61d161d0f6d64841a374ff5f95b913d378e7668b591",
"address": "bcrt1qy6ef2ct6wma3gtdtkcw3v8g0d4jggx3hfl6ljku385mcuangkkgse55nw8",
"type": "witness_v0_scripthash",
"p2sh-segwit": "2N8bkCridMw3dvRz6GxYLtA7JfpyH5ZhbiT"
}
}

asm 필드를 통해 btcc 프로그램이 잘 동작했다는 것을 알 수 있다. 그리고 p2sh 필드에서 P2SH 주소를 확인할 수 있다.

잠금 스크립트 만들기

P2SH의 잠금 스크립트는 아래의 구조를 갖고 있다.

OP_HASH160 <hash160(redeemScript)> OP_EQUAL

hash160sha256rmd160을 순차적으로 실행하는 함수다.

openssl 프로그램을 이용해서 아래와 같이 redeemScript의 해시값을 얻을 수 있다.

echo -n 935794 | xxd -r -p | openssl dgst -sha256 -binary | openssl dgst -rmd160
RIPEMD-160(stdin)= b0018c351b3b4024ec6d5e485db772248bd93fba

이제 btcc 프로그램을 이용해서 잠금 스크립트의 hex값을 얻을 수 있다.

btcc OP_HASH160 b0018c351b3b4024ec6d5e485db772248bd93fba OP_EQUAL
a914b0018c351b3b4024ec6d5e485db772248bd93fba87

사실 잠금 스크립트를 얻기 위해서 앞의 과정이 필요한 것은 아니며 독자의 이해를 돕기 위한 과정이었다.

이전에 확보한 P2SH 주소를 getaddressinfo 명령어에 입력해보자.

bc1 getaddressinfo 2N9HrmafJeoq7kUhsvCWR3bD8oYTZxPoDJW
{
"address": "2N9HrmafJeoq7kUhsvCWR3bD8oYTZxPoDJW",
"scriptPubKey": "a914b0018c351b3b4024ec6d5e485db772248bd93fba87",
"ismine": false,
"solvable": false,
"iswatchonly": false,
"isscript": true,
"iswitness": false,
"ischange": false,
"labels": [
]
}

scriptPubKey 필드에서 잠금 스크립트를 얻을 수 있다.

P2SH 스크립트의 실행

P2SH 스크립트는 아래의 순서로 배열된다.

<code-1>...<code-n> <redeemScript> <scriptPubkey>

scriptPubkey 앞에 있는 코드는 모두 해제 스크립트에 있는 내용과 같다.

해제 스크립트가 아래와 같다고 해보자.

OP_5 OP_3 0x935794

그러면 P2SH 스크립트는 다음과 같이 결정된다.

OP_5 OP_3 0x935794 OP_HASH160 b0018c351b3b4024ec6d5e485db772248bd93fba OP_EQUAL

P2SH 스크립트는 두 번의 단계를 거쳐서 실행된다.

P2SH 실행 단계 1

첫 번째 단계에서는 스크립트의 유효성을 검증한다.

btcdeb 프로그램을 이용해서 첫 번째 단계를 실행해보자.

btcdeb '[OP_5 OP_3 0x935794 OP_HASH160 b0018c351b3b4024ec6d5e485db772248bd93fba OP_EQUAL]'
btcdeb 5.0.24 -- type `btcdeb -h` for start up options
LOG: signing segwit taproot
notice: btcdeb has gotten quieter; use --verbose if necessary (this message is temporary)
6 op script loaded. type `help` for usage information
script | stack
-----------------------------------------+--------
5 |
3 |
935794 |
OP_HASH160 |
b0018c351b3b4024ec6d5e485db772248bd93fba |
OP_EQUAL |
#0000 5
btcdeb> step
<> PUSH stack 05
script | stack
-----------------------------------------+--------
3 | 05
935794 |
OP_HASH160 |
b0018c351b3b4024ec6d5e485db772248bd93fba |
OP_EQUAL |
#0001 3
btcdeb>
<> PUSH stack 03
script | stack
-----------------------------------------+--------
935794 | 03
OP_HASH160 | 05
b0018c351b3b4024ec6d5e485db772248bd93fba |
OP_EQUAL |
#0002 935794
btcdeb>
<> PUSH stack 935794
script | stack
-----------------------------------------+--------
OP_HASH160 | 935794
b0018c351b3b4024ec6d5e485db772248bd93fba | 03
OP_EQUAL | 05
#0003 OP_HASH160
btcdeb>
<> POP stack
<> PUSH stack b0018c351b3b4024ec6d5e485db772248bd93fba
script | stack
-----------------------------------------+-----------------------------------------
b0018c351b3b4024ec6d5e485db772248bd93fba | b0018c351b3b4024ec6d5e485db772248bd93fba
OP_EQUAL | 03
| 05
#0004 b0018c351b3b4024ec6d5e485db772248bd93fba
btcdeb>
<> PUSH stack b0018c351b3b4024ec6d5e485db772248bd93fba
script | stack
-----------------------------------------+-----------------------------------------
OP_EQUAL | b0018c351b3b4024ec6d5e485db772248bd93fba
| b0018c351b3b4024ec6d5e485db772248bd93fba
| 03
| 05
#0005 OP_EQUAL
btcdeb>
<> POP stack
<> POP stack
<> PUSH stack 01
script | stack
-----------------------------------------+-----------------------------------------
| 01
| 03
| 05
btcdeb>

마지막 OP_EQUAL 연산 후 스택의 최상단 값이 0이 아니므로 유효한 스크립트라는 것이 검증됐다.

첫 번째 실행 단계를 통과하면 두 번째 실행 단계로 넘어간다.

P2SH 실행 단계 2

P2SH 스크립트의 두 번째 실행 단계에서는 해제 스크립트에서 hex로 되어 있는 redeemScript를 스크립트로 변환 후 그대로 실행한다.

btcdeb '[OP_5 OP_3 OP_ADD OP_7 OP_SUB]'
btcdeb 5.0.24 -- type `btcdeb -h` for start up options
LOG: signing segwit taproot
notice: btcdeb has gotten quieter; use --verbose if necessary (this message is temporary)
5 op script loaded. type `help` for usage information
script | stack
--------+--------
5 |
3 |
OP_ADD |
7 |
OP_SUB |
#0000 5
btcdeb> step
<> PUSH stack 05
script | stack
--------+--------
3 | 05
OP_ADD |
7 |
OP_SUB |
#0001 3
btcdeb>
<> PUSH stack 03
script | stack
--------+--------
OP_ADD | 03
7 | 05
OP_SUB |
#0002 OP_ADD
btcdeb>
<> POP stack
<> POP stack
<> PUSH stack 08
script | stack
--------+--------
7 | 08
OP_SUB |
#0003 7
btcdeb>
<> PUSH stack 07
script | stack
--------+--------
OP_SUB | 07
| 08
#0004 OP_SUB
btcdeb>
<> POP stack
<> POP stack
<> PUSH stack 01
script | stack
--------+--------
| 01
btcdeb>

스택의 최상단 값이 0이 아니므로 최종적으로 성공했다. 따라서 해당 트랜잭션은 유효하다고 판단한다.

P2SH 비트코인 사용하기

실제로 P2SH에 있는 비트코인을 사용하는 트랜잭션을 만들어보자.

아직은 비트코인이 없으므로 해당 주소로 비트코인을 보내주자.

bc1 sendtoaddress 2N9HrmafJeoq7kUhsvCWR3bD8oYTZxPoDJW 0.1
71664da7c73559220fcc5b7f72faffac01d8d790d10272d1f38810d3ab315263
bc1 -generate 1
{
"address": "bcrt1q3q73efk4z3mzhqu6umdrn9n7gzrfmg823gyc4a",
"blocks": [
"610ce16b0ea7db103bd3c5e93c99b42586e1ea12c37900d27c9e98d4e10e24f8"
]
}

P2SH 지갑 만들기

P2SH 주소의 UTXO 조회를 위한 지갑을 만들자.

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

편의를 위해 bc-sh 별칭을 만들었다.

importdescriptors 명령어를 사용할 예정이므로 P2SH 주소의 descriptor를 만들자. 정확한 이유는 모르겠지만 bc1 decodescript 935794를 실행했을 때 나왔던 descriptor 값은 잘 동작하지 않는다. 차선책으로 addr 함수를 이용하자. (참고 링크)

checksum 값이 필요하므로 getdescriptorinfo 명령어를 이용하자.

bc1 getdescriptorinfo "addr(2N9HrmafJeoq7kUhsvCWR3bD8oYTZxPoDJW)"
{
"descriptor": "addr(2N9HrmafJeoq7kUhsvCWR3bD8oYTZxPoDJW)#qzcyxn7m",
"checksum": "qzcyxn7m",
"isrange": false,
"issolvable": false,
"hasprivatekeys": false
}

descriptor 필드에서 checksum이 붙은 descriptor 정보를 확인할 수 있다.

importdescriptors 명령어를 이용해서 p2sh 지갑에 descriptor를 입력하자.

bc-sh importdescriptors '[{"desc": "addr(2N9HrmafJeoq7kUhsvCWR3bD8oYTZxPoDJW)#qzcyxn7m", "timestamp": 0}]'
[
{
"success": true
}
]
bc-sh listunspent
[
{
"txid": "71664da7c73559220fcc5b7f72faffac01d8d790d10272d1f38810d3ab315263",
"vout": 0,
"address": "2N9HrmafJeoq7kUhsvCWR3bD8oYTZxPoDJW",
"label": "",
"scriptPubKey": "a914b0018c351b3b4024ec6d5e485db772248bd93fba87",
"amount": 0.10000000,
"confirmations": 1,
"spendable": true,
"solvable": false,
"parent_descs": [
"addr(2N9HrmafJeoq7kUhsvCWR3bD8oYTZxPoDJW)#qzcyxn7m"
],
"safe": true
}
]

listunspent 명령어에서 UTXO 조회가 잘 되고있다.

P2SH에 있는 비트코인 보내기

우리가 만든 스크립트는 개인키가 필요 없다. 대신 스크립트 실행 후 스택 최상단이 0이 안되도록 해제 스크립트를 구성해주면 된다.

아래와 같은 간단한 해제 스크립트를 사용하자.

OP_5 OP_3 0x935794

두 숫자의 합이 7이 아니므로 적절한 해제 스크립트다.

createrawtransaction 명령어를 이용해서 임의의 주소로 0.05 비트코인을 보내는 트랜잭션을 생성하자.

bc1 createrawtransaction '[{"txid":"71664da7c73559220fcc5b7f72faffac01d8d790d10272d1f38810d3ab315263","vout":0}]' '{"bcrt1qj88s5vy3mulsp5qsqm5kdcydweckwvdrjtrz7w":0.05, "2N9HrmafJeoq7kUhsvCWR3bD8oYTZxPoDJW":0.04999}'
0200000001635231abd31088f3d17202d190d7d801acfffa727f5bcc0f225935c7a74d66710000000000fdffffff02404b4c000000000016001491cf0a3091df3f00d01006e966e08d76716731a358474c000000000017a914b0018c351b3b4024ec6d5e485db772248bd93fba8700000000

아쉽게도 bitcoin-cli에서는 해제 스크립트를 직접 입력할 수 있는 방법을 제공하지 않는다.

Raw 트랜잭션에 직접 해제 스크립트를 넣는 방법을 사용해보자. Raw 트랜잭션 파싱이 필요하므로 해당 내용을 읽어보자.

입력 데이터에서 sequence 직전에 해제 스크립트(scriptSig)가 온다는 점을 이용하자. Raw 트랜잭션에서 sequence의 위치를 먼저 파악하면 해제 스크립트의 위치도 파악할 수 있다.

decoderawtransaction 명령어를 이용해서 트랜잭션 내용을 살펴보자.

bc1 decoderawtransaction 0200000001635231abd31088f3d17202d190d7d801acfffa727f5bcc0f225935c7a74d66710000000000fdffffff02404b4c000000000016001491cf0a3091df3f00d01006e966e08d76716731a358474c000000000017a914b0018c351b3b4024ec6d5e485db772248bd93fba8700000000
{
...,
"vin": [
{
"txid": "71664da7c73559220fcc5b7f72faffac01d8d790d10272d1f38810d3ab315263",
"vout": 0,
"scriptSig": {
"asm": "",
"hex": ""
},
"sequence": 4294967293
}
],
"vout": [...]
}

sequence 필드의 값은 4294967293이고 16진수로는 fffffffd이다.

트랜잭션 각 필드의 값은 little-endian 방식(작은 단위의 바이트가 앞에 오는 방식)으로 표현된다. 따라서 sequence 필드의 값은 fdffffff이다. Raw 트랜잭션에서 해당 문자열은 한 곳 밖에 없으므로 sequence의 위치를 정확히 알 수 있다.

현재 트랜잭션에는 해제 스크립트가 없으므로 해제 스크립트 자리에는 데이터가 없음을 나타내는 00이 와야 한다. 따라서 해제 스크립트의 위치는 아래와 같다.

0200000001635231abd31088f3d17202d190d7d801acfffa727f5bcc0f225935c7a74d667100000000<scriptSig>fdffffff02404b4c000000000016001491cf0a3091df3f00d01006e966e08d76716731a358474c000000000017a914b0018c351b3b4024ec6d5e485db772248bd93fba8700000000

기존 00 문자열을 지우고 <scriptSig>로 표현했다.

btcc 프로그램을 이용해서 scriptSig hex값을 확인하자.

btcc OP_5 OP_3 0x935794
555303935794

Raw 트랜잭션에는 해당 hex의 크기를 표현해줘야 한다. 555303935794는 6 바이트이므로 앞에 06을 붙여준다.

06555303935794

이제 <scriptSig> 문자열을 scriptSig hex로 대체하면 트랜잭션이 완성된다.

sendrawtransaction 명령어를 이용해서 완성된 트랜잭션을 전송하자.

bc1 sendrawtransaction 0200000001635231abd31088f3d17202d190d7d801acfffa727f5bcc0f225935c7a74d66710000000006555303935794fdffffff02404b4c000000000016001491cf0a3091df3f00d01006e966e08d76716731a358474c000000000017a914b0018c351b3b4024ec6d5e485db772248bd93fba8700000000
0059921ce89ee82f7f264caf412342dfe514e998f95adc506d360c4d203ab108

블록을 채굴하고 P2SH 지갑의 UTXO를 확인해보자.

bc1 -generate 1
{
"address": "bcrt1qd0k2n8la04erypnwpy642hynsnmw0gwnpsdegg",
"blocks": [
"1ec90db6ab03f388c3b7048c2ae9cb086d90e75b92c4c579b307246d516896ac"
]
}
bc-sh listunspent
[
{
"txid": "0059921ce89ee82f7f264caf412342dfe514e998f95adc506d360c4d203ab108",
"vout": 1,
"address": "2N9HrmafJeoq7kUhsvCWR3bD8oYTZxPoDJW",
"label": "",
"scriptPubKey": "a914b0018c351b3b4024ec6d5e485db772248bd93fba87",
"amount": 0.04999000,
"confirmations": 1,
"spendable": true,
"solvable": false,
"parent_descs": [
"addr(2N9HrmafJeoq7kUhsvCWR3bD8oYTZxPoDJW)#qzcyxn7m"
],
"safe": true
}
]

사용된 비트코인을 제외하고 나머지 비트코인이 UTXO로 들어온 것을 확인할 수 있다.

P2WPKH, P2WSH, P2TR

P2WPKH, P2WSH의 해제 스크립트 내용은 P2PKH, P2SH와 같다. 다만 해제 스크립트가 저장되는 위치만 다를 뿐이다.

P2TR은 P2SH보다 훨씬 유연한 방식으로 스크립트 코드를 작성할 수 있다. P2TR에 대한 자세한 내용은 이어지는 글에서 다룬다.

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