好书/好文 [学习笔记] 比特币交易的数据结构

aaron67 · 发布于 2018年12月28日 · 378 次阅读
本帖已被设为精华帖!

文章首发我的博客,搬运一份到这来


比特币的交易,由一个或多个输出和一个或多个输入(coinbase交易是一种特殊情况)构成

交易的每个输出上,都会附上一个加密难题,定义将来在花费这笔UTXO时需要满足的条件

交易的每个输入上,都要提供一个解锁脚本,解决或满足之前附在这笔UTXO上的加密难题或条件,解锁UTXO用于支付

如果你从前面的文章一路看过来,理解了比特币交易的细节,你应该能设计出下面的数据结构

对交易的每个输出TxOut,需要有

  • 这个UTXO的币值
  • 锁定脚本

对交易的每个输入TxIn,需要有

  • 这笔UTXO来自之前哪笔交易的第几个输出(需要表达交易链条
  • 解锁脚本

对交易,需要有

  • 这笔交易的哈希(数据指纹),用于标识和索引这笔交易
  • TxIn数组,表示这笔交易的所有输入
  • TxOut数组,表示这笔交易的所有输出

这样的设计能满足需求,同时又足够精简

这篇文章,介绍比特币交易的数据结构

输出

代码

/**
 * An output of a transaction.  It contains the public key that the next input
 * must be able to sign with to claim it.
 */
class CTxOut {
public:
    Amount nValue;         // UTXO的币值,8字节整数,单位是聪
    CScript scriptPubKey;  // 锁定脚本

    ......
};

Amount的类声明在这里

struct Amount {
private:
    int64_t amount;

    ......
};

CScript的类声明在这里

输入

代码

/**
 * An input of a transaction. It contains the location of the previous
 * transaction's output that it claims and a signature that matches the output's
 * public key.
 */
class CTxIn {
public:
    COutPoint prevout;  // UTXO的来源,包含一个交易哈希和一个索引号,用来表示哪笔交易的第几个输出
    CScript scriptSig;  // 解锁脚本
    uint32_t nSequence;

    ......
};

COutPoint的类声明在这里

/**
 * An outpoint - a combination of a transaction hash and an index n into its
 * vout.
 */
class COutPoint {
private:
    TxId txid;  // 交易哈希
    uint32_t n; // 输出的序号

    ......
};

输入有一个4字节的nSequence字段,以后的文章中再说

交易

代码

/**
 * The basic transaction that is broadcasted on the network and contained in
 * blocks. A transaction can contain multiple inputs and outputs.
 */
class CTransaction {
public:
    const int32_t nVersion;          // 交易结构的版本标识,4字节
    const std::vector<CTxIn> vin;    // 输入数组
    const std::vector<CTxOut> vout;  // 输出数组
    const uint32_t nLockTime;

    ......

private:
    const uint256 hash;              // 交易的哈希

    ......
};

交易有一个4字节的nLockTime字段,以后的文章中再说

序列化和反序列化

程序中,一般用特定的数据结构,来表示和存储具体的数据,就像上面描述的那样

这样的数据,方便人们识别和理解,方便程序操作,但不方便在网络上传输

在传输前需要将数据结构转换成方便网络传输的字节流形式,这个过程称为序列化

从字节流“恢复”数据成数据结构的形式,这个过程称为反序列化

举个例子,方便理解

我们可以定义下面的数据结构,来表示二十四小时制的时间

Type Time {
    uint32_t hour;
    uint32_t minute;
    uint32_t second;
};

时分秒分别用4字节整数表示,20:35:10可以表示为

Time t;
t.hour = 20;    // 00 00 00 14
t.minute = 35;  // 00 00 00 23
t.second = 10;  // 00 00 00 0a

注释后面是数据的十六进制表示

在传输数据时,发送

00 00 00 14 00 00 00 23 00 00 00 0a

并规定

  • 你会收到12字节的数据
  • 第一个4字节数据是 时
  • 第二个4字节数据是 分
  • 第三个4字节数据是 秒

对方在收到数据后,就能把字节流还原成数据结构的形式

注意到

数据结构不仅包含数据的值,还描述“这是什么数据”

当你看到t.hour = 20,你知道这个数据表示时间中的小时,值为20

但当你看到00 00 00 14,你只知道这个数据的值为20,不知道这是20时,还是20分,还是20

所以,需要定义序列化的规则

另一个不容易注意到的点是,需要多字节表达的数据项(用4字节来表达小时字段)的值,如何在在字节流中排列

上面的例子中,你收到第一个4字节的顺序为

00
00
00
14

这默认了,先收到的字节为这个数据的高位字节,后收到的为低位字节,所以你得到00 00 00 14

换个角度说,如果对方认为先收到的字节为这个数据的低位字节,那他会把这个数据解析成14 00 00 00,引起错误

所以,字节流传输时,还需要定义字节的排列模式,这是另一个很有意思的话题,称为大小端模式,你可以搜些文章读一读

比特币系统中,使用小端模式,认为先收到的字节为数据的低位字节

如果我们以小端模式来传输刚才的数据,字节流应该是

14 00 00 00 23 00 00 00 0a 00 00 00

序列化输出

长度(字节) 描述
8 以聪为单位的币值
1~9 VarInt 后面紧跟的锁定脚本,有多少字节
变长 锁定脚本的内容

这样一个序列化后的交易输出

60e31600000000001976a914ab68025513c3dbd2f7b92a94e0581f5d50f654e788ac

可以反序列化为

  • 60e3160000000000是币值,小端模式,值为00 00 00 00 00 16 e3 601500000聪,0.015比特币
  • 19,后面紧跟的25字节是锁定脚本
  • 76a914ab68025513c3dbd2f7b92a94e0581f5d50f654e788ac,锁定脚本的内容

序列化输入

长度(字节) 描述
32 交易哈希,UTXO来自哪笔交易
4 输出的序号,UTXO是那笔交易的第几个输出,从0开始计数
1~9 VarInt 后面紧跟的解锁脚本,有多少字节
变长 解锁脚本的内容
4 nSequence

这样一个序列化后的交易输入(我加了空格和换行方便识别)

186f9f998a5aa6f048e51dd8419a14d8a0f1a8a2836dd734d2804fe65fa35779
00000000
8b
483045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e381301410484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf
ffffffff

可以反序列化为

  • 这笔UTXO,来自之前的交易7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18的第0个输出
  • 8b,后面紧跟的139字节,是解锁脚本,
  • 48304502...8d752adf,解锁脚本的内容
  • ffffffffnSequence的值

序列化交易

长度(字节) 描述
4 交易结构的版本
1~9 VarInt 交易包含几个输入
变长 输入数组
1~9 VarInt 交易包含几个输出
变长 输出数组
4 nLockTime

你注意到没有,交易序列化后,没有交易哈希的部分

只需要对序列化后的交易数据做哈希运算,就可以得到交易的哈希值,这种冗余的信息,并不需要传输

Alice去Bob的咖啡店支付0.015比特币购买咖啡,生成了交易0627052b6f28912f2703066a912ea577f2ce4da4caa5a5fbd8a57286c345c2f2

下面是这笔交易序列化后的样子(我替你加了一些空格和换行),你能从中找到各个字段的信息吗?

01000000
01
186f9f998a5aa6f048e51dd8419a14d8a0f1a8a2836dd734d2804fe65fa35779
00000000
8b
483045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e381301410484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf
ffffffff
02
60e3160000000000
19
76a914ab68025513c3dbd2f7b92a94e0581f5d50f654e788ac
d0ef800000000000
19
76a9147f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a888ac
00000000

One more thing

你有没有发现,序列化规则中,描述脚本长度、数组个数的字段,其长度也是变化的

60e3160000000000 19 76a914ab68025513c3dbd2f7b92a94e0581f5d50f654e788ac

这是刚才的例子,前8字节60e3160000000000表示币值是确定的,因为规则定义了币值用8个字节表达

但“锁定脚本的大小”字段,其长度是不确定的,可以用1~9个字节来表达

为什么我们能确定后面跟着的锁定脚本长度是19,而不是76 19?欢迎留言 😋

参考

共收到 0 条回复
aaron67 将本帖设为了精华贴 12月28日 02:44
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册