Систематическое исследование блокчейна: что такое «транзакция»

Наиболее важной целью блокчейна является реализация передачи стоимости. По сути, это публикация и хранение информации. Например, если я хочу иметь на лицевой стороне сто долларов, то мне нужно вынуть купюру в 100 юаней, которая является просто подтверждением информации «У меня есть сто долларов». Теперь у всех нас есть электронные платежи, поэтому информация «У меня есть 100 юаней» стала числом в кошельке WeChat или Alipay Yu'ebao. Вы показываете ее другим, и когда они видят это число, они считают, что у вас есть это значение.

Тогда суть транзакции по сути заключается в смене информации. Ты взял на еду 50 из ста. Тогда эта информация становится «В настоящее время у вас есть 50 юаней, и владелец отеля увеличил их на 50 юаней». Поскольку эту информацию могут подтвердить все, нам не нужно вынимать банкноты или цифры в электронном виде. кошелек, чтобы доказать это. «Транзакция» блокчейна заключается в том, чтобы записать это изменение информации, а затем позволить всем участникам точно получить эту информацию.

Концепция «транзакции» блокчейна состоит из четырех частей: версия, ввод, вывод и время блокировки. «Версия» используется для записи функционального объема транзакции. Подумайте о разнице между функциями, предоставляемыми двумя версиями системы: windows3.1 и windows11. Чем больше номер версии, тем мощнее функция. Входные данные относятся к количеству потребленных биткойнов, выходные данные относятся к тому, кому переданы потраченные биткойны, то есть получателю потраченных биткойнов, а время блокировки относится к тому, когда транзакция вступит в силу. Давайте воспользуемся кодом для реализации концепции транзакции:

import hashlib

def hash256(s):
    # 连续进行两次sha256运算
    return hashlib.sha256(hashlib.sha256(s).digest()).digest()

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 f"tx: {
      
      self.id()}\n{
      
      self.version}\n: tx_ins:{
      
      tx_ins}\n tx_outs:{
      
      tx_outs}\n locktime:{
      
      self.locktime}\n"

    def id(self):
        # 每个交易都有专门的 id,这样才能进行查询
        return self.hash().hex()
    
    def hash(self):
        return hash256(self.serialize())[::-1]
    
    
    def serialize(self):
        # 以后再具体实现类的序列化
        return f"Tx:{
      
      self.version}"
    
    @classmethod
    def parse(cls,  stream):
        # 将序列化数据转为类实例,以后再实现
        return None

Ниже мы приводим фрагмент двоичных данных, соответствующий транзакции блокчейна. Я буду использовать {}, чтобы выделить поле для анализа. Если поле все еще является подполем, я буду использовать [], чтобы выделить его. Давайте посмотрим на данные сначала:

'{
    
    01000000}01813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d10000000\
06b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02\
207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631\
e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef0100000000\
1976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc\
762dd5423e332166702cb75f40df79fea1288ac19430600

В приведенных выше данных часть, отмеченная {}, представляет собой поле, связанное с номером версии в транзакции. Он занимает 4 байта и хранится в кодировке с прямым порядком байтов, поэтому нам нужно перевернуть его, чтобы он стал 00000001 при анализе, поэтому мы модифицируем приведенный выше код для интерпретации номера версии данных транзакции следующим образом:

 @classmethod
    def little_endian_to_int(cls, b):
        # 读入数据流读入 4 字节,将其以小端方式存储,然后解读成一个整形 int 数值
        return int.from_bytes(b, 'little')

    @classmethod
    def parse(cls, s):
        #数据流的前 4 个字节是交易的版本号,以小端存储
        version = Tx.little_endian_to_int(s.read(4))
        print(f"tx version is :{
      
      version}")
        return None

Затем мы конвертируем данные транзакции в поток данных io и передаем их в интерфейс синтаксического анализа, чтобы увидеть результат выполнения:

hex_transaction = ''
stream = BytesIO(bytes.fromhex(hex_transaction))

#测试读取版本号
Tx.parse(stream)

Результат после запуска приведенного выше кода следующий:

tx version is :1

Далее мы смотрим на входную часть. Ввод эквивалентен переводу вам кого-то другого, а вывод эквивалентен переводу денег с вашего собственного счета другим. Таким образом, вам необходимо иметь деньги на счете, прежде чем вы сможете перевести деньги со счета. В одной передаче может быть несколько транзакций, поэтому входные данные в транзакции, возможно, придется интерпретировать в нескольких частях. Предположим, вы продаете книгу, и покупатель платит вам 50 юаней. Если он переводит вам 50 юаней за один раз, то вход только один. Если он переводит деньги три раза, первый раз - 30 юаней, а следующий два перевода составляют 10 юаней.Затем есть 3 входа, и еще более крайние, если он переводит вам по 1 центу за раз, то в этой транзакции 500 входов, поэтому нам нужно прочитать, сколько входов есть, когда мы интерпретируем входные данные.

Давайте посмотрим на часть приведенных выше двоичных данных, связанную с вводом:

'01000000 {
    
    
[01]813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d10000000\
06b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02\
207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631\
e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278a
}

feffffff02a135ef0100000000\
1976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc\
762dd5423e332166702cb75f40df79fea1288ac19430600

Часть, заключенная в {} в приведенных выше данных, является входной частью, соответствующей транзакции, а часть, заключенная в [], указывает количество входных данных. Судя по значению 01, указанному выше, в настоящее время существует только 1 вход. Вот проблема, если количество ввода превышает один байт, как это выразить, например, ввести 50 юаней, но другая сторона платит по 1 центу за транзакцию, то транзакций 500, как выразить это значение.

Здесь используется метод кодирования, называемый формированием переменных. Если значение меньше 253, мы можем использовать для его представления один байт. Если значение находится между 253 и 2^16 -1, то первому байту присваивается значение 0xfd(253), а затем для его представления используются следующие два байта. Если значение находится между 2^16 и 2^32 -1, то первый байт устанавливается в 0xfe, а затем для его представления используются 4 байта, если значение находится между 2^32 и 2^64 -1, тогда Первый байт имеет значение 0xff, а затем для его представления используются 8 байт. Давайте посмотрим на конкретную реализацию:

@classmethod
    def read_varint(cls, s):
        """
        根据第一个字节读取数据
        如果第一字节小于 0xfd,那么直接读取其数值,
        如果取值 0xfd,则读取后面两字节
        如果取值 0xfe ,读取后面 4 字节
        如果取值 0xff,读取后面 8 字节
        """
        i = s.read(1)[0]
        if i == 0xfd:
            return Tx.little_endian_to_int(s.read(2))
        elif i == 0xfe:
            return Tx.little_endian_to_int(s.read(4))
        elif i == 0xff:
            return Tx.little_endian_to_int(s.read(8))
        else:
            return i

    @classmethod
    def parse(cls, s):
        #数据流的前 4 个字节是交易的版本号,以小端存储
        version = Tx.little_endian_to_int(s.read(4))
        print(f"tx version is :{
      
      version}")
        input_num = Tx.read_varint(s)
        print(f"num for inputs is :{
      
      input_num}")
        return None

После запуска приведенного выше кода результат будет следующим:

tx version is :1
num for inputs is :1

Кроме того, реализуем операцию записи шейпинга переменных, код следующий:

   @classmethod
    def int_to_little_endian(cls, n, length):
        #将给定整形数值以小端格式存储成字节数组
        return n.to_bytes(length, 'little')

    @classmethod
    def encode_varint(cls, i):
        if i < 0xfd:
            return bytes([i])
        if i < 0x10000:
            return b'\xfd' + Tx.int_to_little_endian(i, 2)
        elif i < 0x100000000:
            return b'\xfe' + Tx.int_to_little_endian(i, 4)
        elif i < 0x10000000000000000:
            return b'\xff' + Tx.int_to_little_endian(i, 8)
        else:
            raise ValueError(f'integer too larger: {
      
      i}')

Зная, сколько входов, давайте проанализируем формат входных данных, который состоит из 4 частей:
1. Идентификатор последней транзакции
2. Индекс последней транзакции
3. Соответствующий транзакции сценарий выполнения (scriptSig)
4. Порядковый номер транзакции

Идентификатор последней транзакции является результатом операции hash256 с данными последней транзакции, а его длина составляет 32 байта. Индекс последней транзакции составляет 4 байта. Скрипт выполнения — это код смарт-контракта, соответствующий Биткойну, который может быть выполнен, и его содержание будет обсуждаться позже. Эта часть имеет переменную длину, поэтому для обозначения ее длины требуется переменное целое число. Наконец, Сатоши Накамото разработал серийный номер для реализации высокочастотных транзакций, но в этой конструкции есть серьезные лазейки.Соответствующие двоичные данные части транзакции следующие, которые я пометил знаком {}:

0100000001
{
    
     813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d10000000
06b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02
207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631
e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff }
02a135ef0100000000
1976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc
762dd5423e332166702cb75f40df79fea1288ac19430600

Далее мы реализуем объект ввода, сначала даем его базовую структуру:

class TxIn:
    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 = Script()
        else:
            self.script_sig = script_sig

        self.sequence = sequence

    def __repr__(self):
        return f"{
      
      self.prev_tx.hex()}:{
      
      self.prev_index}"

    @classmethod
    def parse(cls, s):
        #因为它是大端存储,所以数据要倒转过来
        prev_tx = s.read(32)[::-1]
        print(f"prev tx hash: {
      
      prev_tx}")
        prev_index = Tx.little_endian_to_int(s.read(4))
        print(f"prev index for input: {
      
      prev_index}")

        # 解析 script 对象,和 sequence 后面再实现
        script_sig = None
        sequence = 0xffffffff

        return cls(prev_tx, prev_index, script_sig, sequence)

Затем мы модифицируем интерфейс разбора в объекте Tx и добавляем код для разбора входного объекта:

 @classmethod
    def parse(cls, s):
        #数据流的前 4 个字节是交易的版本号,以小端存储
        version = Tx.little_endian_to_int(s.read(4))
        print(f"tx version is :{
      
      version}")
        input_num = Tx.read_varint(s)
        print(f"num for inputs is :{
      
      input_num}")
        inputs = []
        # 解析输入数据
        for _ in range(input_num):
            inputs.append(TxIn.parse(s))
        return None

Затем мы продолжаем запускать код для тестирования, и результат после запуска будет следующим:

tx version is :1
num for inputs is :1
prev tx hash: b'\xd1\xc7\x89\xa9\xc6\x03\x83\xbfq_?j\xd9\xd1K\x91\xfeU\xf3\xde\xb3i\xfe]\x92\x80\xcb\x1a\x01y?\x81'
prev index for input: 0

Далее давайте посмотрим на выходную часть транзакции. Так называемый выход — это сумма, которую вы потратили. Например, в транзакции вы потратили 50 юаней, чтобы продать чашку за 20 юаней, купить зубную щетку за 10 юаней и купить туалетную бумагу за 10 юаней.Тогда в этой транзакции будет три выхода. Данные в выходной части также начинаются с целочисленной переменной, которая используется для указания количества выходных данных. Каждый выходной объект состоит из двух частей: потраченного значения и открытого ключа скрипта (ScriptPubKey). Единицей, соответствующей стоимости, является 1/00 ​​000 000 биткойнов, и это поле занимает 8 байт. Второе поле — открытый ключ скрипта — используется для получения разрешения на выполнение платежного скрипта. Этому публичному ключу может соответствовать только закрытый ключ владельца актива и последующего получения разрешения на выполнение скрипта.

В приведенном выше примере данных мы пометим вывод значком {}:

0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d10000000
06b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02
207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631
e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff0 
{
    
    
2a135ef0100000000
1976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac
}
99c39800000000001976a9141c4bc
762dd5423e332166702cb75f40df79fea1288ac19430600

Давайте посмотрим на базовый кадр вывода:

class TxOut:
    def __init__(self, amount, script_pubkey):
        self.amount = amount
        self.script_pubkey = script_pubkey

    def __repr__(self):
        return f"{
      
      self.amount}:{
      
      self.script_pubkey}"

    @classmethod
    def parse(cls, s):
        amount = Tx.little_endian_to_int(s.read(8))
        print(f"amount for output is :{
      
      amount}")
        # 获取脚本公钥,后面才实现
        script_pubkey = 0 #Script.parse(s)
        return cls(amount, script_pubkey)

Модифицируем метод разбора в Tx:

 @classmethod
    def parse(cls, s, testnet=False):
        #数据流的前 4 个字节是交易的版本号,以小端存储
        version = Tx.little_endian_to_int(s.read(4))
        print(f"tx version is :{
      
      version}")
        input_num = Tx.read_varint(s)
        print(f"num for inputs is :{
      
      input_num}")
        inputs = []
        # 解析输入数据
        for _ in range(input_num):
            inputs.append(TxIn.parse(s))

        output_nums = Tx.read_varint(s)
        outputs = []
        for _ in range(output_nums):
            outputs.append(TxOut.parse(s))
        return cls(version, inputs, outputs, None, testnet=testnet)

Выполнить код в этот раз мы не сможем, поскольку наш анализ во входе TxInput не был полностью реализован, и завершить текущий код мы сможем только после того, как закончим анализ скрипта выполнения. Давайте посмотрим на последнюю часть LockTime.Сатоши Накамото установил это поле для достижения высокочастотных транзакций, потому что, если данные каждой транзакции необходимо добавить в блокчейн, скорость будет очень медленной.Это поле предназначено для скорости. up но есть лазейки, которые имеют размер 4 байта и располагаются в конце данных транзакции:

'0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac{
    
    19430600}

Давайте посмотрим, как разобрать это поле:

@classmethod
    def parse(cls, s, testnet=False):
        #数据流的前 4 个字节是交易的版本号,以小端存储
        version = Tx.little_endian_to_int(s.read(4))
        print(f"tx version is :{
      
      version}")
        input_num = Tx.read_varint(s)
        print(f"num for inputs is :{
      
      input_num}")
        inputs = []
        # 解析输入数据
        for _ in range(input_num):
            inputs.append(TxIn.parse(s))

        output_nums = Tx.read_varint(s)
        outputs = []
        for _ in range(output_nums):
            outputs.append(TxOut.parse(s))
            
        
        #最后 4 字节对应 locktime
        locktime = Tx.little_endian_to_int(s.read(4))
        return cls(version, inputs, outputs, locktime, testnet=testnet)

В приведенном выше коде синтаксический анализ скрипта контракта только для блокчейна еще не реализован, поэтому код не может работать плавно. Давайте посмотрим, как с этим справиться в следующем разделе.

Guess you like

Origin blog.csdn.net/tyler_download/article/details/132348280