[Bitcoin] - ch6. 고급 거래와 스크립팅

‍허진·2023년 3월 6일
0

Blockchain

목록 보기
7/19
post-thumbnail

본 글은 '비트코인, 공개 블록체인 프로그래밍(Andreas M. Antonopoulos 저, 최은실, 김도훈, 송주한 옮김, 2018)'을 바탕으로 작성되었습니다.

> 다중서명

다중서명은 N개의 공개키가 스크립트 내에 기록되어 있고, 예상지출 상태를 풀기 위해서는 이 중 적어도 M개 이상의 개인키가 서명을 제공해야 한다는 조건을 설정한다. M-of-N 제도로도 알려져있다.

M-of-N 다중서명 조건을 설정하는 잠금 스크립트의 일반적인 유형은 다음과 같다.

M <Public Key 1> <Public Key 2> ... <Public Key N> N CHECKMULTISIG

위 식에서 N은 등록된 공개키의 총 개수이고, M은 출력값을 소비하기 위해 필요한 서명의 기준 값이다.

2-of-3 다중서명 조건을 설정하는 잠금 스크립트는 다음과 같은 유형이다.

2 <Public Key A> <Public Key B> <Public Key C> 3 CHECKMULTISIG

위 잠금 스크립트는 서명과 공개키 쌍이 담겨 있는 해제 스크립트로 조건을 만족시킬 수 있다.

<Signature B> <Signature C>

또는 등록된 공개키 3개에 대응하는 개인키 중 2개의 서명으로 조건을 충족할 수 있다.

이 두 가지 스크립트를 사용하여 다음과 같은 복합 유효성 검사 스크립트를 생성할 수 있다.

<Signature B> <Signature C> 2 <Public Key A> <Public Key B> <Public Key C> 3 CHECKMULTISIG

복합 스크립트가 일단 시행되면 잠금 스크립트가 설정한 조건을 해제 스크립트가 만족할 때에만 TRUE 판정을 내릴 것이다. 이 경우 설정 조건은 예상 지출로 설정되어 있는 3개의 공개키 중에서 2개의 공개키에 대응하는 2개의 개인키로부터 나온 유효 서명을 해제 스크립트가 보유하고 있는지 여부다.

> CHECKMULTISIG 실행 시 버그

CHECKMULTISIG가 실행되면 스택 위에 있는 M+N+2개 항목을 변수로 소비해야 한다. 하지만 버그 때문에 CHECKMULTISIG가 추가값 혹은 예상보다 하나 많은 값을 내놓는다. 이 추가 항목은 서명을 체크할 때 무시되기 때문에 CHECKMULTISIG 자체에는 직접적인 영향을 끼치지 않는다. 하지만 이 추가 값은 제시되어야 한다. CHECKMULTISIG가 빈 스택을 올리려고 하기 때문에 스택 에러가 발생하고 스크립트 실패(script failure)가 발생한다(거래가 유효하지 않다고 기록).

따라서 추가 항목을 설정하는데, 이는 무시되기 때문에 어떤 값이라도 될 수 있지만 관례적으로는 0이 사용된다.이에 따른 올바른 유효성 검사 스크립트가 아래와 같이 제시된다.

0 <Signature B> <Signature C> 2 <Public Key A> <Public Key B> <Public Key C> 3 CHECKMULTISIG

> Pay-to-Script-Hash(P2SH)

Pay-to-script-hash(P2SH)는 2012년에 도입된 복잡한 거래 스크립트 사용을 매우 단순화시킨 새로운 유형의 강력한 거래 방식이다. 다중서명 방식 하에서는 암호를 해제하기 위해서 적어도 2개의 서명이 필요한 방식으로 소비자들이 지불한 금액이 잠겨 있다. 이러한 방식을 통해 기업지배구조를 통제할 수 있게 되고 절도나 횡령, 분실로부터의 자금을 보호할 수 있다.

다중서명 스크립트가 효과적인 기능을 가지고 있긴 하지만 사용하기가 번거롭다. 송신자는 다중 서명을 위해 거래 스크립트를 생성할 수 있는 특수 비트코인 지갑 소프트웨어를 사용해야 하고, 구매 스크립트를 사용하는 거래를 이해해야 한다. 또한, 결과값으로 나온 거래의 크기가 단순 지불 거래보다 크므로 수수료 부담이 생긴다. 마지막으로 이러한 대규모 거래 스크립트는 소비될 때까지 풀 노드 내의 RAM 속에 있는 UTXO 세트에 보관되야 하므로 저장소를 잡아먹게 된다.

P2SH는 위와 같은 실질적인 어려움을 해결하고 비트코인 주소에 지불하는 것만큼 쉽게 복잡한 스크립트를 사용할 수 있게 개발되었다. P2SH의 경우, 복잡한 잠금 스크립가 암호 해시인 디지털 지문으로 대체된다. UTXO를 소비하려고 시도하는 거래가 제시되면 해당 거래는 반드시 잠금 스크립트와 함께 해시가 일치하는 스크립트를 담고 있어야 한다. 여기서 말한 잠금 스크립트는 리딤 스크립트(redeem script)라고 불린다.

1) P2SH가 없는 스크립트

  • 잠금 스크립트 : 2 PubKey1 PubKey2 PubKey3 PubKey4 PubKey5 5 CHECKMULTISIG
  • 해제 스크립트 : 0 Sig1 Sig2

2) P2SH로 인코딩된 스크립트

  • 리딤 스크립트 : 2 PubKey1 PubKey2 PubKey3 PubKey4 PubKey5 5 CHECKMULTISIG
  • 잠금 스크립트 : HASH160 <20-byte hash of redeem script> EQUAL
  • 해제 스크립트 : 0 Sig1 Sig2 <redeem script>

P2SH 거래의 잠금 스크립트는 다음과 같다.

HASH160 54c557e07dde5bb6cb791c7a540e0a4796f5e97e EQUAL

따라서 송신자는 이렇게 짧아진 잠금 스크립트만 지불 거래에 포함시키면 된다. 수신자가 해당 UTXO를 소비하려고 할 때 송신자는 최초의 리딤 스크립트(해시가 UTXO를 잠그고 있는 스크립트)와 이 스크립트를 해제하기 위한 서명을 다음과 같이 제공해야 한다.

<Sig1> <Sig2> <2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG>

이제 UTXO의 소비 과정을 살펴보자. 먼저 스크립트가 두 단계에 걸쳐 결합된다. 우선 리딤 스크립트가 해시와 일치하는지 확인하기 위해 리딤 스크립트와 잠금 스크립트를 비교해 본다.

<2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG> HASH160 <redeem scriptHash> EQUAL

리딤 스크립트 해시가 일치하는 경우, 해제 스크립트는 리딤 스크립트를 해제하기 위해 스스로 실행된다.

<Sig1> <Sig2> 2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG

이렇게 P2SH 거래의 소비가 완료되는 것을 확인할 수 있다.

> P2SH의 이점

Pay-to-Script-Hash 기능은 잠금 출력값에 있는 복잡한 스크립트를 직접 사용하는 경우와 비교했을 때 다음과 같은 이점을 제공한다.

  • 복잡한 스크립트는 거래 출력값에서 더 짧은 지문으로 대체되어 해당 거래의 크기를 줄인다.
  • 스크립트는 주소로 코딩될 수 있다. 따라서 송신자와 그 지갑은 P2SH를 실행하기 위해 복잡한 작업이 필요하지 않다.
  • P2SH는 스크립트를 만드는 부담을 송신자가 아니라 수취인에게 부담시킨다.
  • P2SH는 출력값(출력값은 UTXO 세트 내에 들어 있기 때문에 메모리에 영향을 끼침)이 가지고 있던 데이터 저장에 대한 부담을 입력값(입력값은 오직 블록체인에서만 저장)으로 옮긴다.
  • P2SH는 긴 스크립트를 저장해야 하는 부담을 현재(지불하는 시점)에서 미래(소비되는 시점)으로 미룬다.
  • P2SH는 긴 스크립트의 거래 수수료 비용 부담을 송신자에서 수취인으로 옮긴다. 수취인은 거래의 출력값을 소비하기 위해서는 길이가 긴 리딤 스크립트를 거래의 입력값에 포함시켜야 한다.

> 데이터 레코딩 출력(RETURN)

분산화되고 시간 정보가 기록되어 있는 비트코인 장부인 블록체인은 지불 수단 이외에도 잠재적 용도를 여러 개 가지고 있다. 많은 개발자들은 디지털공증서비스, 주권(stock certificate), 스마트 계약 등의 어플리케이션을 위해 거래 스크립팅 언어를 사용해 보고자 했다.
하지만 비트코인 지불과 관련이 없는 데이터를 저장하기 위해 블록체인을 사용하는 것은 논란의 여지가 있는 주제다. 비지불 데이터(non-payment)가 블록체인에 포함되는 것을 반대하는 사람들은 비지불 데이터로 인해서 '블록체인 팽창'이 일어나 원래 계획에는 없던 데이터가 블록체인에 보관되면서 풀 비트코인 노드 운영자들에게 디스크 보관비를 부담시킨다고 주장한다. 뿐만 아니라 이러한 종류의 거래는 목적지 비트코인 주소의 20바이트 공간을 자유로운 목적으로 사용함으로써 소비될 수 없는 UTXO를 생성한다. 목적지 비트코인 주소는 데이터용으로 사용되기 때문에 개인키에 대응하지 않아서 결과값으로 산출된 UTXO는 절대 소비될 수 없다. 따라서 절대 소비될 수 없는 이 거래는 UTXO 세트에서 삭제될 수 없고 이에 따라 영원히 UTXO 데이터베이스의 크기가 '팽창'한다.

비트코인 코어 클라이언트 버전 0.9에서는 RETURN 연산자를 도입하는 합의에 도달했다. RETURN을 통해 개발자들은 80바이트 크기의 비지불 데이터를 거래 출력값에 추가할 수 있게 되었다. 그러나 '가짜' UTXO를 사용할 때와는 달리 RETURN 연산자는 명백하게 지출될 수 없음이 입증되는 출력값을 만들기 위해 해당 출력값은 UTXO 세트에 저장될 필요가 없다. RETURN 출력값은 블록체인에 기록되므로 디스크 공간을 소모하여 블록체인의 크기가 커지도록 만든다. 하지만 이 출력값은 UTXO 세트에는 저장되지 않기 때문에 UTXO 메모리 풀을 확장시키지도 않고 풀 노드에게 값비싼 RAM 비용 부담을 안기지도 않는다.

RETURN 출력값을 '소비'할 수 있는 '해제 스크립트'는 없다. RETURN 연산자는 보통 비트코인 금액이 0인 출력값이다. RETURN이 스크립트 유효화 소프트웨어를 접하게 되면 즉시 유효화 스크립트 실행을 중단하고 거래가 유효하지 않다고 기록한다. RETURN이 실행되면 기본적으로 스크립트가 FALSE를 "RETURN"하고 정지한다. 따라서 사용자가 우연히 RETURN의 출력값을 거래의 입력값으로 참조했다면 해당 거래는 유효하지 않다.

> 타임락(Timelocks)

타임락은 거래 혹은 출력값이 어떤 시점 이후에만 소비될 수 있도록 하는 제한이다. 비트코인은 초기부터 거래 차원의 타임락 기능을 가지고 있었으며, 타임락은 거래 내의 nLocktime 필드에 의해 구현된다. 추가로 UTXO 차원의 타임락을 제안하는 두 개의 새로운 타임락 기능이 도입되었다. 타임락은 거래를 나중에 일어나게 하고 미래의 특정 시점까지 자금을 잠그는 데 유용하다. 더욱 중요한 사실은 타임락이 비트코인 스크립팅을 시간 차원까지 확장해서 복잡한 여러 단계의 스마트 컨트랙트(smart contract)의 시대를 열게 되었다.

> 거래 잠금 시간(nLocktime)

거래 잠금시간이란 거래가 유효화되고 네트워크상에 전송되거나 블록체인 상에 추가될 수 잇는 가장 빠른 시간을 규정하는 거래 차원의 설정(거래 데이터 구조 내의 필드)이다. 바로 실행 킻 전파가 가능한 상태임을 알려 주기 위해 잠금시간은 대부분의 거래에서 0으로 설정되어 있다. nLocktime이 0이 아니거나 5억 미만이라면 잠금시간은 블록의 높이로 해석된다. 즉, 블록 높이가 명시된 높이가 되기 전까지 거래는 유효화되지 않고 중개되거나 블록체인에 포함되지 않는다. 잠금 시간이 5억 이상인 경우 유닉스 기준일자 타임스탬프(Unix Epoch timestamp, 1970년 1월 1일부터 몇 초가 지났는지를 나타내는 정수)로 해석되며, 명시된 시간이 되기 전에는 거래가 유효화되지 않는다.

nLocktime에서는 다음을 알고 가야한다. 만약 Alice가 그녀의 출력값 중 하나를 Bob의 주소에 소비하는 거래에 서명하고 nLocktime을 3달 후로 설정했다고 하자. 이 거래에서,

  • Bob은 3개월이 지날 때까지 해당 금액을 교환하기 위해 거래를 전송할 수 없다.
  • Bob은 3개월이 지난 후에 해당 거래를 전송할 수 있을 것이다.

그런데,

  • Alice는 또 다른 거래 한 건을 생성해서 잠금시간 없이 동일한 입력값을 이중지불할 수 있다. 따라서 Alice는 3개월이 지나기 전에 동일한 UTXO를 소비할 수 있다.
  • Bob에게 Alice가 위에서 언급할 일을 하지 않을 것이라고 보장되지 않는다.

nLocktime의 제한에서 유일하게 보장할 수 잇는 사실은 Bob이 기간이 지나기 전까지는 돈을 교환할 수 없다는 것이다. Bob이 돈을 얻게 될 것이라는 그 어떤 보장도 없다. 해당 금액을 얻을 수 있다는 보장을 받기 위해서는 잠금시간 제한이 거래상에 있기보다는 UTXO에 걸려 있어서 잠금 스크립트의 일부가 되어야 한다.

> Check Lock Time Verify(CLTV)

CLTV는 nLocktime의 경우와는 다르게 거래당 잠금시간이 아닌 출력값당 잠금시간이다. 간단하게 말하자면 출력값의 리딤 스크립트 내의 CLTV 연산코드를 추가함으로써 출력값을 제한할 수 있고, 이를 통해 특정 시간이 지난 후에만 출력값이 소비될 수 있다. CLTV는 nLocktime을 대체하기보다 특정 UTXO가 같거나 더 길게 nLocktime이 걸려 있는 향후 거래에서만 소비될 수 있게끔 해당 UTXO를 제한한다.

CLTV 연산코드는 입력값으로 한 개의 변수를 가지며, 이 변수는 nLocktime과 동일한 포맷으로, 숫자로 표현된다. VERIFY의 접미부에도 나와 있듯이 CLTV는 결과값이 FALSE일 때는 스크립트의 실행을 중단하고 결과가 TRUE이면 실행이 계속된다.

Alice가 Bob의 주소에 지불한다면 해당 출력값은 보통 아래와 같은 P2PKH 스크립트를 포함하고 있게 될 것이다.

DUP HASH160 <Bob's Public Key Hash> EQUALVERIFY CHECKSIG

출력값을 시간으로 잠그기 위해 해당 거래는 아래와 같이 리딤 스크립트가 있는 P2SH 거래가 될 것이다.

<now + 3 months> CHECKLOCKTIMEVERIFY DROP DUP HASH160 <Bob's Public Key Hash> EQUALVERIFY CHECKSIG

Bob이 이 UTXO를 소비하려고 할 때 해당 UTXO를 입력값으로 참조하는 거래를 구성한다. Bob은 서명과 공개키를 입력값의 해제 스크립트에 사용해서 CHECKLOCKTIMEVERIFY에서 Alice가 설정해 놓은 잠금시간과 동일하거나 조금 더 많게 거래 잠금시간을 설정한다. 그 후 Bob은 비트코인 네트워크 상에서 해당 거래를 전파한다.

Bob의 거래는 Alice가 설정한 CHECKLOCKTIMEVERIFY 변수가 소비할 거래의 잠금시간 이하일 경우 스크립트 실행이 계속된다. 그렇지 않은 경우(잠금시간 이상일 경우)에는 스크립트 실행이 중단되고 해당 거래가 유효하지 않다고 간주된다.

CLTV와 nLocktime을 함께 사용함으로써 Alice가 UTXO 자체를 잠갔기 때문에 Alice나 Bob 중 한 사람이라도 3개월이라는 잠금시간이 만료되기 전까지는 해당 금액을 소비할 수 없다.

> 상대적 잠금시간

nLocktime과 CLTV는 모두 절대적인 한 시점을 명시한다는 점에서 절대적 잠금시간이다. 이제 알아볼 두 개의 잠금시간 기능들은 출력값을 소비하는 조건으로 블록체인 내의 출력값 승인 이후 지나간 시간을 명시한다는 점에서 상대적 잠금시간이라고 할 수 있다.

상대적 잠금시간을 통해 두 개 이상의 독립적인 거래로 구성되어 잇는 체인이 상호 의존적인 거래들을 유예하는 동시에 이전 거래의 승인으로부터 경과 시간에 의존하고 있는 거래에는 시간 제한을 둘 수 있기 때문에 상대적 잠금시간은 유용하다. 즉, UTXO가 블록체인 상에 기록되기 전까지는 시계가 경과시간을 재지 않는다.

절대적 잠금시간과 마찬가지로 상대적 잠금시간도 거래 차원의 기능과 스크립트 차원의 연산코드로 구현된다. 거래 차원의 상대적 잠금시간은 모든 거래 입력값 내에 설정되어 있는 거래 필드인 nSequence의 값에 대한 합의 규칙으로 구현된다. 스크립트 차원의 상대적 잠금시간은 CHECKSEQUENCEVERIFY(CSV) 연산코드를 사용해서 구현된다.

> nSequence를 사용한 상대적 잠금시간

nSequence 값이 2^31 미만인 거래 입력값들은 상대적 잠금시간을 가지고 있다고 해석된다. 이 거래는 입력값이 상대적 잠금시간만큼 시간이 흘러야 유효화된다. 예를 들어, nSequence의 상대적 잠금시간이 30블록인 입력값을 가진 거래는 적어도 입력값 내에서 참조된 UTXO가 채굴된 시점으로부터 최소 30블록이 지난 시간이 되어서야 유효화된다. nSequence는 입력값당 필드이기 때문에 거래에는 잠금시간이 설정되어 있는 입력값이 여러 개 들어 잇을 수 있고, 그 모든 입력값들은 거래가 유효화되기 위해 충분한 시간을 가져야 한다.

> CSV를 사용한 상대적 잠금시간

CSV 연산코드가 UTXO의 리딤 스크립트 내에서 평가되는 경우 입력값의 nSequence 값이 CSV 변수 이상인 거래에서만 소비가 가능하다. 이 때문에 UTXO가 채굴되는 시점을 기준으로 블록이 특정 높이가 되거나 시간이 특정한 만큼 지날 때까지는 UTXO의 소비가 제한된다.

CSV를 이용한 상대적 잠금시간은 여러 개의 (체인으로 연결된) 거래들이 생성되고 서명은 되어 있지만 전파되지는 않는 시기, 즉 '오프 체인(off-chain)' 상태로 유지되고 있을 때 특히 유용하다. 부모 거래가 전파되고 채굴되어 상대 잠금시간에서 명시한 시점이 될 때까지는 자식 거래를 사용할 수 없다.

> Median-Time-Past

비트코인에서는 벽시간(wall time)과 합의 시간(consensus time) 사이에 차이점이 있다. 비트코인은 분산화된 네트워크다. 즉, 각 참가자들이 각자의 관점에서의 시간을 가지고 있다. 네트워크 상에서는 모든 곳에서 한꺼번에 사건들이 발생하지 않으므로 각 노드의 관점에서 네트워크 대기시간(latency)도 요인으로 포함되어야 한다. 결국 공통의 장부를 생성하기 위해서는 모든 내용이 동기화된다.

블록 헤더에 설정되어 있는 타임스탬프는 채굴자들에 의해 설정된다. 분산화된 노드 시간의 정확도에 대한 차이를 감안하기 위해 합의 규칙이 허용하는 어느 정도의 관용치는 존재한다. 하지만 이 때문에 채굴자들은 잠금시간 설정이 되어 있고 아직 만기가 되지 않은 거래를 포함시킴으로써 더 많은 수수료를 벌기 위해 블록 내에서 시간과 관련하여 거짓말을 할 수 있다.

Median-Time-Past는 지난 11개 블록의 타임스탬프를 취해서 중앙값을 구함으로써 계산할 수 있다. 이 중앙 시간값은 합의 시간이 되고 모든 잠금시간 계산에 사용된다. 지난 2시간(1개당 10분) 동안 생성된 블록의 중간 지점을 잡게 되면 어느 한 블록의 타임스탬프가 가지는 영향력이 줄어든다. 이렇게 11개의 블록을 통합하게 되면 만기가 되지 않은 잠금시간을 가진 거래로부터 수수료를 벌기 위한 의도를 가진 채굴자가 타임스탬프에 영향력을 행사하지는 못하게 된다.

Median-Time-Past는 nLocktime, CLTV, nSequence, CSV에 대한 시간 계산 구현을 변경한다. Median-Time-Past가 계산한 합의 시간은 항상 벽시간에서 약 1시간 후다. 잠금시간 거래를 생성하려면 nLocktime, CLTV, nSequence, CSV 내에서 인코딩하기 위한 기댓값을 추정할 때 이와 같은 사실을 고려해야 한다.

> 수수료 스나이핑(sniping)에 대응하는 타임락 방어

수수료 스나이핑이란 이론적인 공격 시나리오로, 지난 블록을 다시 작성하고자 하는 채굴자들이 이익을 극대화하기 위해 미래의 블록들에서 좀 더 높은 수수료를 가지는 거래를 '저격'하는 것이다.

예를 들어, 현존하는 블록 중 가장 높은 높이를 가지는 블록은 블록 #100,000이라고 하자. 체인을 연장하기 위해 블록 #100,001을 발굴하는 대신 블록 #100,000을 다시 채굴하려고 하는 채굴자들이 있다. 이들은 후보자 블록 #100,000 내에 (아직 채굴되지 않은) 유효한 거래 중 어떤 것이든 포함시킬 수 있다. 사실상 가장 이익률이 높은(kB당 최고 수수료) 거래를 선택해서 그들의 블록에 포함시키려는 동기를 부여받는다.

그 당시 시점에는 이 공격이 수익성이 그다지 높지 않다. 왜냐하면 블록 보상이 블록당 총 거래 수수료보다 훨씬 많기 때문이다. 하지만 미래의 어느 시점이 되면 거래 수수료가 보상(혹은 보상의 총액)의 대부분을 차지할 것이다. 그 때가 되면 이 시나리오를 언제든지 예상할 수 있을 것이다.

비트코인 코어가 거래를 생성하면서 '수수료 스나이핑'을 방지하기 위해서는 기본적으로 nLocktime을 이용해 생성되는 거래들을 '다음 블록'으로 한정시킨다. 시나리오 상으로는 비트코인 코어가 nLocktime을 생성되는 모든 거래에 100,001로 설정하게 된다. 일반적인 환경에서는 nLocktime이 별다른 효과를 나타내지 못하지만 해당 거래들은 블록 #100,000, 즉 다음 블록 내에 포함될 수 있다.

이렇게 모든 거래들이 블록 #100,001로 잠금시간 설정이 되어 있기에 수수료가 높은 거래들을 끌어올 수 없고, 당시에 유효한 거래를 블록 #100,000에서 채굴하는 것밖에 할 수 없다. 이때 새로운 거래 수수료는 얻지 못한다.

profile
매일 공부하기 목표 👨‍💻 

0개의 댓글