前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >区块链系统化探索:什么是”交易“

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

作者头像
望月从良
发布2023-09-02 10:47:01
1470
发布2023-09-02 10:47:01
举报
文章被收录于专栏:Coding迪斯尼Coding迪斯尼

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

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

在区块链的“交易”概念中包含 4 个部分,分别为版本,输入,输出和锁定时间。“版本”用来记录交易的功能范围,想想 windows3.1 和 windows11 这两个系统版本所提供功能的差异,版本号越大意味着功能越强大。输入指的是消耗的比特币数量,输出指的是花掉的比特币给了谁,也就是被花掉比特币的接受者,锁定时间指的是交易何时能生效。下面我们用代码来实现交易这个概念:

代码语言:javascript
复制
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

下面我们给出一段区块链交易对应的二进制数据,我将使用{}把要解析的字段标注出来,如果字段还分子字段,那么我会使用[]标注出来,我们先看数据:

代码语言:javascript
复制
'{01000000}01813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d10000000\
06b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02\
207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631\
e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef0100000000\
1976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc\
762dd5423e332166702cb75f40df79fea1288ac19430600

在上面数据中,{}标出来的部分就是交易中有关版本号的字段。它占 4 个字节,以小端编码的方式存储,因此我们解析时需要将其倒转过来变成00000001,由此我们修改上面代码来解读交易数据的版本号如下:

代码语言:javascript
复制
 @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 接口看看执行结果:

代码语言:javascript
复制
hex_transaction = ''
stream = BytesIO(bytes.fromhex(hex_transaction))

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

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

代码语言:javascript
复制
tx version is :1

下面我们看输入部分。输入相当于别人对你的转账,输出相当于你从自己的账号中转钱给别人。因此你必须有钱进入账户,你才能有钱转出账户。一次转账可能有多笔,因此交易中的输入数据可能需要分成多部分进行解读。假设你卖了一本书,顾客支付给你 50 块,如果他一次性转账给你 50,那么输入就只有 1 笔,如果他分三次转账,第一次 30,接下来两次转 10 块,那么输入就有 3 笔,更极端的是,如果他一次给你转 1 毛,那么这次交易的输入就有 500 笔,因此我们在解读输入数据时需要先读取这次输入有多少笔数据。

我们看看上面二进制数据中与输入相关部分:

代码语言:javascript
复制
'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 个字节来表示,我们看看具体实现:

代码语言:javascript
复制
@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

上面代码运行后输出结果如下:

代码语言:javascript
复制
tx version is :1
num for inputs is :1

另外我们实现写入变量整形的操作,代码如下:

代码语言:javascript
复制
   @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 字节。执行脚本是比特币对应的智能合约代码,它可以被执行,其内容我们后面再探讨。这部分是可变长,因此它需要一个变量整形来标记其长度。最后中本聪设计序列号的作用是实现高频交易,但这个设计存在严重漏洞,交易部分对应二进制数据如下,我用{}标记出来:

代码语言:javascript
复制
0100000001
{ 813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d10000000
06b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02
207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631
e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff }
02a135ef0100000000
1976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc
762dd5423e332166702cb75f40df79fea1288ac19430600

下面我们实现输入对象,首先我们给出它的基本框架:

代码语言:javascript
复制
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 接口,增加解析输入对象的代码:

代码语言:javascript
复制
 @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

然后我们继续运行代码进行测试,运行后输出结果如下:

代码语言:javascript
复制
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 个字节。第二个字段脚本公钥用于获取执行支付脚本的权限,只有资产的所有人私钥能对应到这个公钥然后获得脚本的执行权限。

在上面给的数据例子中,我们将用{}把输出标注出来:

代码语言:javascript
复制
0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d10000000
06b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02
207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631
e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff0 
{
2a135ef0100000000
1976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac
}
99c39800000000001976a9141c4bc
762dd5423e332166702cb75f40df79fea1288ac19430600

我们看看 输出的基本框架:

代码语言:javascript
复制
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 方法:

代码语言:javascript
复制
 @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 字节,位于交易数据的末尾:

代码语言:javascript
复制
'0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac{19430600}

我们看看如何解析该字段:

代码语言:javascript
复制
@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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-08-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Coding迪斯尼 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档