비트코인은 프로그래밍이 가능한 돈이다. 비트코인을 소비할 수 있는 다양한 조건을 코드로 표현할 수 있다.
코드는 잠금/해제 스크립트(또는 Witness)에 포함되며 opcode라고 불리는 명령어들의 조합으로 구성된다.
P2PKH 같은 스크립트 유형은 정해진 명령어 조합 안에서만 사용할 수 있지만 P2SH, P2TR 등은 허용된 용량 안에서 자유롭게 코드를 작성할 수 있다.
이번 시리즈에서는 P2PKH, P2SH에 대해 알아보자. 특히 P2SH에서 직접 코드를 작성하는 방법에 대해 자세히 알아본다.
이어지는 글에서는 P2TR에 대해 자세히 알아볼 예정이다.
우리가 비트코인 스크립트를 작성했을 때 이를 검증해볼 수단으로 btcdeb를 활용할 수 있다. btcdeb는 우리가 작성한 스크립트가 순차적으로 실행되는 과정을 시뮬레이션 해볼 수 있고, 매 과정마다 스택의 상태도 확인할 수 있다.
설치 가이드를 따라하면서 btcdeb 프로그램을 설치해보자.
which 명령어를 통해 btcdeb가 제대로 설치되었는지 확인해보자.
btcdeb를 설치하면 btcc라는 프로그램도 같이 설치된다. btcc는 스크립트를 hex로 변환할 때 유용하다.
간단한 스크립트를 입력해서 btcdeb 동작을 확인해보자.
처음에 한 번만 step 명령어를 입력해주고 이후에는 Enter 키만 입력하면 된다.
step 명령어가 실행될 때마다 스크립트가 앞에서부터 하나씩 실행된다. 그 과정에서 변화하는 스택의 상황도 확인할 수 있다.
비트코인은 스크립트가 실행된 후 스택 꼭대기 값이 0이라면 실패로 간주한다. 따라서 앞의 예제 스크립트는 실패하는 스크립트가 된다.
실행 초기에 경고 메세지가 출력됐다. 이는 숫자를 표현할 때 10진수인지 16진수인지 헷갈려서 실수할 수 있기 때문이다. 실수를 방지하기 위해서는 다음과 같이 작성하는 게 좋다.
테스트를 위해 P2PKH 주소를 하나 생성하고 해당 주소로 비트코인을 보내자.
address_type에 legacy를 입력하면 P2PKH 주소가 반환된다.
앞에서 생성된 트랜잭션의 정보를 확인해보자.
vout 두 번째 항목을 살펴보자. type 필드의 값 pubkeyhash
을 통해 이 주소가 P2PKH라는 것을 알 수 있다. asm 필드를 통해 P2PKH 잠금 스크립트의 내용을 확인할 수 있다.
이제 해당 주소의 해제 스크립트를 확보해보자. 해제 스크립트는 해당 출력에 묶인 비트코인을 사용할 때 노출된다. vout 두 번째 항목을 사용하는 새로운 트랜잭션을 만들어보자.
새로운 트랜잭션의 출력에 사용할 주소를 생성하자.
이전 트랜잭션의 vout 두 번째 항목을 입력으로 사용하는 트랜잭션을 생성하고 서명한다.
decoderawtransaction 명령어를 이용해서 서명된 트랜잭션의 내용을 살펴보자.
vin 필드의 scriptSig.asm
에서 해제 스크립트를 확인할 수 있다. [ALL]
문자열 왼쪽과 오른쪽으로 구분해서 살펴보자.
[ALL]
문자는 모든 입출력에 대해 서명했다는 의미이며 hex 코드로는 0x01을 의미한다. 따라서 왼쪽 부분에서 [ALL]
문자를 01
로 변경하면 서명 데이터가 된다. 오른쪽은 공개키이다.
서명과 공개키를 쉘 변수에 저장하자.
이제 잠금/해제 스크립트를 모두 얻었다. 비트코인 노드는 검증을 위해 <sig> <pubkey> <scriptPubkey>
순으로 배열해서 실행한다.
btcdeb 프로그램을 이용해서 실행해보자.
OP_HASH160 연산까지 실행한 모습이다. 스택의 최상단에 있는 값과 스크립트 최상단에 있는 값이 같다. 따라서 이후 OP_EQUALVERIFY 연산을 통해 유효한 공개키라는 것이 확인될 것이다.
계속 실행해보자.
예상대로 OP_EQUALVERIFY 연산을 잘 통과했다. 그런데 OP_CHECKSIG를 실행하는 과정에서 에러가 났다. btcdeb이 OP_CHECKSIG 연산에 대해서는 불안정한 상태인 것 같다.
이렇게 P2PKH는 정해진 코드 구조 안에 필요한 데이터만 채워 넣으면 된다. 따라서 우리가 새롭게 작성할 코드는 없다.
P2SH에는 P2PKH처럼 몇 가지 정해진 코드 구조가 존재하지만 우리가 원하는 코드도 직접 넣을 수 있다. 이 번 글에서는 후자에 대해서 자세히 살펴보자.
P2SH에서 우리가 작성하는 스크립트를 redeemScript라고 부른다. P2SH 잠금 스크립트에는 redeemScript의 해시값이 들어가고 해제 스크립트에는 redeemScript의 실제 스크립트 내용이 들어간다.
간단한 redeemScript를 만들어보자.
매우 간단한 redeemScript를 만들었다. 앞에 두 개의 숫자 입력이 있다고 가정한다. 그리고 숫자의 합은 7이 아니어야 한다.
btcc 프로그램을 이용해서 스크립트를 hex로 변경하자.
decodescript 명령어를 이용해서 스크립트의 정보를 확인해보자.
asm 필드를 통해 btcc 프로그램이 잘 동작했다는 것을 알 수 있다. 그리고 p2sh 필드에서 P2SH 주소를 확인할 수 있다.
P2SH의 잠금 스크립트는 아래의 구조를 갖고 있다.
hash160
은 sha256
와 rmd160
을 순차적으로 실행하는 함수다.
openssl 프로그램을 이용해서 아래와 같이 redeemScript의 해시값을 얻을 수 있다.
이제 btcc 프로그램을 이용해서 잠금 스크립트의 hex값을 얻을 수 있다.
사실 잠금 스크립트를 얻기 위해서 앞의 과정이 필요한 것은 아니며 독자의 이해를 돕기 위한 과정이었다.
이전에 확보한 P2SH 주소를 getaddressinfo 명령어에 입력해보자.
scriptPubKey 필드에서 잠금 스크립트를 얻을 수 있다.
P2SH 스크립트는 아래의 순서로 배열된다.
scriptPubkey 앞에 있는 코드는 모두 해제 스크립트에 있는 내용과 같다.
해제 스크립트가 아래와 같다고 해보자.
그러면 P2SH 스크립트는 다음과 같이 결정된다.
P2SH 스크립트는 두 번의 단계를 거쳐서 실행된다.
첫 번째 단계에서는 스크립트의 유효성을 검증한다.
btcdeb 프로그램을 이용해서 첫 번째 단계를 실행해보자.
마지막 OP_EQUAL 연산 후 스택의 최상단 값이 0이 아니므로 유효한 스크립트라는 것이 검증됐다.
첫 번째 실행 단계를 통과하면 두 번째 실행 단계로 넘어간다.
P2SH 스크립트의 두 번째 실행 단계에서는 해제 스크립트에서 hex로 되어 있는 redeemScript를 스크립트로 변환 후 그대로 실행한다.
스택의 최상단 값이 0이 아니므로 최종적으로 성공했다. 따라서 해당 트랜잭션은 유효하다고 판단한다.
실제로 P2SH에 있는 비트코인을 사용하는 트랜잭션을 만들어보자.
아직은 비트코인이 없으므로 해당 주소로 비트코인을 보내주자.
P2SH 주소의 UTXO 조회를 위한 지갑을 만들자.
편의를 위해 bc-sh
별칭을 만들었다.
importdescriptors 명령어를 사용할 예정이므로 P2SH 주소의 descriptor를 만들자. 정확한 이유는 모르겠지만 bc1 decodescript 935794
를 실행했을 때 나왔던 descriptor 값은 잘 동작하지 않는다. 차선책으로 addr 함수를 이용하자. (참고 링크)
checksum 값이 필요하므로 getdescriptorinfo 명령어를 이용하자.
descriptor 필드에서 checksum이 붙은 descriptor 정보를 확인할 수 있다.
importdescriptors 명령어를 이용해서 p2sh
지갑에 descriptor를 입력하자.
listunspent 명령어에서 UTXO 조회가 잘 되고있다.
우리가 만든 스크립트는 개인키가 필요 없다. 대신 스크립트 실행 후 스택 최상단이 0이 안되도록 해제 스크립트를 구성해주면 된다.
아래와 같은 간단한 해제 스크립트를 사용하자.
두 숫자의 합이 7이 아니므로 적절한 해제 스크립트다.
createrawtransaction 명령어를 이용해서 임의의 주소로 0.05 비트코인을 보내는 트랜잭션을 생성하자.
아쉽게도 bitcoin-cli에서는 해제 스크립트를 직접 입력할 수 있는 방법을 제공하지 않는다.
Raw 트랜잭션에 직접 해제 스크립트를 넣는 방법을 사용해보자. Raw 트랜잭션 파싱이 필요하므로 해당 내용을 읽어보자.
입력 데이터에서 sequence 직전에 해제 스크립트(scriptSig)가 온다는 점을 이용하자. Raw 트랜잭션에서 sequence의 위치를 먼저 파악하면 해제 스크립트의 위치도 파악할 수 있다.
decoderawtransaction 명령어를 이용해서 트랜잭션 내용을 살펴보자.
sequence 필드의 값은 4294967293
이고 16진수로는 fffffffd
이다.
트랜잭션 각 필드의 값은 little-endian 방식(작은 단위의 바이트가 앞에 오는 방식)으로 표현된다. 따라서 sequence 필드의 값은 fdffffff
이다. Raw 트랜잭션에서 해당 문자열은 한 곳 밖에 없으므로 sequence의 위치를 정확히 알 수 있다.
현재 트랜잭션에는 해제 스크립트가 없으므로 해제 스크립트 자리에는 데이터가 없음을 나타내는 00
이 와야 한다. 따라서 해제 스크립트의 위치는 아래와 같다.
기존 00
문자열을 지우고 <scriptSig>
로 표현했다.
btcc 프로그램을 이용해서 scriptSig hex값을 확인하자.
Raw 트랜잭션에는 해당 hex의 크기를 표현해줘야 한다. 555303935794
는 6 바이트이므로 앞에 06
을 붙여준다.
이제 <scriptSig>
문자열을 scriptSig hex로 대체하면 트랜잭션이 완성된다.
sendrawtransaction 명령어를 이용해서 완성된 트랜잭션을 전송하자.
블록을 채굴하고 P2SH 지갑의 UTXO를 확인해보자.
사용된 비트코인을 제외하고 나머지 비트코인이 UTXO로 들어온 것을 확인할 수 있다.
P2WPKH, P2WSH의 해제 스크립트 내용은 P2PKH, P2SH와 같다. 다만 해제 스크립트가 저장되는 위치만 다를 뿐이다.
P2TR은 P2SH보다 훨씬 유연한 방식으로 스크립트 코드를 작성할 수 있다. P2TR에 대한 자세한 내용은 이어지는 글에서 다룬다.