操作码 OP_CHECKSIG 和 SIGHASH 标记

锁定脚本中的 OP_CHECKSIG 操作码,会要求解锁脚本能提供正确的 ECDSA 签名。

验证 ECDSA 签名是否有效,需要三个参数:公钥、签名和被签名的消息。对于 P2PKH 这种最常见的使用了 OP_CHECKSIG 的交易模板:

[签名] [公钥]    OP_DUP OP_HASH160 [公钥哈希] OP_EQUALVERIFY OP_CHECKSIG

脚本中并没有直接提供被签名的消息的内容(P2PK 也是这样)。实际上,解锁脚本中的签名数据,由两部分构成:

  • DER 格式序列化后的 ECDSA 签名
  • SIGHASH 标记

在之前的文章中我们提过:“交易中被签名的消息,是交易本身。更准确的说,是通过 SIGHASH 标记区分的、交易中特定的数据子集”。交易本身在签名和验签时是已知的,也就是说,虽然脚本中没有直接提供消息的内容,但存储了能间接推算出消息内容的 SIGHASH 标记,它可以指示 OP_CHECKSIG 如何根据交易本身计算出这个消息(交易摘要),进而完成 ECDSA 的验签

这篇文章,将介绍操作码 OP_CHECKSIG 的工作方式,以及 SIGHASH 标记的技术细节。

交易摘要

序列化后的交易结构,如下图所示。

根据 BIP-143 的定义,对于某个交易输入,它要签名的消息,一共由 10 个部分构成:

  1. nVersion of the transaction,该交易的版本号,即 nVersion(4 字节小端)
  2. hashPrevouts (32 字节)
  3. hashSequence(32 字节)
  4. outpoint,该输入引用的交易输出,即 TxOutHash(32 字节小端) 和 TxOutIndex(4 字节小端)的拼接
  5. scriptCode of the input,该输入的锁定脚本,也就是这个输入在之前作为交易输出时的 scriptLen 和 scriptPubKey 的拼接
  6. value of the output spent by this input,该输入的金额,也就是这个输入在之前作为交易输出时的 Value(8 字节小端)
  7. nSequence of the input,该输入的 nSequence(4 字节小端)
  8. hashOutputs(32 字节)
  9. nLocktime of the transaction,该交易的 nLocktime(4 字节小端)
  10. sighash type of the signature,该输入的 SIGHASH 标记(4 字节小端)

让我们先略过 [2]、[3]、[8] 项,只看剩下的部分。你会发现:

  • [5]、[6] 两项对应当前输入在前序交易中作为输出时的状态
  • [4]、[7] 两项对应当前输入在当前交易中作为输入时的状态。请注意这里是不包含解锁脚本的,因为签名最终会放到解锁脚本中,要签名的消息不能包含签名数据本身
  • [10] 对应当前输入的 SIGHASH 标记
  • [1]、[9] 两项对应当前交易的状态

SIGHASH 标记

SIGHASH 标记一共有 6 种,由 4 个不同的项组合而成。

SIGHASH_ALL 0x01
SIGHASH_NONE 0x02
SIGHASH_SINGLE 0x03
ANYONECANPAY 0x80

其中的 ANYONECANPAY 项可以跟其它三项自由结合,所以能得到 6 种不同的 SIGHASH 标记。

SIGHASH 标记 解释
SIGHASH_ALL 0x01 对所有输入、所有输出签名
SIGHASH_NONE 0x02 对所有输入、不对任何输出签名
SIGHASH_SINGLE 0x03 对所有输入、跟当前输入有相同序号的输出签名
SIGHASH_ALL | ANYONECANPAY 0x81 对当前输入、所有输出签名
SIGHASH_NONE | ANYONECANPAY 0x82 对当前输入、不对任何输出签名
SIGHASH_SINGLE | ANYONECANPAY 0x83 对当前输入、跟当前输入有相同序号的输出签名

不同的 SIGHASH 标记会影响交易摘要中 [2]、[3]、[8] 项的计算结果。它们看起来有些复杂,但其实很好理解。

  • SIGHASH_ALL、SIGHASH_NONE 和 SIGHASH_SINGLE 会对所有的交易输入签名,它们的不同是对交易输出的限制
  • ANYONECANPAY 则是对交易输入限制的放宽。如果包含这个标记,那么只对当前输入签名

通过下面的示意图能清晰的看到交易摘要和 SIGHASH 标记之间的逻辑关系。红色框表示当前是针对哪个输入计算交易摘要,蓝色填充代表计算出的交易摘要中包含了该部分。

总结一下:

  • 交易摘要是针对交易输入来说的,不同的交易输入会对应不同的交易摘要,即使这些输入都使用相同的 SIGHASH 标记,它们的交易摘要也是不同的
  • SIGHASH 标记作用在交易输入上,某个输入应该使用什么标记是由用户需求决定的,所有标记都没有使用限制可以随意选择。例如,你可以使用 SIGHASH_ALL | ANYONECANPAY 募集资金,使用 SIGHASH_NONE 签署空白的支票(任何人都可以添加交易输出)等。但请注意,对于含有多个输入的交易,所有 SIGHASH 标记最终反映到交易上的效果是“叠加”的。交易的一个输入使用 SIGHASH_ALL 另一个输入使用 SIGHASH_NONE 是没有意义的,它跟两个输入都使用 SIGHASH_ALL 效果一样

接下来,让我们看下交易摘要中 [2]、[3]、[8] 项具体的计算方法。

hashPrevouts

  • 如果 SIGHASH 标记不包含 ANYONECANPAY,则按顺序拼接所有输入的 outpoint(即交易摘要的 [4])后做 SHA256 双哈希(double-SHA256)
  • 否则,结果为 32 字节的 0x00

hashSequence

  • 如果 SIGHASH 标记不包含 ANYONECANPAY、SINGLE 和 NONE 三项中的任意一项(只能是 SIGHASH_ALL),则按顺序拼接所有输入的 nSequence 后做 SHA256 双哈希
  • 否则,结果为 32 字节的 0x00

hashOutputs

  • 如果 SIGHASH 标记既不包含 SINGLE 也不包含 NONE(只能是 SIGHASH_ALL 或 SIGHASH_ALL | ANYONECANPAY),则按顺序拼接所有输出(8 字节小端的金额、锁定脚本的长度、脚本的内容)后做 SHA256 双哈希。
  • 如果 SIGHASH 标记包含 SINGLE(只能是 SIGHASH_SINGLE 或 SIGHASH_SINGLE | ANYONECANPAY) 且当前输入的序号小于交易输出的个数,则对跟当前输入有相同序号的输出做 SHA256 双哈希
  • 否则(只能是 SIGHASH_NONE 或 SIGHASH_NONE | ANYONECANPAY),结果为 32 字节的 0x00

交易摘要与交易各部分的关系如下图所示。

结合 SIGHASH 标记示意图,让我们再总结一下:

  • 不论使用哪种 SIGHASH 标记,交易摘要都会包含当前输入([4] ~ [7] 项)
  • 如果包含 ANYONECANPAY,在交易的其它输入发生变化时,交易摘要不受影响([2]、[3] 均为 32 字节的 0x00)。否则,任何输入的改变都会导致交易摘要的计算结果发生变化
  • 不同的 SIGHASH 标记会让 hashOutputs 包含不同的交易输出

实际上,这些规则都是在保证一件事:当交易内容发生不合规则(由 SIGHASH 标记指示)的变化时,交易摘要也要随之改变,从而让输入的签名失效

另一个要注意的点是,表示 SIGHASH 标记只需 1 字节。解锁脚本中签名后紧跟的 SIGHASH 标记是 1 字节,但交易摘要中的 SIGHASH 标记(第 [10] 项)是小端的 4 字节。

FORKID

对 BSV 网络,所有的 SIGHASH 标记都需要带上值为 0x40 的 FORKID 项。

BSV 的 SIGHASH 标记
SIGHASH_ALL | FORKID 0x41
SIGHASH_NONE | FORKID 0x42
SIGHASH_SINGLE | FORKID 0x43
SIGHASH_ALL | FORKID | ANYONECANPAY 0xC1
SIGHASH_NONE | FORKID | ANYONECANPAY 0xC2
SIGHASH_SINGLE | FORKID | ANYONECANPAY 0xC3

你可以参考 bsvlib 里的代码实现,了解更多有关交易摘要计算和 SIGHASH 标记的技术细节。

OP_CHECKSIG 的工作方式

对某个交易输入,当 BVM 执行到 OP_CHECKSIG 操作码时,会要求如下图所示的栈结构:栈顶是公钥,随后是签名。

OP_CHECKSIG 会先从栈中弹出公钥和签名,再根据该输入的 SIGHASH 标记计算交易摘要,然后完成 ECDSA 的验签

这没有什么新鲜的东西,但有一点需要注意:OP_CHECKSIG 并不在意签名是怎么来的。它总是从栈上获取公钥和签名,然后根据当前交易为每个输入计算交易摘要并验签。至于这个签名是解锁脚本里直接提供的还是通过其它方式计算出来后再上栈的,并不重要。这是一个非常有意思的特性,也是 OP_PUSH_TX 技术的基础。

参考