Jason Dev
About
Bitcoin Transaction 이해하기 (3)
published: 2023-10-22
bitcoin
transaction
p2pkh
p2wpkh
p2tr

비트코인 스크립트

비트코인 트랜잭션의 입출력에는 스크립트가 포함되어 있는데 이 스크립트를 실행해서 유효성을 체크한다. 이를 통해 실제 비트코인 소유자만 자신의 코인을 전송할 수 있도록 한다.

입력에는 잠금 스크립트(scriptPubkey)가 있고 출력에는 해제 스크립트(scriptSig)가 있다. 이름에서 알 수 있듯이 일반적으로 잠금 스크립트에는 소유자의 공개키가 기록된다. 해제 스크립트에는 해당 공개키와 쌍을 이루는 개인키로 서명한 내용이 들어간다. 그래서 공개키로 해당 서명을 복호화 가능 여부를 검사해서 소유자가 맞는지 판별한다.

OPCODE

비트코인 스크립트는 스택을 기반으로 동작한다. 거의 모든 명령어가 스택에 데이터를 쓰거나 읽는 과정을 통해 동작한다.

비트코인 스크립트에서 사용할 수 있는 명령어를 opcode라고 부르며 위키에 잘 정리되어 있다. 종류별로 간단하게 살펴보자.

Constants

  • OP_0 or OP_FALSE: 스택에 0을 저장
  • OP_1 or OP_TRUE: 스택에 1을 저장
  • OP_PUSHDATA1 <size> <data>: 스택에 size 만큼의 데이터를 저장
  • 1~75 사이의 숫자: 숫자 그대로 스택에 저장
const script = "OP_FALSE";
const stack = [0];

const script = "OP_TRUE";
const stack = [1];

const script = "OP_PUSHDATA1 2 0x1234";
const stack = [0x1234];

const script = "3";
const stack = [3];

const script = "3 OP_TRUE";
const stack = [3, 1];

Flow control

  • OP_IF <code> OP_ENDIF
    • 스택 최상단 값을 제거하고 그 값이 0이 아니면 code를 실행한다.
  • OP_IF <code1> OP_ELSE <code2> OP_ENDIF
    • 스택 최상단 값을 제거하고 그 값이 0이 아니면 code1을 실행하고 그렇지 않다면 code2를 실행한다.
  • OP_VERIFY
    • 스택 최상단 값을 제거하고 그 값이 0이면 실패로 처리하고 그렇지 않다면 계속 진행한다.
  • OP_RETURN
    • 스택 최상단 값과 무관하게 실패로 처리한다.
const script = "1 OP_IF 2 OP_ENDIF 3";
const stack = [2, 3];

const script = "0 OP_IF 1 OP_ELSE 2 OP_ENDIF 3";
const stack = [2, 3];

const script = "0 OP_VERIFY 1";
const stack = []; // 실패

const script = "1 OP_VERIFY 2";
const stack = [2];

const script = "1 OP_RETURN";
const stack = [1]; // 실패

Stack

  • OP_DROP: 스택의 최상단 값을 제거한다
  • OP_DUP: 스택의 최상단 값을 복사해서 스택에 넣는다
  • OP_SWAP: 스택의 최상단 값과 바로 밑에 있는 값을 서로 바꾼다
const script = "1 2 OP_DROP";
const stack = [1];

const script = "1 2 OP_DUP";
const stack = [1, 2, 2];

const script = "1 2 OP_SWAP";
const stack = [2, 1];

Bitwise logic

  • OP_EQUAL: 스택의 최상단 두 값을 제거하고 두 값이 같으면 1을, 다르면 0을 넣는다.
  • OP_EQUALVERIFY: OP_EQUAL 연산 후 OP_VERIFY 연산을 수행한 것과 같다.
const script = "1 2 OP_EQUAL";
const stack = [0];

const script = "2 2 OP_EQUAL";
const stack = [1];

const script = "1 2 OP_EQUALVERIFY 3";
const stack = []; // 실패

const script = "2 2 OP_EQUALVERIFY 3";
const stack = [3];

const script = "2 2 OP_EQUAL OP_VERIFY 3";
const stack = [3];

Bitwise logic

  • OP_ADD: 스택의 최상단 두 값을 제거하고 서로 더한 값을 넣는다
  • OP_SUB: 스택의 최상단 두 값을 제거하고 서로 뺀 값을 넣는다
  • OP_MIN: 스택의 최상단 두 값을 제거하고 둘 중 작은 값을 넣는다
  • OP_MAX: 스택의 최상단 두 값을 제거하고 둘 중 큰 값을 넣는다
const script = "1 2 OP_ADD";
const stack = [3];

const script = "1 2 OP_ADD 3 OP_SUB";
const stack = [0];

const script = "1 2 OP_MIN";
const stack = [1];

const script = "1 2 OP_MAX";
const stack = [2];

Crypto

  • OP_RIPEMD160: 스택 최상단 값을 제거하고 그 값의 RIPEMD-160 해싱 결과를 넣는다.
  • OP_SHA256: 스택 최상단 값을 제거하고 그 값의 SHA-256 해싱 결과를 넣는다.
  • OP_HASH160: 스택 최상단 값을 제거하고 그 값에 SHA-256, RIPEMD-160을 차례로 해싱한 결과를 넣는다.
  • OP_HASH256: 스택 최상단 값을 제거하고 그 값에 SHA-256 해싱을 두 번 적용한 결과를 넣는다.
  • OP_CHECKSIG
    • 스택 최상단 값은 공개키, 그 다음 값은 서명으로 인식한다
    • 스택에서 읽은 값을 모두 지운다
    • 서명이 유효한지 검사한다. 유효하면 스택에 1을 넣고 그렇지 않다면 실패 처리한다
  • OP_CHECKMULTISIG
    • 스택 최상단 값을 서명 개수로 인식하고 그 수 만큼 서명과 공개키를 스택에서 읽는다.
    • 스택에서 읽은 값을 모두 지운다.
    • 모든 서명이 유효한지 검사한다. 유효하면 스택에 1을 넣고 그렇지 않다면 실패 처리한다.

스크립트 자유도

OPCODE를 얼마든지 자유롭게 조합해서 스크립트를 만들고 제출할 수 있다. 다만 다수의 비트코인 노드에서 표준 스크립트 유형(P2PK, P2PKH, P2SH, P2WPKH, P2WSH, P2TR 등)이 아닌 경우 거부될 수 있다. 또한 다음과 같은 추가 제한 사항이 있다.

  • 스크립트 크기 제한: 10kB 이하
  • 연산 개수 제한: 201 opcodes
  • 스택 요소 제한: 스택에 입력될 수 있는 아이템의 최대 크기는 520B

스크립트 성공 조건

다음과 같이 트랜잭션 출력의 해제 스크립트(scriptSig)와 트랜잭션 입력의 잠금 스크립트(scriptPubkey)를 차례대로 나열해서 하나의 스크립트로 만든다.

<scriptSig> <scriptPubkey>

특정 단계에서 실패 처리가 되지 않고 실행 완료 후 스택 최상단 값이 0이 아니면 성공이다. 성공 후 요청한 비트코인을 사용할 수 있다.

표준 스크립트 예제

P2PKH

P2PKH 스크립트의 해제 스크립트(scriptSig)와 잠금 스크립트(scriptPubkey) 구조는 다음과 같다

  • 해제 스크립트: <signature> <pubKey>
  • 잠금 스크립트: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
  • 합쳐진 모습: <signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

처음 두 개의 데이터는 스택에 추가되며 OP_DUP 실행 후 상태는 다음과 같다.

const script = "OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG";
const stack = [<signature>, <pubKey>, <pubKey>];

그 다음 <pubKeyHash>까지 실행 후 상태는 다음과 같다.

const script = "OP_EQUALVERIFY OP_CHECKSIG";
const stack = [<signature>, <pubKey>, RIPEMD160(SHA256(<pubKey>)), <pubKeyHash>];

OP_EQUALVERIFY는 입력과 출력의 공개키가 같은지 검사한다. 즉, 해당 비트코인 소유자의 공개키가 맞는지 검사한다.

잠금 스크립트에서 순수 공개키를 저장하지 않고 해싱된 공개키를 저장하는 이유는 다음과 같다.

  • 보안성: 공개키를 노출하지 않음으로써 공개키로부터 개인키를 계산하려는 시도 자체를 막을 수 있다.
  • 데이터 크기 절약: 공개키는 일반적으로 33 또는 65 바이트이다. 반면에, RIPEMD-160 해시값은 20 바이트이다.

OP_EQUALVERIFY 검사를 통과한 후 상태는 다음과 같다.

const script = "OP_CHECKSIG";
const stack = [<signature>, <pubKey>];

<signature>는 개인키로 서명한 내용이다. <pubKey>가 해당 서명의 공개키가 맞다면 <signature>를 복호화 할 수 있다. OP_CHECKSIG는 이러한 사실을 이용해서 해당 비트코인 소유자가 맞는지 검사한다.

P2WPKH

P2WPKH는 SegWit 업그레이드에 추가된 스크립트 유형이다. SegWit 업그레이드에 대한 자세한 내용은 이전 글에서 확인할 수 있다. P2WPKH 스크립트에서 프로그램이 검사하는 내용은 P2PKH와 다르지 않다. 다만 데이터 형식이 다를 뿐이다.

P2WPKH 스크립트의 해제 스크립트(scriptSig)와 잠금 스크립트(scriptPubkey) 구조는 다음과 같다

  • 해제 스크립트: empty
    • 트랜잭션 데이터에 없다는 의미이며 실제로는 다른 장소(Witness)에 <signature> <pubKey> 형태로 저장되어 있다.
  • 잠금 스크립트: OP_0 <pubKeyHash>

SegWit 업그레이드는 하위 호환성을 유지하면서 진행됐으므로 스크립트의 실행은 Non-SegWit 노드와 SegWit 노드 양쪽에 대해서 알아보자.

Non-SegWit 노드에서의 P2WPKH 스크립트 실행은 <pubKeyHash>가 스택의 최상위 값이 되면서 항상 참인 상태가 된다. 즉, 아무나 해당 비트코인을 사용할 수 있다. 그럼에도 이게 문제가 되지 않았던 이유는 다음과 같다.

Segwit은 95%의 Miner들이 Segwit으로 업그레이드 되었을 때 활성화 되었는데, 만약 누군가 Segwit 트랜잭션을 서명없이 Spend하였고 이것이 어쩌다 블럭에 들어갔다면, 95%의 Miner들이 이 블럭을 타당한 블럭으로 여기지 않을 것이고, 결국 이는 메인 블럭체인에서 멀어지는 Ophaned Chain으로 남게 될 것이다. (출처: cryptostudy)

SegWit 노드는 OP_0를 통해 이 스크립트가 P2WPKH라는 것을 안다. 참고로 OP_0는 SegWit 스크립트의 버전을 의미한다.

SegWit 노드는 Witness 데이터(<signature> <pubKey>)를 가져와서 P2PKH가 했던 검증 작업을 똑같이 수행한다. 이때는 우리가 위에서 살펴봤던 비트코인 스크립트가 실행되지 않고 노드 프로그램에서 자체적으로 검증 작업을 진행한다.

P2SH

P2PKH, P2WPKH 등은 공개키가 비트코인 주소 역할을 했다면 P2SH에서는 스크립트가 비트코인 주소 역할을 한다. (엄밀히 말하면 공개키와 스크립트 자체가 비트코인 주소는 아니다. 각 스크립트 유형별로 주소를 만드는 규칙이 따로 있다. 비트코인 주소는 사람들의 편의를 위한 개념일뿐 실제 트랜잭션에는 포함되지 않는다.)

P2SH의 주소 역할을 하는 스크립트를 리딤 스크립트(RedeemScript)라고 부른다. P2SH의 잠금 스크립트에는 리딤 스크립트의 실제 내용이 들어가지 않고 스크립트의 해시값이 들어간다. P2SH의 해제/잠금 스크립트 구조는 다음과 같다.

  • 해제 스크립트: <code-1> ... <code-n> <RedeemScript>
    • 앞부분에 원하는 코드를 넣을 수 있고 마지막에 리딤 스크립트가 들어간다
  • 잠금 스크립트: OP_HASH160 <ScriptHash> OP_EQUAL
    • ScriptHash는 리딤 스크립트의 해시값이다

예를 들어, 해제 스크립트가 다음과 같다고 해보자.

1 2 OP_ADD <2 OP_SUB>

스크립트의 초기 구성은 다음과 같다.

const script = "1 2 OP_ADD <2 OP_SUB> OP_HASH160 <ScriptHash> OP_EQUAL";
const stack = [];

OP_ADD 실행으로 스택에는 3이 들어가고, OP_HASH160에 의해 <2 OP_SUB> 해시값이 스택에 들어간다. 아래는 OP_EQUAL 직전까지 스크립트가 실행된 상태이다.

const script = "OP_EQUAL";
const stack = [3, RIPEMD160(SHA256(<2 OP_SUB>)), <ScriptHash>];

OP_EQUAL 검사를 통과하면 스택은 [3, 1] 상태로 정상 종료될 것이라고 예상할 수 있다. 다만 비트코인 노드는 P2SH 스크립트에서 리딤 스크립트가 유효(OP_EQUAL 통과)하다고 판단되면 다음과 같이 리딤 스크립트를 실행할 준비를 한다.

const script = "2 OP_SUB";
const stack = [3];

위 스크립트를 실행하면 스택에 1만 남고 요청한 비트코인이 정상적으로 사용된다.

P2SH 형식으로 비트코인을 사용한 실제 트랜잭션은 여기에서 확인할 수 있다. 그 내용은 아래와 같다.

const scriptSig = "3 5 4 OP_PUSHBYTES_11 6f93598893578893588851";
const redeemScript =
"OP_3DUP OP_ADD 9 OP_EQUALVERIFY OP_ADD 7 OP_EQUALVERIFY OP_ADD 8 OP_EQUALVERIFY 1";

개인키 같은 특별한 키 없이도 스크립트의 내용을 아는 사람이라면 언제든 비트코인을 사용할 수 있었다. 단순한 산술 연산으로만 이루어져 있는데 아마도 P2SH 학습을 메인넷에서 진행한 것 같다.

P2SH 스크립트의 대표적인 사용 예는 다중 서명(multisig)이다. n명이 하나의 주소로 비트코인을 공동으로 관리할 수 있다. 비트코인 사용 시 n명 중에서 (n보다 작은)m명만 서명하면 사용할 수 있도록 설계한 시스템이다.

예를 들어, 3명 중에서 2명만 승인해도 되는 다중 서명은 다음과 같이 스크립트를 구성할 수 있다.

  • 해제 스크립트(scriptSig): 0 <sig1> <sig2> <RedeemScript>
  • 리딤 스크립트: 2 <PubkeyA> <PubkeyB> <PubkeyC> 3 OP_CHECKMULTISIG

3명의 공개키를 기반으로 하나의 비트코인 주소를 구성한 셈이다.

P2TR

2021년에 탭루트(Taproot) 업그레이드를 통해 새롭게 추가된 스크립트 유형이다. 기존 스크립트 유형의 여러 단점을 보완했는데, 아래는 탭루트의 주요 장점이다.

  1. 개인정보 보호(privacy): 다양한 유형의 트랜잭션(단일 서명, 다중 서명 등)이 모두 동일해 보이므로 타인이 특정 유형의 트랜잭션을 구분하기 어렵다.
  2. 데이터 효율성 증가: 스크립트의 모든 내용을 공개하지 않아도 되므로 트랜잭션 크기를 줄일 수 있다. (낮은 수수료로 연결됨)
  3. 유연성: 여러 가지 스크립트를 준비할 수 있고 비트코인을 사용할 때 필요한 스크립트만 사용할 수 있다.

장점 3번 덕분에 매우 다양한 경우를 처리할 수 있는데, 예를 들어 A, B, C 세 사람이 다음과 같은 조건으로 비트코인을 관리할 수 있다.

  1. A, B 모두 동의할 경우
  2. 특정 시간(ex. 30일) 경과 후 A가 동의할 경우
  3. C가 동의할 경우

P2TR 방식으로 비트코인을 사용할 때는 key path와 script path 중에 하나를 선택할 수 있다. key path는 단일 서명처럼 특별한 로직이 필요없는 경우에 사용할 수 있다. script path는 위에 다중 서명 예제처럼 로직이 들어가는 경우에 사용할 수 있다.

P2TR의 스크립트 구성은 다음과 같다.

  • 해제 스크립트: empty
    • 트랜잭션 데이터에 없다는 의미이며 실제로는 Witness에 저장되어 있다.
  • 잠금 스크립트: OP_1 <pubKey>

P2TR은 SegWit v1이라서 잠금 스크립트는 OP_1로 시작한다. 참고로 SegWit v0인 P2WPKH, P2WSH는 OP_0으로 시작한다.

P2TR의 Witness 데이터 구조에 대해서는 여기를 참고하자.

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