3️⃣

트랜잭션

트랜잭션의 구성

우리가 배운 바에 의하면 트랜잭션의 구성은 다음과 같이 요약할 수 있다.
트랜잭션을 구성하는 요소는 다음과 같다.
크기
필드
설명
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.
탐색기에서 잔액과 트랜잭션을 확인해보자.
송신자의 잔액
수신자의 잔액