Fork me on GitHub

比特币那些事(2)——交易

比特币(1)——入门 一文中,我们简要介绍了比特币系统是如何通过 未花费的交易输出(Unspent Transaction Outputs, UTXO)来限制交易的支付者就是交易的发起方。

本文我们来详细探讨一下比特币的交易组成及其验证过程。

概述

交易是比特币系统中最重要的部分。根据比特币系统的设计原理,系统中的所有模块都是服务于交易的,这些模块可以创建、传播、验证交易,并最终将交易写入比特币区块链。

举例

下面,我们以一个具体的例子来介绍交易。

Alice 在 Bob 的咖啡店购买了一杯咖啡,为此,Alice 支付了 0.015 比特币。这笔交易在区块浏览器中显示的内容如下所示(点击查看Alice的交易):

事实上,实际的交易数据与区块浏览器所显示的内容完全不同。我们通过对 Alice 的交易进行解码,可以得到如下所示的实际交易数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"version": 1,
"locktime": 0,
"vin": [
{
"txid":"7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18",
"vout": 0,
"scriptSig": "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf",
"sequence": 4294967295
}
],
"vout": [
{
"value": 0.01500000,
"scriptPubKey": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG"
},
{
"value": 0.08450000,
"scriptPubKey": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG",
}
]
}

交易输入输出

比特币的交易与复式记账类似,下图所示为复式记账。每一笔交易包含一个或多个“输入”,输入是针对支付方的比特币账号。与此同时,每一笔交易还包含一个或多个输出,输出是针对接收方的比特币账号。

输入和输出的总额不必相等。相反,当输出总和小于输入总和时,两者的差值则代表了一笔交易手续费。

在比特币系统中,交易输出 是创建交易的核心元素。交易输入 本质上也是基于交易输出。比特币区块链中记录了所有的交易,每一个交易包含了多个交易输入和交易输出,这些数据都被记录在比特币区块链中,完整的比特币节点可以查询并验证所有的历史交易记录。

交易输出

交易输出根据是否被引用过可以分为两种类型:

  • 未花费交易输出(Unspent Transaction Output,UTXO)
  • 已花费交易输出(Spent Transaction Output,STXO)

一个 UTXO 只能被一个交易引用,当该交易写入区块链后,该 UTXO 就转换成了 STXO,无法再被其他交易引用。

下面,我们来看一下交易输出到底是什么?以上述例子为例,交易输出位于 vout 数组中。

1
2
3
4
5
6
7
8
9
10
"vout": [
{
"value": 0.01500000,
"scriptPubKey": "OP_DUP OP_HASH160 ab68025513c3dbd2f7b92a94e0581f5d50f654e7 OP_EQUALVERIFY OP_CHECKSIG"
},
{
"value": 0.08450000,
"scriptPubKey": "OP_DUP OP_HASH160 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8 OP_EQUALVERIFY OP_CHECKSIG",
}
]

vout 数组中的每一项都是一个交易输出。由此可见,每个交易输出包含两个值:

  • value:指交易输出指定可用的比特币数量(最小单位是“聪”,satoshi)。
  • scriptPubKey:指交易输出作为 UTXO 被引用时,交易的创建者需要满足的条件,或者说是需要解决的 加密难题(cryptographic puzzle)。

加密难题也称为 锁定脚本(Locking Script)见证脚本(Witness Script)脚本公钥(ScriptPubKey)。事实上,交易输出在创建时就已经通过 scripPubKey 指明了可以使用它的账户。

所以说,当用户的钱包“收到”比特币时,指的就是钱包已经检测到了该账户可用的 UTXO。而用户的比特币“余额”指的就是该账户可用的 UTXO 总和。这些 UTXO 可能分散在数百个交易中。

那么, scriptPubKey 到底包含了什么信息呢?我们后面会继续介绍。

交易输入

交易输入本质上还是基于交易输出,它通过引用 UTXO 将其标记为 已花费,并通过 解锁脚本 提供对 UTXO 的所有权。

我们仍然以上述列子为例,该交易的交易输入位于 vin 数组中。

1
2
3
4
5
6
7
8
"vin": [
{
"txid": "7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18",
"vout": 0,
"scriptSig" : "3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813[ALL] 0484ecc0d46f1918b30928fa0e4ed99f16a0fb4fde0735e7ade8416ab9fe423cc5412336376789d172787ec3457eee41c04f4938de5cc17b4a10fa336a8d752adf",
"sequence": 4294967295
}
]

vin 数组的每一项都是一个交易输入。每个交易输入包含几个部分:

  • txid:所引用的 UTXO 所在的交易ID(哈希值)。
  • vout:所引用的 UTXO 在其交易输出中的序号。
  • scriptSig:解锁脚本,与锁定脚本进行配对。
  • sequence:序列号。

仅看交易输入,我们无法直接获取交易输入所引用的 UTXO 的内容。因此,比特币节点会检索整个区块链来查找被引用的 UTXO,从而对本交易进行验证。一旦发现区块链中存在某个交易的交易输入已经引用了该 UTXO,那么说明本交易的交易数据已经不是 UTXO 了,而是 STXO,因此本交易无法通过验证。

那么,scriptSign 到底包含了什么信息呢?解锁脚本和锁定脚本是如何进行验证的呢?

下面,我们来介绍锁定脚本和解锁脚本。

交易脚本

比特币包含两种交易脚本:

  • 锁定脚本
  • 解锁脚本

锁定脚本指出了交易输出作为 UTXO 被引用时,交易的创建者需要满足的条件。由于锁定脚本往往含有一个公钥或比特币地址(公钥哈希值),所以也被称为 脚本公钥(ScriptPubKey)。

解锁脚本则是与锁定脚本相匹配的脚本,可以满足锁定脚本所设置的花费条件。由于解锁脚本包含一个数字签名,因此也被称为 脚本签名(ScriptSig)。

当比特币节点对一笔交易进行验证时,交易验证引擎会进行以下几个步骤:

  1. 通过交易输入的 scriptSig 获取到解锁脚本
  2. 通过交易输入的 txidvout 获取到对应的 UTXO
  3. 通过 UTXO 的 scripPubKey 获取到锁定脚本
  4. 将解锁脚本和锁定脚本组合成组合验证脚本
  5. 执行组合验证脚本
  6. 根据执行结果判断交易验证是否通过,即判断解锁脚本是否满足锁定脚本设置的条件

下图所示为最常见的比特币交易的解锁脚本和锁定脚本的示例,它们在交易验证时会组合成组合验证脚本。

脚本语言

交易脚本使用一种类似 Forth 的逆波兰表达式的基于堆栈的脚本语言。

脚本语言包含许多操作码,这些操作码并不包含 循环或复杂流控制 的能力,从而保证了脚本语言的图灵非完备性。
脚本语言的图灵非完备性,意味着脚本只有有限的复杂性和可预见的执行次数,这样可以确保该语言不被用于创造无限循环或其他类型的逻辑黑洞。

执行堆栈

脚本语言是基于堆栈的语言,因为它使用一种被称为堆栈的数据结构。

脚本语言通过从左到右处理每个项目来执行脚本。数据项被 Push 到堆栈上。操作码会先从堆栈中 Pop 多个参数,对参数进行操作后,可能会将结果 Push 到堆栈中。如:操作码 OP_ADD 会从堆栈中 Pop 两个数据项,对其求和后将结果 Push 到堆栈中。

如下所示,是一个简单的脚本执行堆栈示意图。脚本 2 3 OP_ADD 5 OP_EQUAL 演示了算术加法操作码 OP_ADD,该操作码将两个数据项相加,然后把结果 Push 到堆栈,操作码 OP_EQUAL 则验算之前的两数据项之和是否等于 5。脚本执行完后,堆栈顶部的结果为 true,则表示交易验证通过。

事实上,上述执行是一个组合验证脚本,即解锁脚本和锁定脚本的组合。对应,解锁脚本和锁定脚本可能分别是:

1
2
3
4
5
// 解锁脚本
2

// 锁定脚本
3 OP_ADD 5 OP_EQUAL

注意:最初版本的比特币客户端中,解锁脚本和锁定脚本就是按照上述方式组合后再执行。不过,这种方式存在一定的安全隐患:异常的解锁脚本推送数据入栈可能会污染锁定脚本。为了提高安全性,2010 年比特币客户端的脚本执行方案发生了变化:解锁脚本和锁定脚本各自独立执行。首先执行解锁脚本,如果解锁脚本在执行过程中未报错(如:没有悬挂操作码),则复制主堆栈,并执行锁定脚本。

P2PKH 脚本

P2PKH(Pay-To-Public Key Hash)是一种特定的交易脚本类型(解锁脚本和锁定脚本均使用这种类型)。比特币中的大多数交易都采用了 P2PKH 脚本。P2PKH 锁定脚本将输入锁定为一个公钥哈希值,即比特币地址。由 P2PKH 锁定脚本可以通过提供一个公钥和和由相应私钥创建的数字签名来解锁。其原理的本质如下图所示。

以 Alice 向 Bob 咖啡馆支付 0.015 比特币为例,该交易的交易输出的锁定脚本如下:

1
OP_DUP OP_HASH160 <Cafe Public Key Hash> OP_EQUALVERIFY OP_CHECKSIG

其中,<Cafe Public Key Hash> 为 Bob 咖啡馆的比特币地址,暗示了本交易的此 UTXO 只允许 Bob 咖啡馆才能使用。

上述锁定脚本对应的解锁脚本如下:

1
<Cafe Signature> <Cafe Public Key>

当后续 Bob 咖啡馆向引用此 UTXO 时进行其他交易时,可以使用该解锁脚本。

将两个脚本结合起来可以形成如下的组合验证脚本:

1
<Cafe Signature> <Cafe Public Key> OP_DUP OP_HASH160 <Cafe Public Key Hash> OP_EQUALVERIFY OP_CHECKSIG

其执行堆栈如下所示,只有当解锁脚本得到了咖啡馆的有效签名,交易执行结果才会被通过(结果为真),该有效签名是从与公钥哈希相匹配的咖啡馆的私钥中所获取的。

多重签名脚本

由上述 P2PKH 脚本的机制可知,当用户丢失了私钥,就无法再花费对应地址的比特币。为了避免一个私钥的丢失导致资产冻结。比特币引入了多重签名机制,以分散风险。多重签名脚本就是具体的实现方案(也称 M-N 方案),即锁定脚本记录了 N 个公钥,解锁脚本必须提供了至少 M 个签名才能花费创建交易。

M-N 多重签名脚本的锁定脚本的一般形式如下所示:

1
M <Public Key 1> <Public Key 2> ... <Public Key N> N CHECKMULTISIG

假设有一个 2-3 多重签名脚本,那么它的锁定脚本、解锁脚本将如下所示:

1
2
3
4
5
// 锁定脚本
2 <Public Key A> <Public Key B> <Public Key C> 3 CHECKMULTISIG

// 解锁脚本:可以是任意两个签名
<Signature B> <Signature C>

这里我们考虑一个问题:如果 N 特别大,即允许的签名数量非常多,会产生什么问题?很显然,锁定脚本会特别长,占有的容量也会特别大。一般而言,锁定脚本是由支付方生成的;解锁脚本是由接收方生成的。为了将减小支付方的压力,P2SH 脚本出现了。(其实这里我也不是很明白,为什么要转移压力)

P2SH 脚本

P2SH(Pay-to-Script-Hash)是一种新型的、可简化复杂交易脚本的脚本类型。P2SH 主要用于解决多重签名脚本的锁定脚本冗长的问题。

P2SH 脚本提出了 赎回脚本(Redeem Script)的概念,其内容本质上与多重签名脚本的锁定脚本一致。相应地,P2SH 脚本的锁定脚本和解锁脚本也发生了一定的变化。下面我们以一个简单的例子来对比 P2SH 和多重签名脚本。

多重签名脚本

1
2
3
4
5
// 锁定脚本
2 PubKey1 PubKey2 PubKey3 PubKey4 PubKey5 5 CHECKMULTISIG

// 解锁脚本
Sig1 Sig2

P2SH 脚本

1
2
3
4
5
6
7
8
// 赎回脚本 redeem script
2 PubKey1 PubKey2 PubKey3 PubKey4 PubKey5 5 CHECKMULTISIG

// 锁定脚本
HASH160 <20-byte hash of redeem script> EQUAL

// 解锁脚本
Sig1 Sig2 <redeem script>

关于 P2SH 脚本的验证。比特币系统节点首先会将赎回脚本和锁定脚本进行对比,确认赎回脚本的哈希值是否与锁定脚本中的哈希值一致。

1
<2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG> HASH160 <redeem scriptHash> EQUAL

如果哈希值一致,解锁脚本会被执行以释放赎回脚本。

1
<Sig1> <Sig2> 2 PK1 PK2 PK3 PK4 PK5 5 CHECKMULTISIG

总结

至此,我们知道了比特币系统是如何验证交易的。本质上,交易验证充分的应用了密码学的相关原理。

参考

  1. 《精通比特币》
  2. 《区块链开发指南》
  3. 脚本验证支持MS(Multiple Signatures)多重签名
欣赏此文?求鼓励,求支持!