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

比特币的交易,由一个或多个输出和一个或多个输入(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,引起错误

所以,字节流传输时,还需要定义字节的排列模式,这是另一个很有意思的话题,称为字节序(Endianness),下面是一些资料和讨论

比特币系统中,除了解锁脚本和锁定脚本,其他部分均使用小端模式编码,认为先收到的字节为数据的低位字节

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

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

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

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

通过下面的过程,计算交易的哈希

  1. 对序列化后的交易数据做SHA256运算,得到S1
  2. S1SHA256运算,得到S2
  3. 按字节翻转S2,得到交易的哈希

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

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

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

点下面的链接,体验一下计算交易哈希的过程

  1. 对序列化后的交易数据做SHA256,得到S1dda380359b9d149fbc48d95aebbbe59117d91fb19e00d13f8992b38ada9654be
  2. S1SHA256,得到S2f2c245c38672a5d8fba5a5caa44dcef277a52e916a0603272f91286f2b052706
  3. 按字节翻转S2,得到交易的哈希0627052b6f28912f2703066a912ea577f2ce4da4caa5a5fbd8a57286c345c2f2

另一个需要注意的点是,Coinbase交易虽然不需要输入,但结构上输入数组仍然存在(长度为1),只不过各字段都被置成了特殊值,用来标识

  • 引用的交易哈希全为0
  • 引用的输出序号全为f
  • 解锁脚本的长度为2~100字节
  • 解锁脚本的内容,随意
  • nSequence的值为0xffffffff

交易d0ec21e1d73d06be76c2b5b1e5ec486085bda8264229046c11b95f66f2eded83是一笔Coinbase交易

01000000
01 <== 输入数组的长度为1
0000000000000000000000000000000000000000000000000000000000000000 <== 引用的交易哈希全为0
ffffffff <== 引用的输出序号全为f
45
03ec59062f48616f4254432f53756e204368756e2059753a205a6875616e67205975616e2c2077696c6c20796f75206d61727279206d653f2f06fcc9cacc19c5f278560300
ffffffff
01
529c6d9800000000
19
76a914bfd3ebb5485b49a6cf1657824623ead693b5a45888ac
00000000

One more thing

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

60e3160000000000 19 76a914ab68025513c3dbd2f7b92a94e0581f5d50f654e788ac

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

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

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

参考