区块链系统化探索:什么是”交易“

Python
196
0
0
2024-01-05
标签   区块链

在区块链的目最重要的目的就是实现价值的转移。这本质上是信息的发布和存储。例如我要正面我有一百块钱,那么我需要拿出一张 100 块的钞票,这张纸币只不过是一种“我有一百块”这个信息的证明。现在我们都有电子支付,于是“我有一百块”这个信息就变成了微信钱包或支付宝余额宝里面的一个数字,你拿给别人看,他人看到数字就相信你有这个价值。

那么交易的本质实质上就是信息的变化。你从一百块中拿出 50 吃了顿饭。那么这个信息就变成“你当前拥有 50 块,饭店老板增加了 50 块”,只要这个信息能被所有人确认,那么我们根本用不着拿出纸币或电子钱包里面的数字来证明。区块链的“交易”就是记录这个信息变化,然后让所有参与者都能准确的获得这个信息。

在区块链的“交易”概念中包含 4 个部分,分别为版本,输入,输出和锁定时间。“版本”用来记录交易的功能范围,想想 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 数据流,传入到 parse 接口看看执行结果:

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

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

上面代码运行后所得结果如下:

tx version is :1

下面我们看输入部分。输入相当于别人对你的转账,输出相当于你从自己的账号中转钱给别人。因此你必须有钱进入账户,你才能有钱转出账户。一次转账可能有多笔,因此交易中的输入数据可能需要分成多部分进行解读。假设你卖了一本书,顾客支付给你 50 块,如果他一次性转账给你 50,那么输入就只有 1 笔,如果他分三次转账,第一次 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,上一次交易 ID 2,上一次交易索引 3,交易对应执行脚本(scriptSig) 4,交易序列号

上一次交易 ID是上一次交易数据执行 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 对象中 parse 接口,增加解析输入对象的代码:

 @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 中 parse 方法:

 @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,中本聪设置这个字段的目的在于实现高频交易,因为如果每次交易的数据要加入区块链速度就会非常慢,这个字段是为了加快速度但是却存在漏洞,它的大小为 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)

由于上面代码中,区块链只能合约脚本的解析还没有实现,因此代码还不能顺利运行,下一节我们看看怎么处理。代码下载:https://github.com/wycl16514/blockchain_transation.git