[밑바닥비트코인] 6. 트랜잭션(Transaction)

2021. 7. 20. 21:34Blockchain

이번 장에서는 실제로 트랜잭션이 어떻게 구성되는지 살펴보고 

 

트랜잭션을 파싱하고, 직렬화하는 방법에 대해서 알아보도록 하겠다. 

 

 

트랜잭션의 구조


from lib.helper import hash256
from lib.helper import little_endian_to_int, int_to_little_endian, read_varint, encode_varint
from transaction.TxIn import TxIn
from transaction.TxOut import TxOut

class Tx:
    def __init__(self, version, tx_ins, tx_outs, locktime, testnet=False, segwit=False):
        self.version = version
        self.tx_ins = tx_ins
        self.tx_outs = tx_outs
        self.locktime = locktime
        self.testnet = testnet
        self.segwit = segwit
        self._hash_prevouts = None
        self._hash_sequence = None
        self._hash_outputs = None

트랜잭션에는 사용되는 비트코인의 버전과, 트랜잭션 입력(들), 트랜잭션 출력(들), 록타임 등이 존재한다. 

 

트랜잭션 입력은 사용하고자 하는 비트코인의 이전 출처, 즉 내가 어디서 이 비트코인을 받았는지에 대한 정보이다. 

 

TxIn 클래스에서 더 자세한 정보를 담고 있으며, 트랜잭션 출력은 내가 입력에서 가져온 비트코인들을 어디로 보낼 

 

것인지에 대한 정보이다. 마찬가지로 TxOut 클래스에 더 많은 정보가 있다.

 

이외에도 세그윗(segwit) 및 등등이 있는데 이는 이후에 더 자세히 알아보도록 하겠다.

    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: {}\ntx_outs: {}\nlocktime: {}\n{}'.format(
            # self.id(),
            '---- transaction ----',
            self.version,
            tx_ins,
            tx_outs,
            self.locktime,
            '---- information ----',
        )

Tx 클래스는 현재 단계에서는 위와 같이 표현한다. 버전 정보와 입력 트랜잭션들, 출력 트랜잭션들, 록타임을 출력한다.

from scripts.Script import Script
from lib.helper import little_endian_to_int, int_to_little_endian

class TxIn:
    def __init__(self, prev_tx, prev_idx, script_sig=None, sequence=0xffffffff):
        self.prev_tx = prev_tx
        self.prev_idx = prev_idx
        if script_sig is None:
            self.script_sig = Script()
        else:
            self.script_sig = script_sig
        self.sequence = sequence

    def __repr__(self):
        return '{}:{}\nScript_sig: {}'.format(
            self.prev_tx.hex(),
            self.prev_idx,
            self.script_sig.serialize().hex()
        )

TxIn 클래스는 Tx 클래스 안에 삽입할 입력 트랜잭션 정보에 대한 클래스이다. 

 

내부에 이전 트랜잭션(해시값), 몇 번째 입력인지, 해제 스크립트, 시퀀스를 담고 있다. 

 

입력에 prev_idx를 표시하는 이유는 내가 이전에 받았던 트랜잭션들 중 몇가지 선별하여 일부의 비트코인을 

 

소모할 수 있는데 이러한 트랜잭션을 특정하기 위해서이다.

from scripts.Script import Script
from lib.helper import little_endian_to_int
from lib.helper import int_to_little_endian

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.serialize().hex())

TxOut 클래스에서는 트랜잭션의 출력에 대한 정보를 담고 있다. amount는 얼마나 많은 비트코인을 보낼 것인지,

 

script_pubkey는 잠금 스크립트를 의미한다. 

 

 

 

직렬화


 트랜잭션을 직렬화하는 방법을 알아보자. 

 

먼저 Tx 클래스부터 직렬화하자. 

 class Tx:
     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()
        result += int_to_little_endian(self.locktime, 4)
        return result

버전 정보를 4바이트 리틀엔디언으로 표현하고, 몇 개의 TxIn가 있는지 가변길이 정수(varint)로 바꾼다. 

 

가변길이 정수로 바꾸는 이유는 불필요한 바이트 낭비를 줄이기 위해서이다. 앞에 접두부(fx, re, ff ..)를 붙여줌으로써 

 

몇 바이트까지가 길이정보인지 표시한다. 

def encode_varint(i):
    '''encodes an integer as a varint'''
    if i < 0xfd:
        return bytes([i])
    elif i < 0x10000:
        return b'\xfd' + int_to_little_endian(i, 2)
    elif i < 0x100000000:
        return b'\xfe' + int_to_little_endian(i, 4)
    elif i < 0x10000000000000000:
        return b'\xff' + int_to_little_endian(i, 8)
    else:
        raise ValueError('integer too large: {}'.format(i))

그리고나서 각 TxIn 객체마다 직렬화해주고 TxOut도 같은 과정은 거친다. 

 

마지막으로 록타임(locktime)도 4바이트의 리틀엔디언으로 표현한다. 

 

이제 TxIn을 직렬화하는 방법을 살펴보자. 

class TxIn:
    def serialize(self):
        result = self.prev_tx[::-1] ##트랜잭션 ID는 이미 hex값이라 거꾸로하면 리틀엔디언이 됨
        result += int_to_little_endian(self.prev_idx, 4) #int형 이전 트랜잭션 인덱스를 리틀엔디언으로 변환
        result += self.script_sig.serialize() #해제 스크립트를 자체적으로 직렬화
        result += int_to_little_endian(self.sequence, 4) #int형 시퀀스를 리틀엔디언으로 변환
        return result

이전 트랜잭션(id)를 리틀엔디언으로 만들어주고, prev_idx도 4바이트 리틀엔디언으로 바꿔준다. 

 

해제 스크립트를 직렬화하고, 시퀀스를 4바이트 리틀엔디언으로 변환한다. 

class TxOut:
    def serialize(self):
        result = int_to_little_endian(self.amount, 8)
       	result += self.script_pubkey.serialize()
        return result

TxOut도 비슷하게 amount와 잠금 스크립트를 직렬화하여 반환한다. 

 

 

 

파싱(Parsing)


이제 직렬화된 트랜잭션을 파싱하는 과정을 알아보자.

class Tx:
    @classmethod
     def parse(cls, strm, testnet=False):
         version = little_endian_to_int(strm.read(4))
         num_inputs = read_varint(strm)
         print('input은 총 : ',num_inputs)
         inputs = []
         for _ in range(num_inputs):
             print('input parsing!!')
             inputs.append(TxIn.parse(strm))
    
         num_outputs = read_varint(strm)
         print('output은 총 : ',num_outputs)
         outputs = []
         for _ in range(num_outputs):
             print('output parsing!!')
             outputs.append(TxOut.parse(strm))
         locktime = little_endian_to_int(strm.read(4))
         return cls(version, inputs, outputs, locktime, testnet=testnet)

Tx 클래스를 파싱하면 먼저 4바이트가 리틀엔디언으로 표현된 Version 정보이다. 이를 int형으로 변환한다. 

 

그리고 가변길이 정수로 표현된 트랜잭션 input 개수를 읽어서 해당하는 input 개수만큼 TxIn 객체를 통해 

 

파싱한다. TxOut도 같은 방법으로 파싱한다. 마지막 4바이트는 리틀엔디언으로 표현된 록타임이다. 

 

이를 조합하여 Tx 객체를 반환한다. 

class TxIn:
    @classmethod
    def parse(cls, strm):
        prev_tx = strm.read(32)[::-1]  ##32바이트 리틀엔디언을 읽어들여서 거꾸로 --> 정수형
        prev_idx = little_endian_to_int(strm.read(4))  ## 정수형 4바이트 읽은 리틀엔디언 --> 정수형
        script_sig = Script.parse(strm)
        sequence = little_endian_to_int(strm.read(4))
        return cls(prev_tx, prev_idx, script_sig, sequence)

 TxIn 클래스에서 파싱하는 방법은 먼저 32바이트의 리틀엔디언을 읽어들여서 prev_tx 정보를 가져오고 

 

4바이트 리틀엔디언을 int형으로 읽어서 prev_idx 정보를 가져온다. 해제 스크립트도 Script 클래스의 classmethod를 

 

통해서 파싱하고, 시퀀스 정보도 읽어들여서 이를 취합한 객체를 반환한다. 

class TxOut:
    def parse(cls, strm):
        amount = little_endian_to_int(strm.read(8))
        script_pubkey = Script.parse(strm)
        return cls(amount, script_pubkey)

 

 

 

 

테스트


from transaction.Tx import Tx
from selenium import webdriver
from io import BytesIO

class TxFetcher:
    @classmethod
    def get_url(cls, testnet=False):
        if testnet:
            url = 'https://tbtc.bitaps.com/raw/transaction/'
        else:
            url = 'https://btc.bitaps.com/raw/transaction/'
        return url

    @classmethod
    def fetch(cls, tx_id, testnet=False):
        url = cls.get_url(testnet)
        driver = webdriver.Chrome(executable_path='chromedriver')
        url = url+f'{tx_id}'
        driver.get(url=url)

        raw = driver.find_element_by_id('raw-tx').text
        raw = bytes.fromhex(raw)
        driver.quit()

        if raw[4] == 0:
            raw = raw[:4] + raw[6:]
            tx = Tx.parse(BytesIO(raw), testnet=testnet)
            tx.locktime = little_endian_to_int(raw[-4:])
        else:
            tx = Tx.parse(BytesIO(raw), testnet=testnet)
        return tx

selenium의 webdriver를 이용하여 btc.bitaps.com 에 접속한 뒤, 트랜잭션 ID로 검색해서 트랜잭션의 raw data를 

 

가져오는 코드를 작성하였다. 

    tx_ids = ['995d2f67ca799318415fcf9b9a7c571c4d09915eaaf36ab207cc2d19b839004a']

    tx = TxFetcher.fetch(tx_ids[-1], testnet=False)
    print(tx)

    for i in range(len(tx.tx_ins)):
        print(tx.tx_ins[i].script_sig.cmds)
    for i in range(len(tx.tx_outs)):
        print(tx.tx_outs[i].script_pubkey.cmds)

main 함수에 트랜잭션 ID에 해당하는 트랜잭션을 parsing하여 트랜잭션 정보를 출력하고,

 

입력에 있는 모든 해제 스크립트와 출력이 존재하는 모든 잠금 스크립트를 출력하도록 구현하였다. 

tx: ---- transaction ----
version: 1
tx_ins: 9414683ca8ffba2de87f30cae71339458592f9ba6b3d7900ef4109da25cc4dc8:1
Script_sig : 6a4730440220125511f79c2f9545f2ed899904e54b72da961345967bd333ce6cf2b9450ea4f202205997e8cf91dc4c6e2ad1d672d66c046afaa3f34978b4a76e193cf9da55f3b7470121022b87f12d63387ba24bd5d8fc5e87b0a0a0673a606db4327c7985a3d29c89afce

tx_outs: 16861:17a91431c1b1aca4dcc16783d35c23fde8b2a4c871de7587
25260:1976a9147aa8696be3f3dfa6f4363786c681d2070a10a7a988ac

locktime: 0
---- information ----
[b'0D\x02 \x12U\x11\xf7\x9c/\x95E\xf2\xed\x89\x99\x04\xe5Kr\xda\x96\x13E\x96{\xd33\xcel\xf2\xb9E\x0e\xa4\xf2\x02 Y\x97\xe8\xcf\x91\xdcLn*\xd1\xd6r\xd6l\x04j\xfa\xa3\xf3Ix\xb4\xa7n\x19<\xf9\xdaU\xf3\xb7G\x01', b'\x02+\x87\xf1-c8{\xa2K\xd5\xd8\xfc^\x87\xb0\xa0\xa0g:`m\xb42|y\x85\xa3\xd2\x9c\x89\xaf\xce']
[169, b'1\xc1\xb1\xac\xa4\xdc\xc1g\x83\xd3\\#\xfd\xe8\xb2\xa4\xc8q\xdeu', 135]
[118, 169, b'z\xa8ik\xe3\xf3\xdf\xa6\xf467\x86\xc6\x81\xd2\x07\n\x10\xa7\xa9', 136, 172]

Process finished with exit code 0

출력한 바에 따르면, 트랜잭션의 버전 정보는 1이며, 이전 트랜잭션 ID와 idx를 나타내고 있다. 

 

그리고 해제스크립트와 출력 정보(amount, 잠금 스크립트)를 출력한다. 

 

마지막으로 입력의 해제 스크립트와 출력들의 잠금 스크립트의 command 정보를 출력한다. 

출처: https://blockexplorer.one/bitcoin/mainnet/tx/995d2f67ca799318415fcf9b9a7c571c4d09915eaaf36ab207cc2d19b839004a

 

사이트에서 제공하는 정보와 비교해보니 올바르게 해석한 것 같다!