트랜잭션의 구성
•
우리가 배운 바에 의하면 트랜잭션의 구성은 다음과 같이 요약할 수 있다.
•
트랜잭션을 구성하는 요소는 다음과 같다.
크기 | 필드 | 설명 |
4 바이트 | 버전 | script |
1 바이트(Varint) | 입력의 갯수 | 사용된 입력의 갯수 |
가변 길이 | 입력 | 복수개의 입력 스크립트 |
1 바이트(Varint) | 출력의 갯수 | 사용된 출력의 수 |
가변 길이 | 출력 | 복수개의 출력 스크립트 |
4 바이트 | 록타임 | 트랜잭션 실행 가능 시간 명시 |
•
다음은 트랜잭션의 예시이다.
◦
01000000 : 버전
◦
01 : 입력의 수
◦
813f~ffff : 입력
◦
02 : 출력의 수
◦
02a1~88ac, 99c3~88ac : 두 개의 출력 (출력의 크기는 출력 내에서 정의한다.)
◦
19430600 : 록타임
록타임(locktime)
•
록타임은 트랜잭션의 처리를 지연하기 위한 방법을 제공한다.
•
예를들어 록타임이 600,000이면 600,001블록까지는 트랜잭션을 포함할 수 없다.
•
만약 록타임이 500,000,000보다 크면 이는 유닉스 타임으로 해석하여 처리한다.
•
최대값(ffffffff)이면 록타임을 무시한다.
•
록타임에서 지정한 시간에 도달하기 전에 UTXO가 사용되어 버리면 문제가 발생한다.
•
이러한 문제를 방지하기 위해 BIP65에서 록타임까지 출력을 사용하지 못하게 하는OP_CHECKLOCKTIMEVERIFY가 도입되었다.
트랜잭션의 생성
•
트랜잭션을 설계하기 위해서는 다음을 결정하여야 한다.
◦
비트코인을 어느 주소로 보내고자 하는가?
◦
어느 UTXO를 사용할 수 있는가?
◦
얼마나 빨리 처리되길 원하는가?
•
입력의 구성
◦
자신이 해제 가능한 잠금 스크립트 UTXO를 찾아 트랜잭션 해시와 인덱스를 선택한다.
◦
해제 스크립트를 작성한다. 일반적으로 서명을 만드는 일이다.
•
출력의 구성 (P2PKH 가정)
◦
송금 비용을 결정한다.
◦
받는 사람의 주소를 Base58 디코딩 한다.
◦
스크립트를 작성한다.
•
서명 생성
◦
서명생성은 이미 배운바 있다. 그러나 z(메시지의 해시)를 어떻게 구하느냐가 관건인다.
◦
z를 추출하기 위해 다음 과정을 거친다. (SIGHASH_ALL을 가정하자)
▪
해제 스크립트가 들어갈 자리에 이전 트랜잭션의 잠금 스크립트를 넣는다.
▪
해시 유형을 덧붙인다.
▪
SHA-256 해시를 구하고 이를 z로 사용한다.
◦
z를 통해 서명(s, r)을 만든다.
해시 유형 SIGHASH
•
어느 영역까지 해시를 서명할지를 결정하는 것이다.
•
SIGHASH_ALL은 현재 입력과 다른 모든 출력을 사용한다.
•
SIGHASH_SINGLE는 모든 입력과 입력과 같이 위치하는 출력 하나를 사용한다.
•
SIGHASH_NONE는 모든 입력을 사용하고 출력은 사용하지 않는다.
•
SIGHASH_SINGLE, SIGHASH_NONE는 잘 사용하지 않는다.
•
서명의 직렬화
◦
다음은 서명의 예시이다.
◦
30 : 서명이라는 접두어이다.
◦
45 : 서명의 길이이다.
◦
02 : 서명 구분자이다. 처음 02는 r값이라는 의미이다.
◦
21 : r의 길이이다.
◦
r 값을 넣는다.
◦
02 : 이번엔 s 값이라는 구분자이다.
◦
20: s의 길이이다.
◦
s 값을 넣는다.
트랜잭션의 검증
•
트랜잭션을 수신한 모든 노드는 트랜잭션이 프로토콜에 맞는지 확인한다.
◦
실제 입력이 가리키는 곳에 비트코인이 존재하고 사용 가능한가?
◦
입력의 합이 출력에 합보다 크거나 같은가?
◦
해제 스크립트가 잠금을 잘 해제 하였는가?
•
입력 비트코인 존재 여부 확인
◦
입력 스크립트는 트랜잭션의 해시와 인덱스를 제공한다.
◦
검증자는 해당 인덱스의 UTXO에 충분한 금액이 있는지 확인한다.
◦
해당 UTXO가 마이닝 풀(미 처리된 트랜잭션 집합)에 들어있는지 확인한다. (이중지불 방지)
•
출력과 입력의 합계 확인
◦
총 입력값과 출력값을 비교하여 보유금액보다 더 많은 금액을 보내는지 확인한다.
◦
코인베이스 트랜잭션(채굴 보상)은 예외이다.
•
해제 스크립트 검증
◦
해제 스크립트는 서명 검증이 가장 중요하다.
◦
해제 스크립트 안에는 s, r 값이 존재하지만, z(메시지의 해시)는 존재하지 않는다.
◦
z를 추출하기 위해 다음 과정을 거친다. (SIGHASH_ALL을 가정하자)
1.
트랜잭션 안에 모든 해제 스크립트를 삭제한다. 여러 개인 경우에도 해제 스크립트만 삭제한다.
•
해제 스크립트에는 서명값이 들어가 있다. 서명값을 검증하는데 서명값을 넣는것은 좀 이상하다.
2.
그 자리에 이전 트랜잭션의 잠금 스크립트를 넣는다.
•
과거의 블록을 참조할 필요가 없다. 이미 잠금스크립트에 공개키가 있으므로 공개키의 해시를 넣으면 된다.
3.
해시 유형을 덧붙인다.
•
검증할 트랜잭션과 동일한 해시유형을 붙인다. (메시지의 범위가 같아야 검증이 가능하니까)
▪
SHA-256 해시를 구하고 이를 z로 사용한다.
▪
서명 검증을 수행한다.
트랜잭션 수수료
•
비트코인의 트랜잭션 검증 규칙 중 하나는 모든 입력의 합이 출력의 합보다 크거나 같아야 한다는 점이다.
•
(코인베이스 트랜잭션은 예외이다.)
•
즉 트랜잭션에 사용된 전체의 액수에서 누군가로 전송되는 전체의 액수를 뺀 것이 수수료가 된다.
•
왜 수수료가 필요한가?
◦
수수료는 채굴자들에게 채굴 인센티브 외에 돌아가는 일종의 보상금이다.
◦
과도한 트랜잭션을 막아 시스템 남용을 방지하는 경제적 보안 메커니즘이기도 하다.
•
비트코인 수수료는 전송하려는 비트코인의 액수가 아닌 킬로바이트 단위의 트랜잭션 크기를 기준으로 계산된다.
•
시간이 지나면서 트랜잭션 수수료가 계산되는 방식과 우선 순위에 미치는 영향이 진화했다.
◦
2016년 초부터 비트코인 용량 제한으로 인해 트랜잭션 간 경쟁이 발생했고, 그 결과 수수료가 높아져 사실상 무료로 채굴되는 트랜잭션은 없어졌다.
◦
경제 원칙에 따라 높은 수수료가 먼저 처리되며 수수료가 없거나 매우 낮은 트랜잭션은 거의 채굴되지 않으며 간혹 전파되지도 않는다.
•
지갑, 거래소, 기타 거래를 생성하는 애플리케이션에서는 동적 수수료를 구현해야 한다. 또는 타사의 수수료 추정 서비스를 사용할 수도 있다.
구현 : 트랜잭션
class Tx:
#트랜잭션의 구성요소 : 버전, 입력, 출력, 락타임, 테스트넷 여부
def __init__(self, version, tx_ins, tx_outs, locktime, testnet=False):
self.version = version
self.tx_ins = tx_ins
self.tx_outs = tx_outs
self.locktime = locktime
self.testnet = testnet
def __repr__(self):
tx_ins = ''
for tx_in in self.tx_ins:
tx_ins += tx_in.__repr__() + '\n'
tx_outs = ''
for tx_out in self.tx_outs:
tx_outs += tx_out.__repr__() + '\n'
return 'tx: {}\nversion: {}\ntx_ins:\n{}tx_outs:\n{}locktime: {}'.format(
self.id(),
self.version,
tx_ins,
tx_outs,
self.locktime,
)
#트랜잭션 ID를 추출하여 16진수로 리턴한다.
def id(self):
return self.hash().hex()
#트랜잭션을 직렬화하고 해시값을 만들어 리턴한다.
def hash(self):
return hash256(self.serialize())[::-1]
#트랜잭션 파싱
@classmethod
def parse(cls, s, testnet=False):
#4바이트에서 버전을 추출한다.
version = little_endian_to_int(s.read(4))
#read_varint() : 1바이트의 int를 가져오는 사용자 정의 함수
#입력의 갯수를 가져온다.
num_inputs = read_varint(s)
#입력을 배열에 넣는다
inputs = []
for _ in range(num_inputs):
inputs.append(TxIn.parse(s))
#출력의 갯수를 가져온다.
num_outputs = read_varint(s)
#출력을 배열에 넣는다.
outputs = []
for _ in range(num_outputs):
outputs.append(TxOut.parse(s))
#록타임 4바이트를 가져온다.
locktime = little_endian_to_int(s.read(4))
#인스턴스 생성
return cls(version, inputs, outputs, locktime, testnet=testnet)
#트랜잭션을 직렬화해서 리턴하는 함수
def serialize(self):
#버전
result = int_to_little_endian(self.version, 4)
#입력의 갯수
result += encode_varint(len(self.tx_ins))
#입력
for tx_in in self.tx_ins:
#입력값의 직렬화
result += tx_in.serialize()
#출력의 갯수
result += encode_varint(len(self.tx_outs))
#출력
for tx_out in self.tx_outs:
#출력값의 직렬화
result += tx_out.serialize()
#리틀엔디언 형태의 록타임 4바이트
result += int_to_little_endian(self.locktime, 4)
return result
#트랜잭션의 수수료를 조회하는 함수 (사토시 단위로 리턴)
def fee(self):
input_sum, output_sum = 0, 0
#입력에서 값의 합을 구한다.
for tx_in in self.tx_ins:
input_sum += tx_in.value(self.testnet)
#출력에서 값의 합을 구한다.
for tx_out in self.tx_outs:
output_sum += tx_out.amount
return input_sum - output_sum
#서명을 위한 메시지의 해시를 구하는 함수
def sig_hash(self, input_index, redeem_script=None):
#트랜잭션을 직접 만들어 해시를 구한다.
#버전
s = int_to_little_endian(self.version, 4)
#입력의 수
s += encode_varint(len(self.tx_ins))
#입력
for i, tx_in in enumerate(self.tx_ins):
#서명할 입력을 찾으면
if i == input_index:
#전 트랜잭션에서 공개키 해시를 가져온다. 입력 코드를 참조하자.
script_sig = tx_in.script_pubkey(self.testnet)
# Otherwise, the ScriptSig is empty
else:
script_sig = None
#입력 메시지 완성
s += TxIn(
prev_tx=tx_in.prev_tx,
prev_index=tx_in.prev_index,
script_sig=script_sig,
sequence=tx_in.sequence,
).serialize()
#출력 포함
s += encode_varint(len(self.tx_outs))
for tx_out in self.tx_outs:
s += tx_out.serialize()
#록타임
s += int_to_little_endian(self.locktime, 4)
#해시유형
s += int_to_little_endian(SIGHASH_ALL, 4)
#hash256 해시
h256 = hash256(s)
return int.from_bytes(h256, 'big')
#해제 스크립트 검증
def verify_input(self, input_index):
tx_in = self.tx_ins[input_index]
#이전 트랜잭션에서 공개키의 해시를 가져온다.
script_pubkey = tx_in.script_pubkey(testnet=self.testnet)
#메시지 해시를 가져온다.
z = self.sig_hash(input_index)
#해제 스크립트와 잠금스크립트를 붙인다.
combined = tx_in.script_sig + script_pubkey
#스크립트를 검증한다.
return combined.evaluate(z)
#트랜잭션 검증
def verify(self):
#수수료 검증
if self.fee() < 0:
return False
#입력 검증
for i in range(len(self.tx_ins)):
if not self.verify_input(i):
return False
return True
#해제스크립트에 서명값을 포함
def sign_input(self, input_index, private_key):
#메시지 해시를 가져온다.
z = self.sig_hash(input_index)
#서명값 추출 후 직렬화
der = private_key.sign(z).der()
#서명값에 해시 유형을 넣는다.
sig = der + SIGHASH_ALL.to_bytes(1, 'big')
#공개키를 추출한다.
sec = private_key.point.sec()
#스크립트를 만든다.
script_sig = Script([sig, sec])
#입력에 스크립트를 포함한다.
self.tx_ins[input_index].script_sig = script_sig
# return whether sig is valid using self.verify_input
return self.verify_input(input_index)
Python
복사
트랜잭션을 보내보자.
•
누구든 비트코인 네트워크로 트랜잭션을 보낼 수 있다.
1.
보내는 사람과 받는 사람의 주소를 만들자.
2.
테스트 비트코인을 faucet에 요청하자.
3.
4.
트랜잭션을 만들자.
from ecc import PrivateKey
from helper import decode_base58, SIGHASH_ALL
from script import p2pkh_script, Script
from tx import TxIn, TxOut, Tx
#트랜잭션 주소는 위 탐색기에서 확인할 수 있다.
prev_tx = bytes.fromhex('8122e811e3b60e21cf3cc06ab64f6524b12a7d1fad9b9950d0dec4350d9b7fae')
prev_index = 0
#받는 사람의 주소와 비트코인 수량
target_address = 'ms1qKu2sTqwogBLa8JkrvwjjNdMFQTpM6z'
target_amount = 0.001
#잔돈을 돌려받을 주소와 수량
change_address = 'n3ENFLUALy4zmy9Yjhn14XciVbmbmNSiXr'
change_amount = 0.0009
#입력값 생성
tx_ins = []
tx_ins.append(TxIn(prev_tx, prev_index))
#출력값 생성 : 수신자
tx_outs = []
#Base58 decoding을 통해 받는 사람 주소의 해시값으로 변환한다.
h160 = decode_base58(target_address)
#P2PKH 스크립트를 생성한다.
script_pubkey = p2pkh_script(h160)
#금액을 사토시 단위로 변경
target_satoshis = int(target_amount*100000000)
tx_outs.append(TxOut(target_satoshis, script_pubkey))
#출력값 생성 : 잔돈
h160 = decode_base58(change_address)
script_pubkey = p2pkh_script(h160)
change_satoshis = int(change_amount*100000000)
tx_outs.append(TxOut(change_satoshis, script_pubkey))
#트랜잭션 생성
tx_obj = Tx(1, tx_ins, tx_outs, 0, testnet=True)
#서명값 등록과 검증
priv = PrivateKey(15234)
print(tx_obj.sign_input(0,priv))
#트랜잭션 직렬화 출력
print(tx_obj.serialize().hex())
------
True
0100000001ae7f9b0d35c4ded050999bad1f7d2ab124654fb66ac03ccf210eb6e311e82281000000006a47304402203b63c0562b06f9a9b0ce8d07d980f5104e6e5b9f41bc4105eea8a96eb6e137e202202ed09f7ef556fea4aa79afdbe89204d44d4c3cba6eeb81279332267a0801c31f012103e6657f2b7961f60e0790115185ff798461e7d96a2da095b3716290767b300555ffffffff02a0860100000000001976a9147e1fa137065f9d02ba0e7f5304400f45885e1a7288ac905f0100000000001976a914ee2fb0e388003f290885efe73bde79cc2835be5388ac00000000
Python
복사
5.
트랜잭션을 날려보자
•
네트워크를 ‘Bitcoin Testnet’으로 변경한다.
•
트랜잭션을 브로드캐스팅 한다.
6.
탐색기에서 잔액과 트랜잭션을 확인해보자.
•
송신자의 잔액
•
수신자의 잔액