比特币的交易,由一个或多个输出和一个或多个输入(Coinbase 交易是一种特殊情况)构成。
交易的每个输出上,都会附上一个加密难题,定义将来在花费这笔 UTXO 时需要满足的条件。
交易的每个输入上,都要提供一个解锁脚本,解决或满足之前附在这笔 UTXO 上的加密难题或条件,解锁 UTXO 用于支付。
如果你从前面的文章一路看过来,理解了比特币交易的细节,你应该能设计出下面的数据结构。
对交易的每个输出 TxOut,需要有
- 这个 UTXO 的币值
- 锁定脚本
对交易的每个输入 TxIn,需要有
- 这笔 UTXO 来自之前哪笔交易的第几个输出(需要表达交易链条)
- 解锁脚本
对交易,需要有
- 这笔交易的哈希(数据指纹),用于标识和索引这笔交易
- TxIn 数组,表示这笔交易的所有输入
- TxOut 数组,表示这笔交易的所有输出
这样的设计能满足需求,同时又足够精简。这篇文章,介绍比特币交易的数据结构。
输出
/** |
Amount
的类声明在这里。
struct Amount { |
CScript
的类声明在这里。
输入
/** |
COutPoint
的类声明在这里。
/** |
输入有一个 4 字节的nSequence
字段,以后的文章中再说。
交易
/** |
交易有一个 4 字节的nLockTime
字段,以后的文章中再说。
序列化和反序列化
程序中,一般用特定的数据结构,来表示和存储具体的数据,就像上面描述的那样。
这样的数据,方便人们识别和理解,方便程序操作,但不方便在网络上传输。
在传输前需要将数据结构转换成方便网络传输的字节流形式,这个过程称为序列化。
从字节流“恢复”数据成数据结构的形式,这个过程称为反序列化。
举个例子方便理解。我们可以定义下面的数据结构,来表示二十四小时制的时间。
Type Time { |
时、分、秒分别用 4 字节整数表示,20:35:10
可以表示为
Time t; |
注释后面是数据的十六进制表示。在传输数据时,发送
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 00 14
。
换个角度说,如果对方认为先收到的字节为这个数据的低位字节,那他会把这个数据解析成14 00 00 00
,引起错误。
所以,字节流传输时,还需要定义字节的排列模式,这是另一个很有意思的话题,称为字节序(Endianness),下面是一些资料和讨论。
- 理解字节序
- Little Endian, The order of bytes that a computer like to read in.
- What would you change about the Bitcoin protocol?
比特币系统中,除了解锁脚本和锁定脚本,其他部分均使用小端模式编码,认为先收到的字节为数据的低位字节。
如果我们以小端模式来传输刚才的数据,字节流应该是
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 60
,1500000 聪,0.015 比特币19
,后面紧跟的 25 字节是锁定脚本76a914ab68025513c3dbd2f7b92a94e0581f5d50f654e788ac
,锁定脚本的内容
序列化输入
输入序列化后,格式如下。
长度(字节) | 描述 |
---|---|
32 | 引用的交易哈希,UTXO来自哪笔交易 |
4 | 引用的输出序号,UTXO是那笔交易的第几个输出,从0 开始计数 |
1~9 VarInt | 后面紧跟的解锁脚本,有多少字节 |
变长 | 解锁脚本的内容 |
4 | nSequence |
对于下面这个序列化后的交易输入(我加了换行方便识别),
186f9f998a5aa6f048e51dd8419a14d8a0f1a8a2836dd734d2804fe65fa35779 |
可以反序列化为
- 这个输入使用的 UTXO,是交易
7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18
的第 0 个输出 8b
,后面紧跟的 139 字节,是解锁脚本,48304502...8d752adf
,解锁脚本的内容ffffffff
是nSequence
的值
序列化交易
交易由输入和输出构成。交易序列化后,格式如下。
长度(字节) | 描述 |
---|---|
4 | 交易结构的版本 |
1~9 VarInt | 交易包含几个输入,非零正整数 |
变长 | 输入数组 |
1~9 VarInt | 交易包含几个输出,非零正整数 |
变长 | 输出数组 |
4 | nLockTime |
你注意到没有,交易序列化后,没有交易哈希的部分。
只需要对序列化后的交易数据做哈希运算,就可以得到交易的哈希值,这种冗余的信息,并不需要传输。
通过下面的过程,计算交易的哈希。
- 对序列化后的交易数据做 SHA256 运算,得到 S1
- 对 S1 做 SHA256 运算,得到 S2
- 按字节翻转 S2,得到交易的哈希
Alice 去 Bob 的咖啡店支付 0.015 比特币购买咖啡,生成了交易
0627052b6f28912f2703066a912ea577f2ce4da4caa5a5fbd8a57286c345c2f2 |
下面是这笔交易序列化后的样子(我替你加了换行),你能从中找到各个字段的信息吗?
01000000 |
点下面的链接,体验一下计算交易哈希的过程。
- 对序列化后的交易数据做 SHA256,得到 S1 的值
dda380359b9d149fbc48d95aebbbe59117d91fb19e00d13f8992b38ada9654be |
- 对 S1 做 SHA256,得到 S2 的值
f2c245c38672a5d8fba5a5caa44dcef277a52e916a0603272f91286f2b052706 |
- 按字节翻转 S2,得到交易的哈希
0627052b6f28912f2703066a912ea577f2ce4da4caa5a5fbd8a57286c345c2f2 |
另一个需要注意的点是,Coinbase 交易虽然不需要输入,但结构上输入数组仍然存在(长度为 1),输入结构中的各字段也都会被置成特殊值,用来标识。
- 引用的交易哈希全为
0
- 引用的输出序号全为
f
- 解锁脚本的长度为 2 ~ 100 字节
- 解锁脚本的内容,随意
nSequence
的值为ffffffff
交易d0ec21e1d73d06be76c2b5b1e5ec486085bda8264229046c11b95f66f2eded83
是一笔 Coinbase 交易,序列化后内容如下。
01000000 |
One more thing
你有没有发现,序列化规则中,描述脚本长度、数组个数的字段,其长度也是变化的。
60e3160000000000 19 76a914ab68025513c3dbd2f7b92a94e0581f5d50f654e788ac |
这是刚才的例子,前 8 字节60e3160000000000
表示币值是确定的,因为规则定义了币值用 8 个字节表达。
但“锁定脚本的大小”字段,其长度是不确定的,可以用 1 ~ 9 个字节来表达。
为什么我们能确定后面跟着的锁定脚本长度是19
,而不是76 19
?欢迎留言。 😋