티스토리 뷰
본 게시글에서는 저서 '밑바닥부터 시작하는 비트코인'의 Python으로 작성된 예제 코드를 Go로 컨버팅 하여 작성하였습니다.
📺 시리즈
2023.08.25 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 1장 유한체
2023.08.27 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 2장 타원곡선
2023.08.30 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 3장 타원곡선 암호
2023.09.02 - [블록체인/비트코인] - 밑바닥부터 시작하는 비트코인 - 4장 직렬화
🐱👤 전체 코드
🎭 변경사항
- 자주 사용하는 유틸 함수들을 utils 패키지로 분리하였습니다.
💸 트랜잭션이란?
트랜잭션(Transaction)은 블록체인 상에서 상태를 변환시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위입니다.
기존의 중앙화된 은행 시스템에서는 Alice가 Bob에게 송금을 하면 거래 당사자들끼리만 거래 내역을 확인할 수 있으며 그 내역은 은행을 통해서만 증명되었습니다. 반면, 블록체인 상에서 모든 트랜잭션은 공개되어 있고 분산되어 저장되어 있으므로 누구나 거래 내역을 확인할 수 있고 증명할 수 있습니다.
이번 장에서는 트랜잭션이 어떻게 구성되어 있고 어떻게 동작하는지 알아보도록 하겠습니다.
💼 트랜잭션 구성 요소
트랜잭션은 다음 네 가지로 요소로 구성됩니다.
- 버전(Version)
- 입력(Inputs)
- 출력(Outputs)
- 록타임(Locktime)
구성 요소에 대한 자세한 내용을 아래에서 하나하나 살펴보겠습니다.
🔰 버전
트랜잭션의 버전을 의미하며 어떤 부가기능을 사용할 수 있는지 명시해 주기 위해 사용됩니다. 이 값은 보통은 1이지만, 일부 부가 기능을 사용하기 위한 규정(BIP122, BIP68)에 따라 2로 표기하기도 합니다.
버전은 16진수 표현으로 0x01000000입니다. 이 4바이트 값을 리틀엔디언으로 읽으면 1이 됩니다.
📥 입력
입력은 이전 트랜잭션의 출력을 가리킵니다. 이따 살펴볼 테지만 출력에는 비트코인 금액과 잠금 스크립트로 구성되어 있습니다. 입력은 이전 트랜잭션의 출력에 들어있는 비트코인 금액을 가져와 사용하는데 이 비트코인이 정말로 본인이 소유한 것이 맞는지 증명하기 위해 소유자의 개인키로 만든 전자서명으로 잠금 스크립트를 해제해야 합니다. 여기서 3장에서 배운 타원곡선 알고리즘이 사용됩니다.
입력은 여러 개가 있을 수 있습니다. 3만 원짜리 케이크를 사기 위해 5만 원권을 사용할 수 도 있고 1만 원권 3장을 사용할 수 도 있고 아니면 1천 원권 30장을 사용할 수도 있습니다. 이렇게 입력의 개수가 가변적이기 때문에 트랜잭션을 직렬화할 때 입력의 수를 표시해줘야 합니다. 그렇지 않으면 역직렬화가 불가능해질 것입니다.
입력의 수는 트랜잭션 버전 부분 바로 다음에 옵니다.
입력의 개수는 varint라는 형식으로 표현합니다. varint는 가변 정수(variable integer)의 약자로 0에서 264 - 1 사이의 정숫값을 바이트로 표현하는 방법입니다. 264 - 1 이하의 값을 표현하기 위해서 간단하게 8바이트를 고정해 놓으면 좋겠지만, 1~2바이트의 작은 값을 자주 사용하게 된다면 결국 나머지는 바이트 낭비로 이어지게 됩니다. 이에 주어진 값에 따라 가변적으로 바이트 크기를 사용하는 varint 형식이 고안되었습니다.
정수 범위 | Varints 표현 방법 | 예시 |
0 ~ 252 | 1바이트 표현 | 100 -> 0x64 |
253 ~ 216 - 1 | 접두부 0xfd 이후 2바이트를 리틀 엔디언으로 표현 | 255 -> 0xfdff00 |
216 ~ 232 - 1 | 접두부 0xfe 이후 4바이트를 리틀 엔디언으로 표현 | 70015 -> 0xfe7f110100 |
232 ~ 264 - 1 | 접두부 0xff 이후 8바이트를 리틀 엔디언으로 표현 | 18005558675309 -> ff6dc7ed3e60100000 |
각각의 입력은 4개의 하부필드를 가지고 있습니다. 처음 두 개의 필드는 이전 트랜잭션의 출력을 가리키며 나머지 두 개의 필드는 이전 트랜잭션의 출력을 사용하는 방법을 정의합니다.
- 이전 트랜잭션의 해시값 혹은 ID (Previous Tx ID)
- 이전 트랜잭션의 출력 번호 (Previous Tx Index)
- 해제 스크립트 (ScriptSig)
- 시퀀스 (Sequence)
이전 트랜잭션의 해시값 혹은 ID
이전 트랜잭션의 해시값은 hash256 함수의 출력(sha256 함수의 출력을 다시 sha256으로 돌린)이므로 충돌이 (거의) 존재하지 않습니다. 따라서 이전 트랜잭션이 어떤 것인지 쉽게 특정 지을 수 있으므로 이전 트랜잭션 ID라고도 합니다.
이전 트랜잭션의 출력 번호
출력도 입력과 마찬가지로 여러 개가 있을 수 있습니다. 따라서 정확히 몇 번째의 출력인지에 대한 정보가 필요합니다.
해제 스크립트
해제 스크립트는 단단히 잠긴 자물쇠를 여는 열쇠라고 생각하면 됩니다. 이전 트랜잭션의 출력을 사용하기 위해서 해당 출력을 소유하고 있다는 것을 증명하기 위해 사용됩니다.
해제 스크립트는 다른 필드들과는 달리 가변적이므로 정확한 길이를 표시해 주어야 합니다. 이를 위해 해제 스크립트 필드는 varint 형식으로 시작해서 먼저 필드의 길이를 설정합니다.
스크립트에 대한 자세한 내용은 6장에서 다룹니다.
시퀀스
시퀀스는 비트코인의 창시자 사토시 나카모토가 빈번하게 발생하는 거래를 최종 정산만 기록할 수 있도록 고안한 필드입니다. 의도 자체는 좋으나, 거래에 참여하는 사용자들은 또한 채굴자가 될 수 있다는 문제가 있습니다. 채굴자는 자신의 이익을 극대화하기 위해 최종 정산이 이루어지기 전에 특정 시퀀스에서 발생한 트랜잭션을 블록에 포함시킬 수 있습니다.
예를 들어, Alice가 Bob에게 1 비트코인을 빌려주고 Bob이 Alice에게 이자를 붙여서 1.2 비트코인을 돌려주기로 약속을 했습니다. Alice는 Bob에게 시퀀스 1의 트랜잭션으로 1 비트코인을 지급합니다. 이어서 Bob이 시퀀스 2의 트랜잭션으로 1.2 비트코인을 Alice에게 돌려줍니다. 여기서 Bob이 자신의 이익을 극대화하기 위해 최종 정산을 하지 않고 시퀀스 1의 트랜잭션을 블록에 추가합니다. 이렇게 되면 시퀀스 1에서 정산이 이루어지고 시퀀스 2의 트랜잭션은 사라지게 됩니다. 따라서 Bob이 Alice에게 1.2 비트코인을 지급한 것이 무효가 되면서 Bob은 Alice와의 거래가 이루어지기 이전 상태에서 1 비트코인을 이득 보게 되고 Alice는 1 비트코인을 잃게 됩니다.
이러한 문제점에도 불구하고 아이디어는 발전하여 '지불 채널(Payment Channel)'과 '라이트닝 네트워크(Lightning Network)'의 토대가 되었습니다.
📤 출력
출력은 비트코인의 거래 후 종착지를 정의합니다.
각 트랜잭션은 하나 이상의 출력을 가지고 있으며 하나의 트랜잭션으로 여러 명에게 보내는 것이 가능합니다.
출력은 2개의 하부필드로 구성되어 있습니다.
- 비트코인 금액
- 잠금 스크립트(ScriptPubKey)
비트코인 금액
금액은 출력이 내포하는 비트코인의 양이며 사토시 단위(1억 분의 1 비트코인)로 표현합니다. 할당 가능 금액의 최대치는 2100만 비트코인으로 사토시로는 2100조 비트코인입니다. 이 값은 32비트로 나타낼 수 없으므로 64비트로 표현합니다. 그리고 금액은 리틀엔디언으로 적어 직렬화합니다.
잠금 스크립트
잠금 스크립트는 소유자만 잠금 해제할 수 있는 금고입니다. 해제 스크립트와 마찬가지로 varint 형식으로 시작하는 가변 길이 필드로 시작합니다.
🕐 록타임
록타임은 트랜잭션 전파 후 실행을 지연시키는 방법을 제공합니다. 록타임 값이 500,000,000보다 크거나 같으면 유닉스 타임으로 해석하며 그 미만은 블록 높이로 해석합니다. 즉, 트랜잭션은 발생하더라도 트랜잭션의 록타임이 의미하는 시점에 도달하기 전까지는 입력이 가리키는 출력을 사용할 수 없습니다.
록타임은 입력에 포함된 시퀀스 값이 ffffffff일 때 무시됩니다.
록타임은 4바이트의 리틀엔디언으로 직렬화됩니다.
록타임의 주요 문제는 록타임에 도달했을 때 트랜잭션의 수신자가 트랜잭션이 유효한지 확신할 수 없다는 점입니다. 트랜잭션의 입력이 가리키는 출력을 록타임에 도달하기 전에 다른 트랜잭션에서 이미 사용해 버렸다면 록타임이 걸렸던 트랜잭션은 무효 트랜잭션이 되는 것입니다.
이러한 문제를 해결하기 위해 BIP65에서 도입한 OP_CHECKLOCKTIMEVERIFY는 록타임까지 출력을 사용하지 못하게 합니다.
💰 트랜잭션 수수료
비트코인 합의 규칙 중 하나는 코인베이스 트랜잭션을 제외한 모든 트랜잭션의 입력 합은 출력의 합보다 크거나 같아야 한다는 것입니다. 트랜잭션의 수수료는 단순히 입력 합에서 출력 합을 빼는 것으로 계산하며 채굴자에게 채굴에 대한 보상으로 지급됩니다. 그런데 채굴자는 자신의 이익을 극대화하기 위해 블록에 추가한 트랜잭션을 선택할 수 있습니다. 즉, 수수료가 적을수록 해당 트랜잭션은 채굴자들에게 선택받을 기회가 적어지게 되고 어쩌면 영원히 블록에 올라가지 못할 수도 있습니다.
출력의 합은 각 출력의 필드에 포함된 비트코인 금액의 합으로 구할 수 있습니다. 그런데 입력의 합은 어떻게 구해야 할까요? 바로 각 입력이 가리키는 이전 트랜잭션의 출력에서 비트코인 금액을 가져와야 합니다. 만약 비트코인 풀 노드를 가지고 있지 않다면 믿을 수 있는 제삼자가 제공하는 풀 노드로부터 이 정보를 불러와야 합니다. 예제에서는 아래 링크의 블록 탐색기를 사용하여 트랜잭션 정보를 불러옵니다.
https://github.com/Blockstream/esplora
제삼자를 최대한 믿지 말아야 하는 이유
닉 사보는 그의 저명한 에세이에서 '신뢰하는 제삼자가 보안의 구멍이다'라고 언급했습니다. 우리는 신뢰하는 제삼자가 제공해 주는 정보를 의심의 여지없이 그대로 받아들이곤 합니다. 이것이 보안적인 맹점이라는 것입니다. 지금 당장은 문제가 없더라도 제삼자는 언제 돌변하여 우리에게 피해를 줄지 모릅니다. 따라서 정보를 맹목적으로 받아들이기보다는 여러 가지 방식으로 검증을 해야 합니다. 그런 점에서 비트코인은 안전하다고 볼 수 있습니다.
🎡 UTXO 집합
UTXO(Unspent Transaction Output)는 아직 사용하지 않은 트랜잭션 출력을 의미합니다. 그리고 이러한 미사용 출력의 집합을 UTXO 집합이라고 합니다. 이 집합은 현재 유통 중인 모든 비트코인을 의미하며 네트워크 상의 풀 노드는 항상 UTXO 집합을 최신 상태로 유지해야 합니다.
UTXO 집합은 존재하지 않는 출력을 사용하려는 시도 또는 이미 사용된 출력을 사용하려는 이중 지불 문제들을 찾아내고 잘못된 트랜잭션을 무효화시키는 데 사용됩니다.
❗ 코딩 중 발생한 이슈
1. 6장과 연결되어 있는 코드
트랜잭션의 입력에서 해제 스크립트를 파싱하거나 트랜잭션의 출력에서 잠금 스크립트를 파싱하는 부분이 6장과 연결되어 있어서 5장에 나와있는 코드만으로는 내용을 완결 지을 수 없었습니다. 따라서 이번 장에서는 코드와 관련된 내용을 다수 제외했습니다.
2. API 호출 응답 16진수 처리하기
url := fmt.Sprintf("%s/tx/%s/hex", tf.GetURL(testnet), txID)
resp, err := tf.client.Get(url) // GET 요청을 보내 트랜잭션의 16진수 직렬화 결과를 가져옴
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("error fetching %s: %s", txID, resp.Status)
}
raw, err := io.ReadAll(resp.Body) // 응답 바디를 읽어서 raw에 저장
if err != nil {
return nil, err
}
트랜잭션을 직렬화한 16진수값을 가져오는 api 호출의 응답이 문자열로 되어 있어서 버전 정보의 경우 리틀엔디언 형식으로 01000000인데 바이트로 변환할 경우 [48 49 48 48 48 48 48 48]이 되어서 리틀엔디언에서 정수로 변환했을 때 이상한 값이 나오는 문제가 있었습니다.
rawHex := make([]byte, hex.DecodedLen(len(raw)))
_, err = hex.Decode(rawHex, raw) // raw를 16진수로 디코딩한 결과를 rawHex에 저장
if err != nil {
return nil, err
}
문제를 해결하는 방법은 생각보다 간단했습니다. 응답으로 읽어 들인 바이트 슬라이스를 16진수 바이트 슬라이스로 디코딩하면 트랜잭션 버전 정보가 [1 0 0 0]으로 정상적으로 불러와지는 것을 확인했습니다.
📍 더 찾아볼 거리
- UTXO
- 지불 채널
- 라이트닝 네트워크
- 코인베이스 트랜잭션
- BIP65
📖 참고자료
글에서 수정이 필요한 부분이나 설명이 부족한 부분이 있다면 댓글로 남겨주세요!
'블록체인 > Bitcoin' 카테고리의 다른 글
밑바닥부터 시작하는 비트코인 - 7장 트랜잭션 검증과 생성 (1) | 2023.09.16 |
---|---|
밑바닥부터 시작하는 비트코인 - 6장 스크립트 (0) | 2023.09.11 |
밑바닥부터 시작하는 비트코인 - 4장 직렬화 (0) | 2023.09.02 |
밑바닥부터 시작하는 비트코인 - 3장 타원곡선 암호 (0) | 2023.08.30 |
밑바닥부터 시작하는 비트코인 - 2장 타원곡선 (0) | 2023.08.27 |