Intro
해당 Posting은 Bitcoin이 무엇이고, 이것으로 무엇을 할 수 있는지에 대해서 설명하지 않고 Bitcoin을 구현하는 기술에 대하여 다룹니다. 또한, 책의 모든 내용을 충실히 번역하는 것이 아닌 작성자의 생각이 많이 담겨 있으니 유의 바랍니다.
해당 chapter에서는 Bitcoin이 실제로 어떻게 전달되며, Bitcoin을 이용한 거래는 어떻게 수행되는지를 다룹니다. 또한, 이를 위해 Bitcoin에서 구축한 Script라는 language를 소개하고 여러 인증 방식을 소개합니다.
1. Transaction
Transaction은 Bitcoin의 꽃이라고 표현할 수 있을 정도로 가장 중요한 위치를 차지하고 있습니다. 앞서 보았던, Signature, Serialization 역시 이를 위한 토대라고 보시면 됩니다. 우선 Transaction이 무엇인지에 대해서부터 알아봅시다.
Transaction의 뜻을 한국어로 번역하면, 거래라고 할 수 있습니다. Bitcoin system 상에서는, Bitcoin을 누군가에게 전송하는 행위를 Transaction이라고 정의합니다. Bitcoin 상에서 Transaction은 여러 특이한 속성을 가집니다. 이를 이해하는 것이 Bitcoin을 이해하는 기반이 될 것입니다. (아래 특징은 저의 주관적인 생각을 담은 것입니다. 더 많은 특징이 있지만, 해당 chapter에서 설명할 내용을 이해하기 위해서는 다음 내용을 일단 머릿속에 새기고 가도록 합시다.)
1-1. 특징
- 공개성 (Public) : 모든 Transaction은 공유됩니다. 누구나 원한다면, 조회가 가능합니다.
- 연속성 (Continuity) : coinbase에서 직접 생성한 Transaction을 제외하고는 모든 Transaction은 이전 Transaction에 의존하여 정의됩니다. 또한, 해당 chapter에서는 coinbase에서 생성된 Transaction에서는 고려하지 않습니다. (실생활에서 생각해보면, 거래라는 것도 은행에서 직접 전달받은 돈이 아니라면, 모두 다른 사람과의 거래를 통해서 생성되는 것이므로 당연하다고 할 수 있습니다.)
- 일회성 (One time) : 하나의 Transaction이 여러 번 사용될 수 없습니다. 단 한 번만 사용됩니다. (Transaction은 output을 여러 개 가지므로, 이들이 각 각 사용될 수는 있어도, 같은 Transaction의 같은 output은 단 한 번만 사용되어야 한다는 점입니다. 그렇기에 잔돈이 발생한다면, Transaction에 다시 자신에게 보내는 output을 생성합니다.)
- 익명성 (Anonymous) : 해당 Transaction의 output을 사용할 사람을 명시하지 않습니다. 즉, 누구나 해당 Transaction output의 소유권을 주장할 수 있습니다. (실제 세상에서 누가 누구에게 보내는 것인지는 알 수 없습니다. 원한다면, 거래 address를 계속 바꿀 수도 있습니다. 그리고 그렇게 계속 바꾸는 것을 권장하기도 합니다.)
Transaction을 통해서 우리는 Bitcoin을 받을 수도 있고, 전달할 수도 있으며, 해당 Bitcoin이 자신의 소유라는 것을 증명할 수 있습니다.
또한, 기억해두어야 할 점은 Transaction의 Output은 Bitcoin을 포함한다는 사실을 기억합시다. 그리고, 모든 Bitcoin은 Transaction에 의해서 존재한다는 것을 기억하는 것입니다.
이것이 어떻게 가능한지 Transaction의 구성 요소를 먼저 살펴보고 알아보도록 합시다.
1-2. 구성요소
- Version : Transaction의 version이 존재합니다. Bitcoin 자체가 계속해서 발전해왔기 때문에, 하나의 version으로 고정되어 있지는 않습니다. 대게는 1이지만, 필요에 따라 다른 version을 써야 하는 경우도 있습니다.
- Outputs : 여러 개의 output을 가질 수 있으며, 각 output은 다음과 같은 값을 포함합니다.
- amount : 해당 output이 가질 bitcoin의 양을 의미합니다.
- ScriptPubKey : 해당 output이 후에 사용될 때, 정당한 권한이 있는지를 확인할 수단이 됩니다. 마치 금고의 잠금 장치라고 생각할 수 있습니다. 이를 어떻게 잠그는지에 대해서는 밑에서 Script part에서 설명합니다.
- Inputs : 하나의 input은 이전 Transaction 중 하나의 output을 가르키고 있으며, 이를 여러 개 가질 수 있습니다. 여기서 각 input은 두 가지 기능을 할 수 있어야 합니다. 첫째로, 특정 Transaction의 하나의 output을 식별할 수 있어야 합니다. 사용하고자 하는 Transaction이 자신의 것임을 증명할 수 있어야 합니다.
- PrevTxId : 이전 Transaction을 고유하게 식별할 수 있는 값입니다.
- PrevTxIndex : 이전 Transaction의 output 중 하나를 식별하기 위한 값입니다.
- ScriptSig : 해당 output이 자신이 사용할 수 있는 데이터임을 증명할 수 있는 수단입니다. 마치 금고의 열쇠로 생각할 수 있습니다.
- Sequence : 초기에 Bitcoin 설계 시에는 동시에 서로 간에 너무 많은 Transaction이 생기는 것을 막기 위해서, 여러 Transaction을 하나의 Transaction으로 통합시키기를 원했습니다. 그래서 그때 해당 거래가 몇 번째 인지를 표시하기 위한 수단으로 사용되었으나 현재에는 보안상의 취약점이 발견되어 사용되고 있지 않기에, 4 bytes little endian으로 최댓값(0xffffffff)으로 표기합니다.
- Locktime : 위에서 설명한 Sequence 처럼, Transaction을 어느 정도 시간이 될 때까지 Transaction의 input으로써 사용되는 것을 막는 것입니다. 이는 다음 chapter에서 설명할 Block의 height가 될 수도 있고, Unix timestamp를 통해서 시간을 지정할 수도 있습니다. 이 또한, 4 bytes를 통해서 표현합니다.
1-3. 추가 개념
- Fee
일명 거래 수수료라고 생각할 수 있습니다. 대게, 이 거래를 확인해주는 miner들에게 주어지는 보상으로 생각할 수 있습니다. 송신자는 거래를 할 때, 이를 인증받기 위해서 이를 확인해줄 여러 제3자들에게 일정 수수료를 제시합니다. 빠른 거래를 원한다면, 더 많은 비용을, 천천히 해도 상관이 없다면, 적은 비용을 투자할 수 있습니다. - UTXO(Unspent Transaction Output) Set
사용하지 않은 Transaction Output의 집합입니다. 이를 유지할 수 있어야지만, 왜냐하면, 두 번 이상 사용한다는 것 자체를 막아야만 하기 때문입니다. 모든 UTXO를 포함하고 있으며, 이를 계속해서 추적하는 노드를 Full nodes라고 부르며, 이것이 있어야지만 Bitcoin 거래를 빠르게 수행할 수 있습니다.
Transaction이라는 것은 결국 Bitcoin을 전달하는 방법입니다. 이것이 있어야만 우리는 실세계에 있는 물건을 사는 명분을 가질 수 있는 것입니다. 그런데 여기서, 의문점이 가장 크게 생길 수 있는 부분이 있습니다. 뭔가 거래를 하기 위해서는 Transaction을 통해서 Bitcoin을 보내야 하는 것은 이해했는데, 내가 보낼 때 사용하는 Transaction이 모두에게 공개된다고 했는데, 이게 자신의 것이라는 것을 어떻게 증명할 수 있을까요? 이 방법으로 고안된 것이 Script입니다. 이에 대해서 살펴봅시다.
2. Script
Bitcoin(Transaction의 Output)을 전달할 때, 이를 누구나 쓸 수 없게 **잠그는 과정(lock)**이 필요하고, 후에는 이를 사용할 수 있게 **해제 과정(unlock)**이 필요합니다. Bitcoin에서는 이를 위해서 Script라는 것을 고안해냈습니다. Script는 완성된 형태로 존재할 때, 해당 output의 소유가 자신이라는 것을 누구나 인정할 수 있는 문서가 됩니다. 그래서 이를 반으로 잘라서, 하나는 output 쪽에 붙여두고(ScriptPubkey), 나머지 하나(ScriptSig)는 자신이 소유하고 있다가 사용할 때, 이 조각을 들이 밀어서 자신임을 인정하는 것입니다.
그렇다면, Script라는 것이 도대체 무엇이길래 누구나 인정할 수 있는 문서가 될 수 있는 것일까요? 이는 Script의 해석법을 알면, 이해할 수 있습니다.
2-1. Script (language)
Script라고 쓰기도 하고, Script language라고도 부르는 해당 언어는 영어 같은 사람의 언어로 작성되지도 않고, python이나 c와 같은 turning complete(=모든 계산 가능한 문제를 표현할 수 있는, 대게는 loop, condition, memory 제어 기능을 포함하는지를 의미합니다.**)**한 programming language 로 작성하지 않습니다. 왜냐하면, 이러한 Script는 효율을 위해서 너무 복잡한 로직을 가져서도 안되고, 보안상의 취약점을 만들 수 있는 수단 자체를 막기 위해서 입니다. 따라서, Bitcoin의 Script에서는 loop문을 허용하지 않는 형태를 가집니다.
2-2. 구성요소
Script를 해석하기 위해서는, 구성요소를 먼저 알아야 합니다. 따라서, 각 구성요소에 대해서 알아보겠습니다.
- Element : 일반 programming language에서 variable이 하는 역할을 맡습니다.
- Operation : 일반 programming language에서 function의 역할을 맡습니다. 이는 element를 받아서 동작을 수행하여, Stack의 변화를 일으킵니다. 만약, 중간의 Operation의 동작이 실패한다면, 해당 Script의 실행은 종료되고, 타당하지 않은 Script라는 결론을 내놓습니다.
- Stack : 일반 programming language와는 다르지만, 후위 표현법에 기반한 language에서는 흔히 볼 수 있는 특징입니다. element가 존재할 수 있는 공간으로 operation은 stack 안에 있는 element만을 소비하여 동작합니다.
전체 코드는 위에서부터 실행되면서, 마치 하나의 stack처럼 구성되며, 후위 표기식처럼 동작한다고 생각하면 됩니다. 후위 표기식에서는 element가 먼저 나오고 이를 기억해두고 있다가 연산을 수행하는 방식입니다. (따라서, stack이 필요한 것입니다.) 따라서, 위에서 부터 실행하면서 element가 나온다면, stack에 쌓아두고, operation이 나온다면 stack에 있는 데이터를 최신순으로 사용합니다.
2-3. Operation의 종류
앞 서 Script는 turning complete한 언어가 아니라고 했으므로, operation에는 loop를 포함하지 않거나 현재는 사용되지 않습니다. 또한, 보안상 취약점이 밝혀진 Operation 역시 사용되지 않습니다. 여기서는 Script의 특성을 설명할 수 있는 몇 가지의 Operation을 설명하고 동작 방식을 설명합니다.
- OP_DUP : stack에 있는 element 중 가장 앞에 있는 element를 복사해서 stack에 추가합니다.
- OP_x(number) : stack에 x element를 추가합니다. 이때, x는 0 ~ 16까지의 수를 뜻합니다.
- OP_PUSHDATAx : 먼저 x byte에 해당하는 값을 받습니다. 이는 이제 stack에 입력할 데이터의 크기를 의미합니다. 그 후 입력받은 길이만큼을 읽어 들인 후에 이를 stack에 추가합니다. x는 1, 2,4가 존재하지만, stack에 입력하는 데이터는 최대 520 bytes 까지만 허용합니다.
- OP_VERIFY : stack에서 하나의 값을 꺼낸 후, 1인지를 확인합니다.
- OP_EQUAL : stack에서 두 개의 값을 꺼낸 후, 서로 같은지를 확인하고, 같다면 1 다르다면, 0을 추가합니다.
- OP_CHECKSIG : stack에서 두 개의 값을 꺼낸 후, 첫 번째 element는 PubKey 그리고, 두 번째 element는 Signature로 하여 ECDSA를 만족시키는지를 확인합니다.
- OP_MULTI_CHECK_SIG : 이는 stack에 PubKey와 Signature가 하나 이상일 때, 이를 모두 확인하는 방법입니다.
- OP_HASH160, OP_SHA1, OP_SHA256, OP_HASH256 : stack의 하나의 element를 꺼낸 후, hash하여 다시 stack에 추가합니다.
추가적인 Operation에 대해서도 궁금하다면, 아래 link를 참고해주세요.
2-3. Goal
Script의 최종 목적은 Script의 모든 구성요소를 실행시켜서, 중간에 operation의 에러 없이 stack에 1이라는 숫자를 남기는 것입니다. 따라서, 해석이 실패하는 경우는 두 가지 입니다. Script의 해석 도중에 operation이 에러를 발생시켰거나, 모든 Script를 해석했음에도 stack에 1이 아닌 값이 있거나 아무 값도 없는 경우입니다.
아까 말했듯이 우리는 이 Script를 두 개의 조각(ScriptPubKey, ScriptSig)으로 나눕니다. 즉, 코드를 중간에 툭 잘라버린다는 것입니다. 그래서, 후반부에 해당하는 내용(ScriptPubKey)을 Transaction의 Output에 넣어서 보관합니다. 그래서, ScriptPubKey에 전반부에 해당하는 ScriptSig를 가진 사람이 있다면, 그 사람이 Transaction의 Output에 있는 Bitcoin을 해당하는 amount만큼 사용할 수 있다는 것입니다.
2-4. Example
이제 ScriptPubKey가 주어졌다고 가정합시다. 그렇다면, 이는 마치 하나의 문제처럼 느껴질 수 있고, 우리는 이를 만족하는 값을 ScriptSig에 넣어서 만들어주기만 하면 됩니다.
2-4-1. 을 만족하는 x 값 넣기
완성된 Script에서 x에 무엇이 들어가면 될지를 추측해봅시다.
결론상 을 만족하는 x값이 필요하므로 여야 합니다.
2-4-2. SHA-1의 collision을 발생시키는 두 개의 값
이번에는 다음과 같은 ScriptPubKey가 주어졌을 때, ScriptSig로 뭐가 들어가야 할지를 추측해봅시다.
ScriptPubKey
다음과 같은 형태가 주어질 때, 이를 역연산하여 필요로 하는 값이 무엇인지 찾아나갈 수 있습니다.
결론상 다음의 조건을 만족하는 h, k를 ScriptSig로 넣어주면 됩니다.
이는 SHA-1이 collision을 발생하게 하는 두 개의 값을 넣어주면 됩니다. (이를 찾을 수 있는지 없는지가 hash함수의 성능을 표시하는데 가장 큰 지표가 됩니다.)
이와 같은 형태로 특정 수학 문제, 또는 hash collision 예시에 대해서 Bitcoin을 통해서 현상금을 걸기도 한다고 합니다.
2-5. 주요 Script
위의 예시를 살펴보았지만, 사실 위와 같은 형태로 Script를 표현한다면, 누구나 해당 Transaction의 Output안에 있는 Bitcoin을 사용할 수 있을 것입니다. 이제 나만 사용할 수 있는 Transaction Output을 만들기 위한 주요 Script 형태를 알아보겠습니다.
2-5-1. p2pk(Pay to PubKey)
ScriptPubKey에는 PubKey와 OP_CHECKSIG 두 개를 넣어두고, ScriptSig에 Signature만 넣어두는 방식입니다. 이 방식 때문에, Transaction Output에 있는 ScriptPubKey의 이름이 이렇게 불려지고, Transaction Input의 ScriptSig의 이름이 정해지게 된 것입니다.
2-5-2. p2pkh(Pay to PubKey Hash)
p2pk에서 문제가 하나 발생할 수 있습니다. 바로 공개키가 모든 이들에게 보인다는 점입니다. 이것이 왜 문제일 수 있냐고 할 수 있지만, 해당 거래 자체가 모두 공개되기 때문에 공개키를 열어두고, 긴 시간 동안 사용하지 않는다면, 언제 가는 무식하게 풀어나가는 과정에서 답을 찾아낼 수도 있습니다. 따라서, 대게는 공개키의 유효시간을 두고 하는 것이 일반적인 경우가 많습니다. 하지만, Transaction의 유효기간을 둘 수 없으므로, PubKey를 바로 공개하지 않는 방식입니다. 따라서, 이름에서부터 느껴지겠지만, PubKey의 hash를 수행합니다. 그리고, 결론적으로 거래를 사용할 때, output을 사용하는 입장에서, pubkey와 signature를 모두 사용하는 방식입니다. (여기서 PubKey에 hash160을 수행하고, Base58로 encoding 한 것을 address라고 합니다.)
ScriptPubKey 와 ScriptSig
Script의 성공적인 동작예시 1
Script의 성공적인 동작예시 2
2-5-3. p2psh (Pay to Script Hash)
p2psh는 여러 개의 key와 signature를 가지는 경우에 사용할 수 있습니다. 여러 개의 PubKey를 포함하고 있는 RedeemScript라는 것을 Hash 하여 ScriptPubKey에 추가시키는 것입니다. RedeemScript라고 불리는 이유는 이것이 후에 다시 하나의 Script로 동작하기 때문입니다. 내부에 들어가는 RedeemScript는 Serialization 해서 element로 넣습니다.
ScriptPubKey, ScriptSig 과 RedeemScript
Script의 동작 예시 - 1
Script의 동작 예시 - 2
Script의 동작 예시 - 3
2-5-0. Signature 생성
주요 Script를 알아보기 전에 빠트린 부분을 먼저 채우고 가야 합니다. 바로, 이전에 ECDSA에서 사용하던 변수 중 현재 누락된 변수를 채우는 것입니다. 이전에 살펴봤듯이 특정 data의 소유가 자신이라는 것을 인증하기 위해서, ECDSA에서는 공개하는 데이터로 Signature와 data의 hash 값 그리고 Public Key를 이용했고, 공개하지 않고, 자신만 가지는 데이터로 Private Key라는 것을 가졌습니다. 여기서 누락된 것은 바로 data의 hash 값과 Signature의 생성 방법입니다.(왜냐하면, Signature는 data의 hash값이 존재해야만 수행할 수 있기 때문입니다.) 이는 어떻게 만들어지는 알아봅시다.
- 현재의 Transaction에서 모든 input의 ScriptSig를 빈 값으로 변환합니다.
- 그리고, ScriptSig를 생성해야 하는 input에 ScriptSig 부분에만 이전 Transaction의 ScriptPubKey를 대입합니다. (이해를 돕기 위해서 이렇게 썼지만, 이 방법만 있는 것은 아닙니다. 이런 방법이 몇 개 더 있으면, 이 방법에 대한 식별 값이 4번에서 표시됩니다.)
- 이를 이제 Serialization 합니다.
- Hash 방법에 해당하는 data의 맨 뒤에 4 bytes로 삽입합니다.
- 그리고, 이를 Hash 합니다.
- 이렇게 생성된 hash 값을 이용해서 Signature를 생성합니다.
이제, 우리는 z와 signature를 생성했습니다. 이제 Transaction의 생성자는 signature를 포함시킬 수 있게 되었습니다. 또한, Transaction을 볼 수 있는 다른 모든 사람들도 해당 Transaction이 타당한지 확인하기 위해서는 단지 Transaction을 위와 같이 변경하여서, z를 얻을 수 있습니다.
3. Transaction Validation
그렇다면, 우리는 Transaction이 언제 타당하다고 말할 수 있을까요? 바로 다음 세 가지를 만족시켜야지 우리는 해당 Transaction이 타당하다고 합니다.
- 사용하고자 하는 Transaction의 output이 진짜 사용된 적이 없는지를 확인해야 합니다. 이는 위에서 제시했던 UTXO를 조회하는 방법으로 수행합니다. 지금은 이 정도로 밖에 설명할 수 없지만, 이는 후에서 더 자세히 다룹니다.
- Transaction의 input들보다 output들이 더 큰 값을 내보내지는 않았는지 확인해야 합니다. 이는 이전 Transaction Output을 모두 더한 값이 혹여 현재 Transaction의 Output의 총합보다 큰지를 확인하도록 해야 합니다. 위의 진짜 사용 여부를 확인하는 과정에서 이전 Transaction output의 amount도 알 수 있으므로 이는 쉽게 계산할 수 있습니다.
- ScriptSig + ScriptPubKey로 만들어진 최종 Script가 타당한지 확인해야 합니다. 이는 위에서 진행했던 Script의 Check를 통해서 수행 가능합니다.
이 모든 과정을 통과했을 때, 우리는 해당 Transaction이 타당하다고 말할 수 있습니다.
4. Transaction Creation
이제까지의 모든 것을 정리하여, Transaction의 생성 과정을 모두 정리해보겠습니다.
- Transaction을 통해 Bitcoin을 보낼 대상의 PubKey 또는 address(PubKey를 hash + Base58)를 받아오고, 얼마나 Bitcoin을 보낼지(amount)를 결정합니다.
- Transaction의 fee로 얼마나 지출할지를 결정합니다.
- Inputs가 사용하는 이전 Transaction Outputs의 합이 (fee + 지출할 Bitcoin) Outputs의 총합보다 크도록 CTXO를 하나 이상 선택합니다. (이 과정에서 CTXO가 정말 사용된 것이 아닌 것인지에 대한 확인도 수행합니다.)
- CTXO가 자신의 값임을 증명할 수 있도록 Signature를 생성하고, (이 과정에서 당연히 2-5-0에서 설명된 과정이 수행됩니다.) 이전 Transaction Output에서 제시한 ScriptPubKey와 결합했을 때, 타당한 Script가 될 수 있도록 하는 ScriptSig를 생성합니다.
- 위에서 생성한 ScriptSig와 이전 Transaction Output을 특정할 수 있는 값을 묶어서 Transaction Input들을 작성합니다.
- Bitcoin을 보낼 대상의 address를 이용해서, 적절한 ScriptPubKey를 생성하여, amount와 함께 Transaction Output들을 만듭니다. 이때 잔돈이 발생한다면, 자신에게 다시 보내는 Transaction Output도 생성해야 합니다.
- Transaction에 version, locktime 등을 추가하여, Transaction을 최종으로 생성합니다.
이전 글과 동일하게 구현 사항은 github에 정의해두었습니다.
Reference
- 🔗 Programming Bitcoin
- Tumbnail : Photo by Icons8 Team on Unsplash
Comments