1️⃣

입력과 출력

트랜잭션 내용 보기

실제 트랜잭션은 다음과 같이 구성된다.
그림을 보면 0.1BTC를 송금하고 2개의 Unspent 가 생성되었다.
0.15BTC는 다른 주소로 가고 0.0845BTC는 다시 자기 자신으로 보내졌다.
수수료 0.0005BTC도 책정된 것을 볼 수 있다.
위 그림을 실제 데이터로 보면 다음과 같다.
{ "version": 1, "locktime": 0, "vin": [ { "txid": "7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18", "vout": 0, "scriptSig" : "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf", "sequence": 4294967295 } ], "vout": [ { "value": 0.01500000, "scriptPubKey": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG" }, { "value": 0.08450000, "scriptPubKey": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG", } ] }
JSON
복사
vin은 위 그림에서 보는 하나의 송금내역 데이터를 보여주고,
vout은 두 개의 Unspend를 나타낸다.
트랜잭션의 실제 데이터에는 보내고 받는 사람의 주소가 없다.
보내는 사람의 0.1BTC가 어디에도 나와있지 않다.

트랜잭션 출력과 입력 (Outputs and Inputs)

트랜잭션의 기본 구성 요소는 출력이다.
출력은 비트코인 화폐 덩어리로, 전체 네트워크에서 유효성을 보장한다.
여러 출력 중 미사용 출력(Unspent transaction output, UTXO)를 추적하여 잔액을 계산할 수 있다.
위 그림은 세 번의 거래를 도식화 한 것이다.
277278 블록: Joe는 Alice에게 가지고 있는 비트코인 전액을 보냈다. Joe의 UTXO는 사용(spent) 되었고, Alice의 UTXO가 생겼다.
277298 블록 : Alice는 Bob에게 가지고 있는 비트코인의 일부를 보냈다. Alice의 과거 UTXO는 사용되었고 새로운 UTXO 두 개가 생겼다. 하나는 Bob에게 보낸 비트코인이고, 하나는 보내고 남은 나머지 잔액이다.
277316 블록 : Bob이 Gopesh에게 비트코인을 일부 보냈다. Bob의 과거 UTXO는 사용되었고, 새로운 UTXO 두 개가 생겼다. 장부상에는 총 3개의 UTXO가 존재한다.
트랜잭션에서 바트코인 금액은 사토시 단위 (1억분의 1 비트코인)으로 기록되어 정수로 표기된다.
한번 생성된 출력(UTXO)는 재사용될 수 없다.
UTXO를 사용하고 새로운 UTXO를 생성하며 비트코인의 이전이 이루어진다.
여러개의 UTXO를 사용하고 한 개의 UTXO로 생성될 수도 있다.
한개의 UTXO가 여러개의 UTXO로 생성될 수도 있다.
사용된 입력의 총 합에서 새로 생성된 UTXO 금액의 총 합을 빼면 수수료가 된다.

비트코인과 현금의 유사성

앞에서 알아본 바와 같이 비트코인 UTXO는 금액을 가지고 있는 현금과 비슷하다.
실제 지갑에 현금이 얼마가 있는지 알아보려면 직접 세어봐야 하는것 처럼 비트코인도 UTXO의 잔액을 알아야 내 재산이 얼마인지 알 수 있다.
현금에 부여된 일련번호와 유사하게 각 잔액의 돈에 식별자가 있다.
지불결제할 때에도 현금과 비슷하다.
예를들어 5000원짜리를 구매할 때 10000원짜리를 지불하면 새로운 5000원이 생겨서 나에게 돌아온다.
현금과 UTXO에도 차이가 있다.
현금의 ID는 사라지지 않지만, UTXO의 식별자는 한번 사용하고 나면 다른 식별자로 대체된다.
현금은 천원권, 만원권 등 액수가 정해져있지만 UTXO는 따로 액수가 정해져있지 않다.
따라서 비트코인의 거래는차변과 대변으로 이루어지는 복식부기와 유사하다.
이더리움의 거래
비트코인이 현금과 유사한점이 많다면, 이더리움의 거래는 은행의 통장과 유사하다.
각 주소는 얼마를 가지고 있는지 총액 데이터를 상태저장소에 저장한다.
내가 이더리움을 지불하는 경우 총액에서 송금액만큼을 차감하여 재 저장한다.
각 방법의 장단점이 있다. 고민해보자.

비트코인의 송금

비트코인의 현금(UTXO)은 개인의 지갑에 있지 않고 블록체인에 기록되어 있다.
모두의 컴퓨터 안에 있는 이 현금은 아무나 가져가선 안된다.
그렇다면 저 널려있는 현금을 가져갈 수 있는 사람은 어떻게 결정될까?
그것은 보내는 사람 마음이다.
비트코인도 돈뿌리기가 가능하다. “아무나 가져가세요. 먼저 줍는 사람이 임자.”
비트코인의 입력과 출력은 이렇게 작성한다.
돈을 가지고 있는 사람은 돈을 보낼 때 문제를 낸다.
돈을 받는 사람은 문제를 맞추고 소유권을 가져온다.
원하면 다음과 같은 트랜잭션도 가능하다.
내가 돈을 보내면서 “5에 몇을 더하면 9가 될까요?" 문제를 낸다.
그러면 누군가 가장 먼저 4를 넣고 가져오면 된다.
입력 : 누군가 A 에게 돈을 보냈다.
출력 : A가 문제를 냈다.“5에서 몇을 더하면 9가 될까요?"
내 돈을 보내면서 문제를 냈다.
즉 문제를 통해 내 돈을 잠궜다.
그래서 이를 잠금 스크립트라 한다.
입력 : B가 정답을 맞췄다.“정답은 4”
누군가가 정답을 맞췄다.
문제를 풀었기 때문에 이를 해제 스크립트라 한다.
출력 : B는 자기 주소로 비트코인을 보냈다.
피터 도트의 퀴즈와 상금
2013년 피터 도트는 자신의 2.48BTC 비트코인을 잠그고, 다음 문제를 냈다.
“해시 충돌을 찾아라.”
2017년 구글이 이 문제를 풀어서 가져갔다.
비트코인의 서명과 검증
그럼 일반적으로 사람들은 어떤 문제를 낼까?
잠금 스크립트 : “이 공개키의 서명을 만들 수 있으면 이 돈을 가져가.”
잠금 스크립트엔 공개키가 들어있다.
그래서 다른말로 ScriptPubKey라 부른다.
해제 스크립트 : “이것이 나의 서명이야. 그러니까 이 돈은 내꺼.”
해제 스크립트에는 서명값이 들어있다.
그래서 다른말로 ScriptSig라 부른다.
지갑 속에 나의 비트코인 총액은 어떻게 표기할 수 있을까?
장부에 있는 모든 UTXO에 있는 잠금 스크립트를 본다.
잠금 스크립트 내에 다음 문제를 찾는다.
“내 주소(공개키)의 서명값을 제출하라.”
그 UTXO에 달린 비트코인의 합을 계산한다.
지갑에서 잔액을 보여준다.

트랜잭션의 출력

다음은 출력의 예시이다. 두 개의 출력이 있는것을 알 수 있다.
"vout": [ { "value": 0.01500000, "scriptPubKey": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG" }, { "value": 0.08450000, "scriptPubKey": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG" } ]
JSON
복사
value는 이 출력에 달려있는 비트코인 금액이다.
scriptPubKey는 이 돈을 가져갈 수 있는 암호화 퍼즐이다.
이 출력을 네트워크로 다른사람에게 보낼때는 이 내용을 다음과 같이 직렬화 해서 보낸다.
크기
필드
설명
8 바이트(리틀 엔디언)
금액
사토시 단위의 비트코인 가치
1 바이트(VarInt)
잠금 스크립트 길이
뒤이어 나오는 바이트 단위의 잠금 스크립트 길이
가변 길이
잠금 스크립트
출력을 소비하기 위한 조건을 정의한 스크립트
직렬화란? 위의 보이는 그림처럼 json같은 형태로 구조화 해서 전송할 수 있다. 하지만 구조화된 데이터는 필요없는 텍스트가 많아 전송시 낭비가 심하다. 직렬화는 데이터를 일정 규칙대로 붙여서 보내는 것을 말한다. 수신자도 규칙을 알기 때문에 다시 구조화를 할 수 있다.
VarInt (Variable Integer)
큰 정수를 표현할 때 가변적 용량을 사용하여 효율성을 높이기 위해 고안되었다.
이론적으로는 1바이트에서 표시할 수 있는 수는 255까지이다.
더 큰수를 표현하기 위해선 더 많은 용량을 확보해야 하는데, 잘 사용하지 않는 경우 용량 낭비가 심할 수 있어서 고안되었다.
접두부 1바이트에 사용할 바이트 수를 결정하고 이후에 숫자를 표현한다
접두부 없음 : 0~252까지 표현 (예약 접두부 3개 제외)
fd : 이후 2바이트가 숫자로 표현된다. (2^16)-1까지
fe : 이후 4바이트가 숫자로 표현된다. (2^32)-1까지
ff : 이후 8바이트가 숫자로 표현된다. (2^64)-1까지
위 트랜잭션을 직렬화하면 다음처럼 표현이 가능하다.
위 텍스트는 앨리스 거래의 전체 트랜잭션이다. 매우 복잡한 거래가 직렬화를 전송 용량을 줄일 수 있다.
하지만 저 텍스트는 사람이 해석하기 위해서는 규칙을 따라가야 한다. (색깔로 잘 구분해서 따라오자)
02 : 두 개의 출력이 있다는 의미이다.
60e3160000000000 : 1,500,000사토시를 보낸다는 의미이다.
1500000을 16진수로 나타내면 16 e3 60인데 리틀 엔디언이라 작은 자릿수를 더 앞에 적는다.
따라서 60 e3 16 00 00 00 00 00이 되었다.
19 : scriptPubKey의 길이를 나타낸다. 총 25바이트이나 16진수로 변경되어 19가 되었다.
76 a9 14 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 88 ac : scriptPubKey이다.
스크립트 명령어는 향후 다룰 예정이다. 지금은 직렬화된 데이터를 해석하는데 촛점을 맞추자.
76 : 스크립트 명령어중 OP_DUP를 의미한다.
a9 : 스크립트 명령어 중 OP_HASH160을 의미한다.
14 : 다음 20 바이트(16진수로 14)는 데이터임을 의미한다.
ab68025513c3dbd2f7b92a94e0581f5d50f654e7 : 스크립트에 사용하는 데이터 영역이다.
88 : 스크립트 명령어 중 OP_EQUALVERIFY을 의미한다.
ac : 스크립트 명령어 중 OP_CHECKSIG를 의미한다.
그 뒤 굵은 글씨로 된 하나의 출력이 더 있다. 각자 해석해보자.

트랜잭션의 입력

다음은 입력의 예시이다.
"vin": [ { "txid": "7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18", "vout": 0, "scriptSig" : "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf", "sequence": 4294967295 } ]
JSON
복사
입력은 어떤 돈을 다른 사람에게 보낼지를 결정해야 하고,
UTXO에 달려있는 scriptPubKey인 암호화 퍼즐을 푼 정답이 필요하다.
비트코인을 송금할 때는 대상자를 따로 정하지 않고, 문제를 내서 그 문제를 푼 사람만이 이 돈을 가져가게 할 수 있는 구조이다. 일반적으로는 받는 사람만이 풀 수 있는 퍼즐을 낸다. 대표적인 문제로는 “특정 공개키에 해당하는 서명을 할 수 있는가?” 이다.
txid 보낼 UTXO가 들어있는 트랜잭션의 식별자를 의미한다.
vout 는 UTXO가 해당 트랜잭션의 몇번째 출력인지를 가르킨다.
scriptSig 는 퍼즐의 정답이다.
secuence는 일련번호이다.
입력의 직렬화 규칙은 다음과 같다.
크기
필드
설명
32 바이트
트랜잭션 해시
소비될 UTXO를 포함하는 트랜잭션에 대한 참조
4 바이트
출력 인덱스
소비된 UTXO의 인덱스
1 바이트 (VarInt)
해제 스크립트 길이
이어 나오는 바이트 단위의 해제 스크립트 길이
가변 길이
해제 스크립트
UTXO 잠금 스크립트의 조건을 충족하는 스크립트
4 바이트
시퀀스 번호
잠금 시간을 위해 사용하거나 비활성화(0xFFFFFFFF)
다음 직렬화된 데이터를 통해 알아보자
186f9f998a5aa6f048e51dd8419a14d8a0f1a8a2836dd734d2804fe65fa35779: 트랜잭션 해시
00 00 00 00: UTXO 인덱스
8b: scriptSig의 길이. 139 바이트를 16진수로 나타낸 값
48 30 45 … d7 52 ad: scriptSig
ff ff ff ff ff: 시퀀스 번호(비활성화)

UTXO(Unspent transaction output)

UTXO는 아직 입력에 사용 되지 않은 상태의 출력을 말한다.
즉 UTXO는 사용가능한 잔액을 의미하며

구현 : 입력과 출력

입력을 처리하기 위한 코드
class TxIn: #입력의 구성요소 : 어떤 출력을 지불할건지에 대한 트랜잭션 ID와 인덱스, 스크립트, 시퀀스 def __init__(self, prev_tx, prev_index, script_sig=None, sequence=0xffffffff): self.prev_tx = prev_tx self.prev_index = prev_index if script_sig is None: self.script_sig = Script() else: self.script_sig = script_sig self.sequence = sequence def __repr__(self): return '{}:{}'.format( self.prev_tx.hex(), self.prev_index, ) #입력을 파싱하는 함수 @classmethod def parse(cls, s): #32바이트의 이전 트랜잭션 ID prev_tx = s.read(32)[::-1] #출력의 인덱스 prev_index = little_endian_to_int(s.read(4)) #스크립트를 각각 파싱한다. script_sig = Script.parse(s) #시퀀스 sequence = little_endian_to_int(s.read(4)) #초기화 return cls(prev_tx, prev_index, script_sig, sequence) #직렬화를 위한 함수 def serialize(self): #참조할 출력이 있는 트랜잭션 result = self.prev_tx[::-1] #트랜잭션의 인덱스 result += int_to_little_endian(self.prev_index, 4) #입력 스크립트 result += self.script_sig.serialize() #시퀀스 result += int_to_little_endian(self.sequence, 4) return result def fetch_tx(self, testnet=False): return TxFetcher.fetch(self.prev_tx.hex(), testnet=testnet) #입력에 정의된 비트코인 액수를 리턴하는 함수 def value(self, testnet=False): #이전 트랜잭션에서 값을 가져오는 사용자 정의 함수를 사용하여 #대상 출력을 fecth함. tx = self.fetch_tx(testnet=testnet) #출력에 정의되어 있는 액수를 가져온다. return tx.tx_outs[self.prev_index].amount #이전 트랜잭션에서 공개키를 가져오는 함수 def script_pubkey(self, testnet=False): tx = self.fetch_tx(testnet=testnet) return tx.tx_outs[self.prev_index].script_pubkey
Python
복사
출력을 처리하기 위한 코드
class TxOut: #출력의 구성요소 : 금액, 스크립트 def __init__(self, amount, script_pubkey): self.amount = amount self.script_pubkey = script_pubkey def __repr__(self): return '{}:{}'.format(self.amount, self.script_pubkey) @classmethod def parse(cls, s): amount = little_endian_to_int(s.read(8)) script_pubkey = Script.parse(s) return cls(amount, script_pubkey) def serialize(self): result = int_to_little_endian(self.amount, 8) result += self.script_pubkey.serialize() return result
Python
복사

참고자료