本文将针对收款方如何代付 BSV 交易的矿工费,给出可行的解决方案。
显而易见,用户只有用 ANYONECANPAY 标记签名,商户才能为交易添加 gas 输入。问题的核心是怎么限制交易输出,是 SIGHASH_SINGLE,还是 SIGHASH_ALL。
Alice 去店里买咖啡,需要支付 1 BSV,她会将签好名的付款交易直接发给 Bob,然后等待;Bob 收到 Alice 发来的交易后确认交易有效,然后交付商品,流程结束。在整个交互过程中,Bob 会关心自己是否真的收到了钱,所以他会把 Alice 的交易迅速发给矿工来验钞和“结算”;而 Alice 只关心自己是否能尽快取走咖啡,以及自己的支付找零是否安全。
所以对 Alice 来说,使用 SIGHASH_SINGLE 来限制交易找零十分合理。对下面这笔交易,Alice 会使用 SIGHASH_SINGLE | ANYONECAYPAY 标记来签名以放开对交易其它输入输出的限制,同时能保证找零给自己的输出(蓝色)不可修改。她把这个交易直接发给 Bob,意思是我支付了 1 BSV 给你,其它的事情不用我操心,如果你确认交易没问题,那我就可以拿走咖啡。
Bob 收到这笔交易后,会先添加 1 BSV 咖啡钱的输出(绿色)给自己,然后添加 gas 的输入和找零(红色),并使用 SIGHASH_ALL 标记签名。Bob 最终发给矿工的交易如下图所示。
整个方案很简单,但在工程实现上,还有两个细节需要确认:
如果 Alice 有多个输入,第一个输入使用 SIGHASH_SINGLE 其它输入使用 SIGHASH_NONE 就可以,因为她只关心给自己的找零是否不可修改。
对第二个问题,因为实际支付可能是并发的,当遇到多人同时付款时,Bob 需要保证给不同交易分配的 gas UTXO 是彼此互斥的,否则整个系统就只能排队执行,每个交易都把前一个交易的 gas 找零当做自己的 gas 输入。要解决这个问题也很简单,提前拆分 UTXO 用一个队列管理起来就可以了。之前我在给 WitnessOnChain 加 BSV 测试网水龙头时就是用的这个方案,实测的效果很不错。
使用 SIGHASH_SINGLE 方案的优势十分明显:Bob 可以很灵活的修改交易。例如,他可以每卖出一杯咖啡就向自己的合伙人 Charles 支付一笔钱。
实际上,Bob 都不需要为这个方案提前准备 gas UTXO,他可以直接从咖啡款里支付矿工费。因为 Alice 发来的交易是确定的,他计划为这个交易添加多少输出也是确定的,所以 Bob 能提前精确的算出最终的交易需要多少矿工费。
基于此,如果 Bob 决定这样支付矿工费,那 ANYONECANPAY 也可以去掉,因为他已经不再需要为交易添加其它输入了。
通过这些讨论你能看到,使用 SIGHASH_SINGLE 的好处就是灵活。不管怎样,Bob 都必须保证一点:Alice 发来的交易不能被他人截获,因为任何拿到这笔交易数据的人都可以添加输出取走里面的 1 BSV。但注意,这个要求对 Bob 来说是合理的,因为他有义务更有动力来采取措施以保证自己的资金安全。实际上要解决这个问题也不复杂,点对点发送交易时选择 HTTPS 或者近场通信,就可以大幅提升 MITM 的攻击成本。
那 Alice 使用 SIGHASH_ALL | ANYONECANPAY 签名行不行?也可以,但会损失一部分灵活性。
使用 SIGHASH_ALL 就意味着 Bob 不能添加输出,因为没有 gas 找零,所以 gas 输入必须是精确的。
所以这个方案要解决的问题,是 Bob 如何在每次收款时都能精确的提供 gas 输入。博泉的 CEO 林哲明给出了这个问题的解决办法:每次都现场构造一个交易。
Bob 还是可以用上文提到的方法提前拆分和管理 gas UTXO,但在每次取队列后,都立即花费拿到的 gas UTXO(红色)新生成一个特定金额的输出(黄色)来作为这次付款交易的 gas 输入。因为 Bob 只会对收到的付款交易添加 1 个输入,所以他同样能提前精确的算出最终交易需要多少矿工费。
这个方案相比 SIGHASH_SINGLE 来说更加“安全”,即使 Alice 发送的交易不小心被他人截获,Bob 也不会有任何经济损失。
针对支付 BSV 时可能出现的收款方代付交易矿工费的场景,本文给出了两种切实可行的解决方案,它们各有优劣,并且工程实现都不复杂,欢迎留言。😊
有 12 个外观一样的小球,只知道其中一个球的重量与其它球不同。用一个没有砝码的天平,最坏情况下最少要称几次,才能找到这个重量不一样的小球并确定它是偏轻还是偏重。
正确答案 3 次。
称球问题有很多变化,每次遇到,可能花点时间写写画画就能解决,就像上面这个方案一样,更多的是“灵光一现”,恰好“碰”到了答案。对于各种称球问题,我很好奇到底有没有套路可循。
维基百科给出了下面四类称球问题的通解。
已知条件 | 目标 | 称 $k$ 次最多能有几个球 | 有 $n$ 个球最坏情况下最少要称几次 |
---|---|---|---|
有一个问题球,它偏轻或偏重 | 确定问题球 | $3^k$ | $\lceil log_3{n} \rceil$ |
有一个问题球,它的重量与其它球不同 | 确定问题球 | $\cfrac{3^k - 1}{2}$ | $\lceil log_3(2n + 1) \rceil$ |
有一个问题球,它的重量与其它球不同 | 确定问题球,并确定它是偏轻还是偏重 | $\cfrac{3^k - 3}{2}$ | $\lceil log_3(2n + 3) \rceil$ |
可能有一个问题球,它的重量与其它球不同 | 确定有没有问题球 | $\cfrac{3^k - 3}{2}$ | $\lceil log_3(2n + 3) \rceil$ |
如果你对为什么会是这样感兴趣,请继续阅读。接下来将证明这些结论,并介绍如何构造称球问题的通解方案。
一个球在参与称量之前,它的状态是未知的:可能有问题,也可能没问题。如果某个球有问题,那么还对应两种可能:偏轻或偏重。
在天平两边各放相等数量的球称一次,如果平衡,那么天平上的所有球都正常,有问题的球在剩下的那堆球里。如果不平衡,那么剩下的那堆球都正常,有问题的球在这些参与称量的球里,假设此时天平左轻右重,那么只会是左边的某个球偏轻或右边的某个球偏重。也就是说,一个球在参与称量后,如果不能确定它是正常球,我们也至少能确定它是“可能偏轻”还是“可能偏重”。某个球一旦“上过天平”,就不会出现既“可能偏轻”又“可能偏重”的情况。
例如,称 ① ② 和 ③ ④ 结果天平左轻右重,那么我们能确定问题球在这四个球里,仅凭这个结果还不能确定具体哪三个球正常,但能确定如果 ① 或 ② 是问题球那它就只会偏轻,如果 ③ 或 ④ 是问题球那它就只会偏重。所以如果之后某次称球时又用到了 ① 并且天平放 ① 的那边更重,就可以确定 ① 一定是正常球,因为它如果是问题球就不可能偏重。
对 $n$ 个都“上过天平”的球,最坏情况下最少要称几次,才能确定问题球并能确定它是偏轻还是偏重?倒着想容易一些。
因为这里讨论的球都“上过天平”,所以只要能找到问题球,就等于确定了它是偏轻还是偏重。
若 $n = 1$,显而易见,需要称 0 次。
若 $n = 2$,则需要称 1 次。此时有两种情况:(1)两个球状态相同,都“可能偏轻”或都“可能偏重”;(2)两个球一个“可能偏轻”另一个“可能偏重”。对于(1)直接把这两个球放到天平上称一次即可,谁更轻或更重则谁有问题。对于(2)则需要用到额外的一个正常球[注]:任意拿一个球跟正常球称一次,不平衡则这个球是问题球,平衡则另一个球是问题球,根据问题球在称之前的状态就可以确定它具体是偏轻还是偏重。所以不管怎样,都需要称 1 次。
若 $n = 3$,则需要称 1 次。此时至少有两个球状态一样,要么都“可能偏轻”,要么都“可能偏重”,直接把这两个状态一样的球放到天平上称一次即可。
若 $4 \le n \le 9$,很明显,称 1 次是不行的,用反证法很容易证明(有四个状态相同的球)。那称两次行不行?
我们将所有球尽可能的等分成 A、B、C 三堆,保证每堆球都不超过 3 个,且 A、B 两堆各有相同数量“可能偏轻”的球,也各有相同数量“可能偏重”的球。这总是可以做到的,先从“可能偏轻”的球中任意挑出 $2a$ 个球($a \ge 0$)均分成 A、B 两堆,再从“可能偏重”的球中任意挑出 $2b$ 个球($b \ge 0$)均分到 A、B 中来平衡三堆球的数量,以保证每堆球都不超过 3 个。即保证
$$\begin{cases}1 \le a + b \le 3 \\[0.5em]1 \le n - 2(a + b) \le 3\end{cases}$$将 A、B 两堆球放到天平上称一次。如果平衡,则 A、B 中的所有球都正常,有问题的球一定在 C 里,而 C 里最多 3 个球,根据之前的讨论再称一次就能确定问题球并确定它是偏轻还是偏重。如果 A 轻 B 重,则 A 中所有“可能偏重”的球、B 中所有“可能偏轻”的球和 C 中的所有球都正常,有问题的球只可能是 A 中“可能偏轻”的球或 B 中“可能偏重”的球,共有 $a + b$ 个也不大于 3,只需要再称一次。如果 A 重 B 轻,则 A 中所有“可能偏轻”的球、B 中所有“可能偏重”的球和 C 中的所有球都正常,有问题的球只可能是 A 中“可能偏重”的球或 B 中“可能偏轻”的球,共有 $b + a$ 个也不大于 3,也只需要再称一次。第一次称完后不论结果如何,都只需要再称一次。所以在 $4 \le n \le 9$ 时称 2 次就能确定问题球并能确定它是偏轻还是偏重。
注:按此方案分球,每次称球时如果平衡则能确定天平上的球都正常,如果不平衡则能确定剩下的球都正常。所以不论结果如何,称一次都能确定出至少一个正常球。也就是说,如果总共要称多次,那在最后一次称球时一定已经确定出了至少一个正常球,即额外需要一个正常球这个条件总是能被满足。
至此,我们可以得出结论:在 $n$ 个($n \ge 3$)都“上过天平”的球中确定问题球并确定它是偏轻还是偏重,最坏情况下最少要称 $\lceil log_3{n} \rceil$ 次。请注意,为了严谨,我们限制了 $n \ge 3$,这好像不太优雅,但我们可以反过来说:
这个结论用数学归纳法很好证明。
当 $k = 1$ 时,由上面的讨论可知,最多能从 3 个都“上过天平”的球中确定问题球并确定它是偏轻还是偏重,结论成立。
假设当 $k = i$($i > 1$)时,结论成立,最多可以有 $3^i$ 个都“上过天平”的球。
当 $k = i + 1$ 时,最多有 $3^{i+1}$ 个球,使用“三分法”,我们总是可以将这些球分成三堆,使每堆球都不超过 $3^i$ 个,并同时保证至少有两堆球各有相同数量“可能偏轻”的球,也各有相同数量“可能偏重”的球。此时,称 1 次就可以将问题转化为已经讨论过的情况,不论结果如何,根据假设,再称 $i$ 次就能确定问题球并确定它是偏轻还是偏重,所以总共需要称 $i + 1$ 次,当 $k = i + 1$ 时结论也成立。
由数学归纳法可知,当 $k \ge 1$ 时,结论成立。证毕。
如果是 1 个球,直接拿它跟正常球称 1 次就可以得出结论。
如果是 2 个球,任选一个球跟正常球称 1 次,如果不平衡则找到了问题球并能确定它是偏轻还是偏重;如果平衡则另一个球是问题球,这是结论中仅有的一种情况,我们只能通过“排除”法确定问题球,但无法确定它是偏轻还是偏重。
如果是 3 个球,很明显,称 1 次在最坏情况下无法确定问题球,不论是两边各放一个球还是两边各放两个球(额外还有 1 个正常球)。
所以当 $k = 1$ 时,最坏情况下最多可以从 2 个球中找到问题球,有且只有一种情况无法确定它是偏轻还是偏重,结论成立。
假设当 $k = i$($i > 1$)时,结论成立,最多可以有 $\cfrac{3^i + 1}{2}$ 个球,有且只有一种情况无法确定它是偏轻还是偏重。
当 $k = i + 1$ 时,最多有 $\cfrac{3^{i+1} + 1}{2}$ 个球。考虑第一次称球,天平一边放 $\cfrac{3^i + 1}{2}$ 个球,另一边放 $\cfrac{3^i - 1}{2}$ 个球和 1 个已知的正常球。
$$\frac{3^i + 1}{2} = \frac{3^i - 1}{2} + 1$$
如果不平衡,根据结论一,都“上过天平”的 $\cfrac{3^i + 1}{2} + \cfrac{3^i - 1}{2} = 3^i$ 个球只要称 $i$ 次,就能找到问题球并确定它是偏轻还是偏重。如果平衡,那么问题球在剩下的那堆球中,最多 $\cfrac{3^{i+1} + 1}{2} - 3^i = \cfrac{3^i + 1}{2}$ 个,此时已经有 $3^i$ 个正常球了,根据假设,也只要再称 $i$ 次就能确定问题球,但会有且只有一种情况无法确定它是偏轻还是偏重。所以不论第一次称球结果如何,都只需要再称 $i$ 次,总共要称 $i + 1$ 次,当 $k = i + 1$ 时结论也成立。
由数学归纳法可知,当 $k \ge 1$ 时,结论成立。证毕。
那为什么会有一种情况无法确定问题球是偏轻还是偏重呢?
如果是 2 个球称 1 次:
如果是 5 个球称 2 次:
看到没,最后那个球一直被“剩下”从来都没“上过天平”,我们自始至终都没用到这个球,它有问题是被每次天平都平衡“排除”出来的,有没有它对具体怎么称球都没有影响。在所有可能的称球结果中,有且只有这一种情况,我们只能用“排除法”确定出问题球但无法知道它是偏轻还是偏重。
那如果需要确定问题球并确定它是偏轻还是偏重,怎么办?没有这个球,不就可以了?😂
讨论到现在好像都没解决问题,但细品后你会发现,不论第一次怎么称球,称完后的局面都会变成结论一或结论二中的情况。
有 $n$ 个球($n \ge 3$),在天平两边各放 $p$ 个球称一次($p \ge 1$),还剩下 $q$ 个球没有称($q \ge 0$)。如果平衡则这 $2p$ 个球是正常球,问题球在剩下的 $q$ 个球中:用结论二可解;如果不平衡则剩下的 $q$ 个球是正常球,问题球在这 $2p$ 个球中,它们都是“上过天平”的球:用结论一可解。
所以为了保证方案在最坏情况下称的次数最少,我们只要让这两个结果“分支”所需的称球次数都尽可能少就可以。假设总共要称 $k$ 次来确定问题球并确定它是偏轻还是偏重,对第一次称球,如果:
因此最多共可以有 $3^{k-1} - 1 + \cfrac{3^{k-1} - 1}{2} = \cfrac{3^k - 3}{2}$ 个球。
如果只需要找到问题球而不用确定它是偏轻还是偏重呢?
那平衡“分支”里可以多一个球。每次称球天平都平衡,这个多出来的球会被“排除”成问题球,因为从没“上过天平”所以无法确定它是偏轻还是偏重,也就是结论二中描述的那种仅有的情况。所以此时最多共可以有 $3^{k-1} - 1 + \cfrac{3^{k-1} + 1}{2} = \cfrac{3^k - 1}{2}$ 个球。
至此,我们详细说明了二、三两种类型称球问题的通解,并证明了结论。
对于类型一的称球问题,已知问题球是偏轻或偏重实际上相当于把所有球都变成了“上过天平”的球,它们的状态只会是都“可能偏轻”或都“可能偏重”,所以能直接使用结论一:称 $k$ 次最多可以有 $3^k$ 个球。
对于类型四的称球问题,可能有一个问题球需要确定有没有,实际上跟类型三是完全等价的。为什么只找到问题球不行还要能确定它是偏轻还是偏重?或者说,为什么称 $k$ 次最多不能是 $\cfrac{3^k - 1}{2}$ 个球?因为如果称球时出现了结论中描述的那种仅有的情况,天平一直平衡,就无法确定最后剩下的球是什么状态,这时候不能用“排除法”因为我们事先不知道这堆球里到底有没有问题球。或者说,如果多出来这么个球,就会有一种情况无法确定这堆球里到底有没有问题球,所以不能有这个球:称 $k$ 次最多可以有 $\cfrac{3^k - 3}{2}$ 个球。
上面这些结论的证明都是构造性的,以此为据,可以很容易的写出通解方案。让我们还以 12 个球为例,重新设计方案,称 3 次(绿色、蓝色、红色)从这堆球中确定问题球并确定它是偏轻还是偏重。
根据结论一,第一次要称 $3^2 - 1 = 8$ 个球,称 ① ② ③ ④ 和 ⑤ ⑥ ⑦ ⑧,如果
最后留一道经典题给你思考。称 3 次最多能从 13 个球中确定问题球,但会有一种情况无法确定问题球是偏轻还是偏重,要怎么设计称球方案?如果额外给 1 个正常球,又该如何设计称球方案才能从这 13 个球中确定问题球并确定它是偏轻还是偏重?欢迎在评论区里言。😊
支持用户从合约中提取资金并不复杂,唯一的难点是冷却时间,我们可以使用 non-final 交易来实现。
contract Faucet { |
这个设计非常巧妙。在 nLocktime 指示的时间到来之前,调用交易是 non-final 的,虽然节点会接受和传播它,但这没什么用,任何人都不能继续花费 non-final 交易的输出,所以此时此刻,你其实并没有真正收到水龙头发出的 BSV。但它又不是完全没用,你只需等待,等到当前时间超过 nLocktime 的限制让这个交易自动变成 final 后,就会收到这些 BSV。
那为什么还要限制交易 nLocktime 的值不能太大呢?
require(SigHash.nLocktime(txPreimage) - this.lastWithdrawTimestamp < 2 * this.withdrawIntervals); |
为了防止有人捣乱。有个叫张三的人他很坏,他在调用合约提款时把交易 nLocktime 的值设置的非常大,让交易很久都能不能 final,从而“卡住”合约,这样所有人就只能等 14 天,等交易超时被移出内存池后才能继续调用。那 non-final 交易不是可以被替换吗?是可以被替换,但张三可以在调用时把合约输入的 nSequence 设置成 0xFFFFFFFE,或者在调用时加上自己的输入来阻止你。😂
给合约充值和销毁合约这两个功能,都在文章计数器合约中详细讨论过。
这里我们用 SIGHASH_SINGLE | ANYONECANPAY 来实现 deposit,因为它既简单又灵活。
public function deposit(SigHashPreimage txPreimage, int depositAmount) { |
水龙头合约在 BSV 测试网上的运行效果如下:
具体规则为:
节点会转发 non-final 交易,但只会打包 final 交易。也就是说,如果一个交易是 non-final 的,那么它一定是未确认的。
请注意,你可以在交易未确认时就花费它的输出,但有一个前提:这个交易必须是 final 的。花费 non-final 交易节点会报错。
有两种方法,可以让交易从 non-final 变成 final:
如果节点收到了一个新交易,它能同时满足:
节点就会认为这个新交易是比之前收到的 non-final 交易更“新”的版本,就会用它替换掉之前的 non-final 交易。如果这三个条件不能同时满足,节点就会拒绝这个新交易。
如果新交易的输入与之前 non-final 交易的输入相冲突(部分相同但又不完全一致),那么根据“先见”原则,新交易会被当成是“双花”交易,也会被拒绝。
本文将介绍该合约 5 个不同版本的实现,带你了解如何使用 sCrypt 在 BSV 上开发智能合约。
根据之前介绍的实现原理,我们把 UTXO 的锁定脚本分成两部分:逻辑(代码)和状态(数据)。
数据段(蓝色)占 1 字节,用于记录当前状态。代码段(黄色)是整个合约的控制逻辑(花费该 UTXO 的条件),用来保证合约的状态延续(强制传递锁定脚本逻辑)且状态的变化符合要求(状态随交易流转依次递增)。
实现状态的延续和控制需要在解锁 UTXO 时知道当前交易的上下文,可以通过在解锁脚本中放入当前交易的 sighash preimage(交易摘要)来实现,并使用 OP_PUSH_TX 技术来保证传入的 preimage 是真实的。
contract CounterV1 { |
代码的整体过程十分清晰:
hash256(expectedOutput)
)跟 txPreimage 中的 hashOutputs(SigHash.hashOutputs(txPreimage)
)保持一致请注意,sighash preimage 中与当前交易输出有关的项只有 hashOutputs,它的值具体是什么与签名时使用的 SIGHASH 标记有关。
函数 Tx.checkPreimage 会使用 SIGHASH_ALL 来进行链上的签名和验签,所以此时从 txPreimage 中解析出的 hashOutputs,是按顺序拼接当前交易所有输出(8 字节小端的金额、锁定脚本的长度、脚本的内容)后的 SHA256 双哈希。这就是最后一行代码的含义:我们期望每次调用这个合约的交易只有一个输出。
另外注意到,用 1 字节记录整数会有“溢出”的问题。代码 num2bin(counter, dataLen)
会在 counter 等于 128 时报错(整数 128 无法编码成 1 字节)。
手动解析和构造解锁脚本看起来有些繁琐,我们可以使用 state 关键字来简化。
contract CounterV2 { |
与 v1 相比,v2 代码没有任何逻辑上的变化,但不再需要手动处理脚本,简洁了很多:
注意到,这里使用的是 sCrypt int 类型来记录状态,它没有溢出风险。
我们可以不断调用 v2 合约,让计数器的值一直增加下去。如果部署合约时锁定了 5000 聪 BSV,每次调用都需要 2000 聪矿工费,那么在第三次调用时会余额(1000 聪)不足,这时候需要增加交易输入“为合约充值”。因为代码里没有对合约之外的其它交易输入做任何限制,所以你才能在调用合约的交易中添加其它输入。但注意,这些交易只能有 1 个输出。
在实际使用过程中,只允许交易有 1 个输出很不方便,因为不能在“充值调用”时添加找零,它会把所有其它输入中的金额都充值到合约中,或作为矿工费消耗掉。
给 v2 合约新增一种解锁方法就可以解决这个问题。
contract CounterV3 { |
我们把 v2 的 unlock 方法重命名成 increase 保留,用来兼容之前所有的交互方式,同时新增 fund 解锁方法,用来支持“充值调用”时的找零。
请注意,fund 解锁方法要求调用交易必须有 2 个输出:第一个输出是合约本身,第二个输出是交易找零。为了能在解锁时确定所有的交易输出,我们需要把 fundAmount、changePkh 和 changeAmount 都放到解锁脚本里。另外还要保证 fundAmount 的值大于 0,以避免在调用时从合约中“偷取”资金。
这个版本的代码看起来有些复杂,它有两种不同的解锁方式,你可以这么认为:sCrypt 会帮你把这些解锁方法都合并到一起,“智能”的构造出统一的包括所有逻辑的锁定脚本,不需要你费心。
前三个版本的代码,都会在合约中锁定一定数量的 BSV。这可以用 SIGHASH_SINGLE 标记来优化,让合约只锁定最少数量的 BSV(满足 dust 限制即可)。
contract CounterV4 { |
新的 v4 代码与之前的合约相比,有两处不同:
这样的处理十分精妙,不仅能兼容已实现的所有功能,还能减少合约中锁定的 BSV,每次调用时你只需添加其它输入支付矿工费即可。SIGHASH_SINGLE 会让 txPreimage 中的 hashOutputs 只包含与当前输入(合约输入)相对应的输出(合约输出)的 SHA256 双哈希。所以最后一行代码既能保证合约的状态延续,又能让你随意添加其它交易输出(在“充值调用”时增加一个或多个找零)。
当然,你还可以使用 SIGHASH_SINGLE | ANYONECANPAY 标记让调用合约的交易变得更灵活,它不仅允许你在合约输出的后面添加任意输出,还允许你在合约输入的后面添加任意输入。我们在上文中提到:可以在调用(不限制其它输入的)合约时添加输入“为合约充值”。这两点并不矛盾,对之前调用合约的交易,你确实可以添加输入来“为合约充值”,但在调用交易的所有输入都签名后,就不能再添加其它输入了,否则会改变 preimage 导致现有输入中的所有签名都失效,而 ANYONECANPAY 会放开这个限制,让你可以在签名后的调用交易上继续添加其它输入。
对“有状态”的 UTXO,因为锁定脚本的逻辑会被强制传递,所以其状态才能在交易链条中保持住。也就是说,不论何时都会有一个 UTXO 记录着合约的当前状态。由于粉尘限制(dust limit)的存在,我们又必须在合约的 UTXO 中锁定少量的 BSV。这两点一结合,就搞出了新“问题”:合约里锁定的 BSV 会被永久锁定,它们就像被“燃烧”了一样,再也无法使用。
别慌,问题不大。我们可以在 v4 代码的基础上,增加一个 destroy 方法来“销毁”合约,以回收被锁定的 BSV。
contract CounterV5 { |
在本例中,我们限制了只有部署合约的人(creatorPkh)才可以执行销毁,就像 P2PKH 一样,要求提供的签名和公钥能与 creatorPkh 相匹配。v5 合约在 BSV 测试网上的运行效果如下:
本文将试着对签名相关的技术名词做一个梳理和对应。
对原始数据做哈希运算,可以得到其对应的数据指纹(数据摘要):
$$hash(m) = d$$
我们称:
在对 BSV 交易签名时,我们会先根据原始交易为每个输入计算它对应的交易摘要(这是我“自创”的词),然后为每个输入做 ECDSA 签名。
交易摘要由 10 个部分构成,在 BIP-143 中定义。我们说,这个交易摘要就是被签名的消息。
在输入的解锁脚本中,有 1 个字节的标记数据,用来指示交易摘要具体的计算方式,一共有 6 种可能。我们把这个标记称为 SIGHASH 标记(SIGHASH flags)。在其它资料中,SIGHASH 标记常见的名称还包括:
在某些特定的上下文中,也可以直接用 SIGHASH 来表示。请注意,这里的 SIGHASH 一般会大写。
ECDSA 算法会对消息做哈希。
在对输入签名时,选取的哈希方法是 SHA256 双哈希(double-SHA256,也常用 hash256 表示)。一般的,我们把交易摘要哈希后的结果,称为 sighash。请注意,这里的 sighash 一般会小写。
$$hash256(交易摘要) = sighash$$
sighash 是 SignatureHash 的缩写。在比特币节点的早期实现中有一个叫 SignatureHash 的函数,用于根据交易输入和当前交易计算要签名的消息对应的哈希,所以常用 sighash 来表示这个函数的计算结果。
uint256 SignatureHash(CScript scriptCode, const CTransaction& txTo, unsigned int nIn, int nHashType); |
结合刚才像和原像的概念,在 sCrypt 中,会把交易摘要称为 sighash preimage,可以翻译成 sighash 原像或交易原像。
BSV 交易原像的一些历史:
特别感谢 nChain Research Team 的 Wei Zhang 对我的指导和帮助,以及对本文内容的审阅和优化。
]]>那 UTXO 可以做到“有状态”吗?
本文将介绍“有状态” UTXO 的实现原理。请在继续阅读前,先理解 OP_CHECKSIG 操作码的工作方式。
BSV 的交易会消耗一个或多个输入,同时产生一个或多个输出。交易(除了 Coinbase 交易)的输入,都是之前某笔交易的输出。如果某个交易输出当前是未花费(unspent)的状态,也就是这个交易输出当前还没有被某个交易拿去作为交易输入使用,我们就称之为未花费的交易输出(UTXO,Unspent Transaction Outputs)。这些(前序)交易未被花费的交易输出(UTXO),可以在之后,作为交易输入被花费。我们说,UTXO 是构成交易的基本元素,因为每笔交易(除了 Coinbase 交易)都是在不断地消耗已存在的 UTXO,同时产生新的 UTXO。
交易的输入和输出上都有一个脚本。输出上的脚本被称为锁定脚本(Locking Script,也常用 scriptPubKey 表示),它是一个加密难题,指定了今后花费这个 UTXO 必须要满足的条件,相当于一把锁。输入上的脚本被称为解锁脚本(Unlocking Script,也常用 scriptSig 表示),它解决或满足了之前放置在这个 UTXO 上的加密难题或条件,解锁 UTXO 用于支付,相当于一把钥匙。请注意,当前交易输入上的“钥匙”试图打开的是它前序交易输出上的“锁”。
比特币虚拟机(BVM)在验证钥匙是否能开锁时,会把 [输入的解锁脚本] 和 [该输入引用的输出的锁定脚本] 拼接起来从左到右执行一遍。如果执行过程中没有出现错误并且执行结果为真,则验证通过,意味着钥匙打开了锁,这个 UTXO 可以被花费。
总结起来两句话:只要提供的钥匙能开锁,UTXO 就可以被花费。只有当提供的钥匙能开锁时,UTXO 才可以被花费。
请注意,解锁脚本里不能出现 PUSHDATA 以外的任何操作码,否则会报错
16: mandatory-script-verify-flag-failed (Only non-push operators allowed in signatures) |
也就是说,解锁脚本里只能有数据不能出现逻辑操作,否则任何 UTXO 都可以用 OP_RETURN 解锁。
OP_TRUE OP_RETURN |
换个角度来看:锁定脚本就像是函数(数据和操作),而解锁脚本是函数的入参(只有数据)。
def unlock(解锁脚本) -> bool: |
BVM 验证脚本就像是在调用 unlock 函数,如果返回结果为真则验证通过,否则失败。
我们可以把 P2PKH 的脚本
[签名] [公钥] OP_DUP OP_HASH160 [公钥哈希] OP_EQUALVERIFY OP_CHECKSIG |
等价成下面的形式:
class P2PKH: |
注意到,解锁脚本中的签名和公钥,变成了 unlock 方法的参数,锁定脚本中的公钥哈希变成了类的成员变量,锁定脚本中的两个限制写到了 unlock 方法中。此时,创建 UTXO 就等价于新建类实例(instance), 花费 UTXO 就等价于调用对应实例的 unlock 方法。
if __name__ == '__main__': |
这种把脚本抽象成函数的思路十分重要,能帮助你理解 sCrypt 代码的组织方式。
什么是“有状态”的 UTXO?
第一点很容易实现,可以直接在 UTXO 的锁定脚本里记录状态数据,让这个 UTXO 变成一个“有状态”的 UTXO。使用 OP_RETURN 或者 OP_DROP 都可以满足这样的需求。
... OP_RETURN data1 data2 data3 ... |
... data1 OP_DROP data2 OP_DROP data3 OP_DROP ... |
对于第二点,UTXO 的状态能按需变化很好理解,但状态链条不会中断,就要求锁定脚本中限制该 UTXO 被花费的条件会被传递,而且会被强制传递。也就是说,对某个 UTXO,花费它的条件是:产生的新 UTXO 的锁定脚本逻辑,必须跟该 UTXO 保持一致,否则就无法花费。
只记录状态是不够的,UTXO 的状态还需要能在交易流转过程中保持住。如果不能保证锁定脚本的逻辑强制传递,状态链条就会中断。
这看起来很不可思议。你转出一笔 BSV 后,还能限制这些 BSV 如何被使用。
我们要实现一个“计数器” UTXO,把自己被花费的次数记录在链上,具体为:
如上图所示,如果锁定脚本的逻辑不是强制传递的,就可以创建出 TX2’ 这样的交易导致状态中断。概括成一句话:有状态的 UTXO 必须要强制传递锁定脚本的逻辑。
我们可以将锁定脚本分成两个部分:逻辑(代码)和状态(数据)。
前者用于控制状态转换,并在交易流转过程中保持不变。
对 P2PKH 和 P2PK 这类“支付型” UTXO,它们当然是“无状态”的,因为锁定脚本中既没有保存状态,也没有强制传递逻辑限制(不是必须创建新的同类型 UTXO 才可以花费当前 UTXO)。
为了实现“有状态”的 UTXO,在花费时(绿色),至少需要知道以下信息(黄色):
很眼熟是不是?这些信息,都能在当前交易的交易摘要中找到:
至此,我们可以得出以下结论:“有状态”的 UTXO 需要在解锁时知道当前交易的上下文,可以通过在解锁脚本中放入当前交易的交易摘要来实现。
但这样会带来新的问题:放入解锁脚本中的交易摘要,如何保证不可伪造。换句话说,如何保证放入解锁脚本中的交易摘要,真的是当前交易的交易摘要。
操作码 OP_CHECKSIG 的工作方式十分有趣,它并不在意签名是怎么来的。它总是从栈上获取公钥和签名,然后根据当前交易为每个输入计算交易摘要并验签。至于这个签名是解锁脚本里直接提供的还是通过其它方式计算出来后再上栈的,并不重要。
注意到没,OP_CHECKSIG 从栈上拿到签名和公钥后,会把当前交易的交易摘要当做被签名的消息来验签。
我们假想有这样一个操作码 OP_SIGN,它完成两个动作:
如果实现了这样的“链上签名”逻辑,就可以“借助” OP_CHECKSIG 来保证放入解锁脚本中的交易摘要,确实是当前交易的交易摘要。
任意选取一对公私钥 $a$ 和 $A = aG$,对下面这段脚本:
... [交易摘要] [a] OP_SIGN [A] OP_CHECKSIG ... |
当 BVM 执行到假想的操作码 OP_SIGN 时,会先从栈上弹出我们手动放入的交易摘要和私钥 $a$ 进行 ECDSA 签名,然后把签名结果重新压回栈中。
... [链上计算出的签名] [A] OP_CHECKSIG ... |
当 BVM 继续执行到操作码 OP_CHECKSIG 时,会先从栈上弹出公钥 $A$ 和链上计算出的签名,然后计算当前交易的交易摘要,把它当做被签名的消息来验签。如果验签通过,那么只有一种可能,就是我们手动放入脚本的交易摘要,与当前交易的交易摘要完全一致,因为 OP_CHECKSIG 只有在被签名的消息是当前交易的交易摘要时才会验签通过。
请注意,直接把私钥 $a$ 放到脚本中公开这没有任何问题,因为它不控制任何 BSV 而只是为了完成 ECDSA 签名。这里的私钥 $a$ 可以重复使用,当然也可以每次都随机选取。
我们给这段脚本起一个名字:OP_PUSH_TX。虽然它也以 OP 开头,但跟 OP_SIGN 一样,是一个假想出来的操作码。使用 OP_PUSH_TX 的目的非常直观:保证解锁脚本中交易摘要的真实性。
那么,这么牛逼的特性该如何上手呢?
sCrypt 已经把这些技术都封装好了。
创世纪升级后,你可以在 BSV 中使用几乎全部的脚本操作码(还剩 5 个操作码计划在 Chronicle 升级中重新启用)。直接使用它们在脚本里编程是十分可怕的,不紧难度大、可读性差,还很容易让别人原地爆炸。如果把脚本操作码看成是汇编指令,把脚本看成是汇编代码,那 sCrypt 就是一门高级编程语言。
不论是编写逻辑复杂的脚本,还是管理“有状态”的 UTXO,sCrypt 都可以胜任。只有正确理解并熟练使用它,才能在 BSV 上进行智能合约的开发。
你可以参考 sCrypt 的官方文档,了解更多技术细节。
本文围绕 UTXO 的状态延续问题进行了一系列介绍,主要内容包括:
验证 ECDSA 签名是否有效,需要三个参数:公钥、签名和被签名的消息。对于 P2PKH 这种最常见的使用了 OP_CHECKSIG 的交易模板:
[签名] [公钥] OP_DUP OP_HASH160 [公钥哈希] OP_EQUALVERIFY OP_CHECKSIG |
脚本中并没有直接提供被签名的消息的内容(P2PK 也是这样)。实际上,解锁脚本中的签名数据,由两部分构成:
在之前的文章中我们提过:“交易中被签名的消息,是交易本身。更准确的说,是通过 SIGHASH 标记区分的、交易中特定的数据子集”。交易本身在签名和验签时是已知的,也就是说,虽然脚本中没有直接提供消息的内容,但存储了能间接推算出消息内容的 SIGHASH 标记,它可以指示 OP_CHECKSIG 如何根据交易本身计算出这个消息(交易摘要),进而完成 ECDSA 的验签。
这篇文章,将介绍操作码 OP_CHECKSIG 的工作方式,以及 SIGHASH 标记的技术细节。
序列化后的交易结构,如下图所示。
根据 BIP-143 的定义,对于某个交易输入,它要签名的消息,一共由 10 个部分构成:
让我们先略过 [2]、[3]、[8] 项,只看剩下的部分。你会发现:
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 标记之间的逻辑关系。红色框表示当前是针对哪个输入计算交易摘要,蓝色填充代表计算出的交易摘要中包含了该部分。
总结一下:
接下来,让我们看下交易摘要中 [2]、[3]、[8] 项具体的计算方法。
交易摘要与交易各部分的关系如下图所示。
结合 SIGHASH 标记示意图,让我们再总结一下:
实际上,这些规则都是在保证一件事:当交易内容发生不合规则(由 SIGHASH 标记指示)的变化时,交易摘要也要随之改变,从而让输入的签名失效。
另一个要注意的点是,表示 SIGHASH 标记只需 1 字节。解锁脚本中签名后紧跟的 SIGHASH 标记是 1 字节,但交易摘要中的 SIGHASH 标记(第 [10] 项)是小端的 4 字节。
对 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 标记的技术细节。
对某个交易输入,当 BVM 执行到 OP_CHECKSIG 操作码时,会要求如下图所示的栈结构:栈顶是公钥,随后是签名。
OP_CHECKSIG 会先从栈中弹出公钥和签名,再根据该输入的 SIGHASH 标记计算交易摘要,然后完成 ECDSA 的验签。
这没有什么新鲜的东西,但有一点需要注意:OP_CHECKSIG 并不在意签名是怎么来的。它总是从栈上获取公钥和签名,然后根据当前交易为每个输入计算交易摘要并验签。至于这个签名是解锁脚本里直接提供的还是通过其它方式计算出来后再上栈的,并不重要。这是一个非常有意思的特性,也是 OP_PUSH_TX 技术的基础。
请在继续阅读前,先弄清楚 BSV 脚本里整数的编码规则。
操作码 OP_BIN2NUM 会将字节序列 a 按小端原码解释成整数 x(其实还是字节序列)。
a OP_BIN2NUM -> x |
其中,
执行脚本 { 0x03 0x02 0x00 0x00 OP_BIN2NUM },字节序列 0x000002 会被转成整数 2,所以栈顶结果为 0x02。
执行脚本 { 0x04 0x01 0x00 0x00 0x80 OP_BIN2NUM },字节序列 0x80000001 会被转成整数 -1,所以栈顶结果为 0x81。
执行脚本 { 0x01 0x00 OP_BIN2NUM },字节序列 0x00 会被转成整数 0,所以栈顶结果为空的字节序列。
执行脚本 { 0x01 0x80 OP_BIN2NUM },字节序列 0x80(-0)也会被转成整数 0,所以栈顶结果为空的字节序列。
操作码 OP_NUM2BIN 会根据字节序列 a 对应的整数值,生成长度为 b 字节的字节序列 x。
a b OP_NUM2BIN -> x |
其中,
请注意,虽然这个操作码的名字叫 NUM2BIN,但并没有要求 a 最短编码,所以它更像是一种 BIN2BIN,在保证 a 和 x 对应的整数值不变的前提下,让用户能自由的缩放字节序列的长度。
执行脚本 { OP_0 OP_1 OP_NUM2BIN },整数 0(OP_0)会被转成 1(OP_1)个字节的序列,所以栈顶结果为 0x00。
执行脚本 { OP_0 OP_2 OP_NUM2BIN },整数 0(OP_0)会被转成 2(OP_2)个字节的序列,所以栈顶结果为 0x0000。
执行脚本 { 0x02 0x80 0x00 OP_1 OP_NUM2BIN },会出错,因为整数 128(0x0080)无法用 1(OP_1)个字节来编码。
执行脚本 { 0x02 0x80 0x00 OP_2 OP_NUM2BIN },整数 128(0x0080)会被转成 2(OP_2)个字节的序列,所以栈顶结果为 0x0080。
执行脚本 { 0x02 0x80 0x00 OP_3 OP_NUM2BIN },整数 128(0x0080)会被转成 3(OP_3)个字节的序列,所以栈顶结果为 0x000080。
执行脚本 { 0x03 0x80 0x00 0x00 OP_2 OP_NUM2BIN },整数 128(0x000080)会被转成 2(OP_2)个字节的序列,所以栈顶结果为 0x0080。
简单来说,只有两点:
因为本质上还是使用 pushdata 并且这部分脚本会被执行,所以需要遵守“最小推送”(minimal push)规则。
对于有符号整数的字节序列,可以用一个符号位来表示正负,用其它位来表示该数的绝对值。符号位一般是字节序列最高字节(the most significant byte)的最高位,为 0 表示正数,为 1 表示负数。
因此,1 个字节原码能表示的整数范围是 -127(1111 1111)~ -0(1000 0000)及 +0(0000 0000)~ +127(0111 1111)。
请注意,使用原码表示有符号整数会出现 -0 的情况,需要特殊处理。对于整数 0,一般只使用 0000 0000 来表示。
根据规则很容易写出实现代码:
def int_to_signed_magnitude(num: int, byteorder: Literal['big', 'little'] = 'big') -> bytes: |
变量 octets 是 num 绝对值对应的最短原码序列,如果其最高字节的最高位(符号位)被占用,就根据大小端规则在左侧或后侧追加一个字节,最后根据 num 的正负设置符号位。
具体规则为:
请注意,整数 0 的原码为 0x00,但栈上的整数 0 是用空的字节序列(OP_0)来表示的,这是一个特例。
整数 10 的原码为 0x0a,即推送 0x0a,“推送 0x01 ~ 0x10,必须使用操作码 OP_1 ~ OP_16”,所以推送整数 10 的脚本应为 { OP_10 },即
{ 0x5a } |
整数 -1 的原码为 0x81,即推送 0x81,“推送 0x81,必须使用操作码 OP_1NEGATE”,所以推送整数 -1 的脚本应为 { OP_1NEGATE },即
{ 0x4f } |
整数 128 的原码为 0x0080,请注意,需要使用小端模式,所以要推送的数据实际为 0x80 0x00,加上 pushdata 操作码,其对应的脚本应为
{ 0x02 0x80 0x00 } |
可以用脚本 { 0x03 0x80 0x00 0x00 } 推送整数 128 吗?不可以。因为 0x000080 不是 128 对应原码编码的最短序列(minimal encoding),节点会报错:
64: non-mandatory-script-verify-flag (Non-minimally encoded script number) |
下面是一个更直观的例子。如果你也想发送此类交易来测试,可以直接使用 bsvlib 里提供的示例代码。
交易 ea15f649f5512d3d671b216d8ecb3e0b9b276ac4e9bd633c9c53b35a5335f420 的第 0 个输出,创建了一个 UTXO,其锁定脚本为
-129 128 OP_ADD OP_EQUAL |
这个 UTXO 在交易 a930294d018d80f0878406d24c4d9320aded8bcd35b4be506eadb5249450dc0e 中作为第 0 个输入被花费,对应的解锁脚本为
-1 |
阅读节点源码可以发现,脚本中整数的最大字节长度是通过配置项 maxscriptnumlengthpolicy 来限制的,其默认值在 v1.0.10 版本里是 10000 字节。
通过调用 encode_pushdata,我们可以写出以下代码实现:
def encode_int(num: int) -> bytes: |
实际上,[2] 是错误的,因为它违反了“最小推送”(minimal push)规则。
这个规则很简单,可以用一句话概括:针对 18 种要压栈的数据,特定的操作码让我们能在脚本里用更短的指令(1 字节)表达动作,不再需要使用 pushdata 的方式(2 字节)。
如果违反该规则,节点会报错:
64: non-mandatory-script-verify-flag (Data push larger than necessary) |
请注意,推送 0x00 不在这 18 个特例范围内,所以正确的脚本应该是 { 0x01 0x00 }。
另一个要注意的点是,在 OP_RETURN 输出里推送数据不受此规则限制,因为 OP_RETURN 操作码之后的脚本不会被执行。
特别感谢 @xhliu 的指点和帮助,其他参考资料如下:
代码实现:
def encode_pushdata(pushdata: bytes, minimal_push: bool = True) -> bytes: |
这是一次全新的探索,如果你也对这样的实践感兴趣,请继续阅读。
Webot 一年的运行成本约 100 BSV,2022 年预计总收入 500 BSV。
所以会有 500 张权益 NFT 卡片被创造出来,每张卡片价值 1 BSV,对应全年总收入的千分之二分成。我自己保留 100 张卡片用于抵扣成本,不参与流通。
权益 NFT 的最终结算将在 2023 年 1 月 1 日进行。如果届时每张 NFT 的分红超过 1 BSV,那么你将根据实际情况获得收益。如果届时每张 NFT 的分红少于 1 BSV,那么你仍将获得 1 BSV。也就是说,这个 NFT 的权益是“保本”的。
目前,Webot 几乎所有的收入都来自用户发送的 MetaCoin 挖矿交易。所以如果 MetaCoin 能被持续赋能,或者 Webot 能在明年有比较大的用户和业务增长,你就很有可能因此而获得更多的 BSV。
所有 400 张参与流通的 NFT 卡片
字段 | 值 |
---|---|
Code Hash | 0d0fc08db6e27dc0263b594d6b203f55fb5282e2 |
Genesis ID | 4a4c589f5e53b5b0227b9de00e611c22ecebd2c2 |
Genesis TXID | cfed129fbea37525d87f16c2ec60c667a3247460d6834abf3f135c49c9c96192 |
Sensible ID | 9261c9c9495c133fbf4a83d6607424a367c660ecc2167fd82575a3be9f12edcf0000000 |
Webot 在没有任何宣传的情况下,能在四年多的时间里发展到今天这样的程度,已经远远超出了我的预期。盈利的多少并不是我持续维护项目的理由,要知道在之前的多年时间里 Webot 都没有任何收入。
如果明年 Webot 的全年收入超过 500 BSV,那么我只是少赚了点,如果不到 500 BSV,你跟我也都没有损失。
所以不管怎样,看起来都不算亏。整个实践可以被当成是一次“让利”推广,我希望 Webot 的用户和业务能不断增长,你在给朋友推荐时也会更有动力。
请在参与活动之前认真理解规则,你可能损失 BSV 的场景包括但不限于
自北京时间 2022 年 1 月 1 日零时起至 2023 年 1 月 1 日零时止,Webot 将始终使用地址
1HG6gBQDcr9s9hEvGmVrtVW3Do2d3KGHX7 |
收款,并定期合并 UTXO 以保证可用性,欢迎所有人监督。
经过一年时间,截至 2023 年 1 月 1 日 0 时快照,共有 163 个地址持有卡片,Webot 2022 年的全年收入为 50.23797491 BSV。
根据约定,每张 NFT 卡片最终对应的权益为 1 BSV,结算交易已发。
最后,感谢你的信任和参与。
]]>则
$$
\sqrt{87} \approx 9 + \cfrac{6}{2 \times 9} = 9\frac{1}{3} \approx 9.333333
$$
用计算器算的结果 $\sqrt{87} \approx 9.32737905$。
这个解法用到的原理很简单。求 $\sqrt{x}$,设第一步找到的整数是 $a$,第二步算出的差为 $b$,则有
$$\begin{aligned}& x - a^2 = b \\[0.5em]\Rightarrow \quad & (\sqrt{x} + a)(\sqrt{x} - a) = b \\[0.5em]\Rightarrow \quad & \sqrt{x} - a = \frac{b}{\sqrt{x} + a} \\[0.5em]\Rightarrow \quad & \sqrt{x} = a + \frac{b}{a + \color{red}{\sqrt{x}}}\end{aligned}$$其中 $\color{red}{\sqrt{x}}$ 可以被自己替换,搞出来连分式的形式:
$$
\sqrt{x} = a + \cfrac{b}{a + a + \cfrac{b}{a + a + \cfrac{b}{a + a + \cdots}}} = a + \cfrac{b}{2a + \color{blue}{\cfrac{b}{2a + \cfrac{b}{2a + \cdots}}}}
$$
注意到,我们选取的 $a$ 已经尽可能大了,这意味着 $b$ 会是一个比较小的数,所以省略上式中的蓝色部分,对整个分数值带来的误差会比较小,因为 $2a$ 比 $b$ 大的多,分数 $\cfrac{b}{2a}$ 是一个很小的数。例如
$$
\cfrac{5}{72} \approx 0.069\color{red}{444}
$$
$$
\cfrac{5}{72 + \cfrac{5}{72}} = \cfrac{360}{5189} \approx 0.069\color{red}{378}
$$
这就是在 $x$ 比较大时,$(a + \cfrac{b}{2a})$ 的值会非常接近 $\sqrt{x}$ 的原因。并且随着迭代次数的增多,结果的精度也会越高。
$$
\sqrt{87} \approx 9 + \cfrac{6}{18 + \cfrac{6}{18}} = 9\frac{18}{55} \approx 9.327273
$$
$$
\sqrt{87} \approx 9 + \cfrac{6}{18 + \cfrac{6}{18 + \cfrac{6}{18}}} = 9\frac{55}{168} \approx 9.327381
$$
$$
\sqrt{87} \approx 9 + \cfrac{6}{18 + \cfrac{6}{18 + \cfrac{6}{18 + \cfrac{6}{18}}}} = 9\frac{1008}{3079} \approx 9.327379
$$
$$
\sqrt{87} \approx 9.32737905
$$
注意到,对于像 $\sqrt{7}$ 这样比较小的数,运用此方法时需要多计算几层以保证准确,因为此时 $b$ 跟 $2a$ 的差距没有那么大,省略分母中的 $\cfrac{b}{2a}$ 会给结果带来较大误差。
$$
\sqrt{7} \approx 2 + \cfrac{3}{4 + \cfrac{3}{4}} = 2\frac{12}{19} \approx 2.631579
$$
$$
\sqrt{7} \approx 2 + \cfrac{3}{4 + \cfrac{3}{4 + \cfrac{3}{4 + \cfrac{3}{4}}}} = 2\frac{264}{409} \approx 2.645477
$$
或者更简单些,直接对 700 开方,再将得到的结果缩小 10 倍。
$$\begin{aligned}& \sqrt{700} \approx 26 + \cfrac{24}{52 + \cfrac{24}{52}} = 26\frac{156}{341} \approx 26.457478 \\[0.5em]\Rightarrow \quad & \sqrt{7} \approx 2.645748\end{aligned}$$用计算器算的结果 $\sqrt{7} \approx 2.64575131$。
]]>质数 $p$ 和 $q$,且有 $p \equiv 3 \pmod{4}$,$q \equiv 3 \pmod{4}$。
组合 $(p, q)$ 为私钥,两数的乘积 $n = pq$ 为对应的公钥。
使用私钥 $(p, q)$ 对消息 $m$ 签名的结果为组合 $(S, U)$,满足
$$
S^2 \equiv H(m||U) \pmod{n} \tag{1}
$$
其中的 $U$ 被称为填充值,$H(m||U)$ 的意思是将 $m$ 和 $U$ 的二进制流拼接在一起后再计算哈希,并将哈希结果(二进制流)看成一个整数。
令 $H = H(m||U) \bmod{n}$ 来简化书写。当 $U$ 确定后,可以用下式计算 $S$ 的值。
$$
S = \left[q \cdot (H^{\frac{p+1}{4}} \bmod{p}) \cdot (q^{p-2} \bmod{p}) + p \cdot (H^{\frac{q+1}{4}} \bmod{q}) \cdot (p^{q-2} \bmod{q})\right] \bmod{n} \tag{2}
$$
这样便得到了一组 $(S, U)$,将结果代入 (1) 式验算,若成立则返回这组结果,反之则改变 $U$ 的值重新计算 $S$,直到找到一组满足 (1) 式的解。
请注意,当 $p \equiv 3 \pmod{4}$ 时,$\frac{p+1}{4}$ 是一个整数,同理 $\frac{q+1}{4}$ 也是一个整数。
完整代码请参考 rabin.py。签名时,通过循环在 $m$ 后追加字节 0x00 来改变 $H$ 的值,并在计算出 $S$ 后验证 (1) 式是否成立,是则跳出循环返回,否则继续寻找。
def sign(p: int, q: int, digest: bytes) -> tuple: |
验签时只需判断 (1) 式是否成立即可。
def verify(n: int, digest: bytes, s: int, u: bytes) -> bool: |
通过上面的描述你能看到,Rabin 签名的计算过程相对复杂,但验证过程极其简单。这个特性使得它非常适合在链上直接验签,虽然我们也可以在比特币脚本中实现 ECDSA 的验签逻辑,但它的计算成本要比验证 Rabin 签名高出许多。
整个算法设计基于这样的数学难题:在模是大合数的情况下,求二次剩余平方根是困难的。使用 Rabin 签名时,为了获得足够的安全性,公钥 $n$ 和哈希 $H(m||U)$ 的结果至少需要 3072 位(bit)。
至此,我们介绍了如何计算和验证 Rabin 签名,并给出了完整代码,方便你直接使用。如果你还关心这些结论和公式背后的推导过程,可以继续阅读,我们留了一些问题没有解答:
证明当 $p \equiv 3 \pmod{4}$ 时,$\frac{p+1}{4}$ 是一个整数。
根据同余的定义,对 $p \equiv 3 \pmod{4}$,存在整数 $m$、$n$ 和 $r$ 满足:
$$\begin{cases}p = 4m + r \\[0.5em]3 = 4n + r\end{cases}$$两式相减,有
$$
p - 3 = 4(m - n) \tag{3}
$$
因为 $m$ 和 $n$ 都是整数,所以 $(m - n)$ 也是整数。也就是说,若两数在某个模数下同余,则两数的差是模数的整数倍,反之亦然。
等式 (3) 两边同时加 4,得到 $p + 1 = 4(m - n + 1)$,即 $\frac{p+1}{4} = m - n + 1$。因为 $m$ 和 $n$ 都是整数,所以 $(m - n + 1)$ 也是整数,证毕。
在数论特别是同余理论中,一个整数 $X$ 对另一个整数 $p$ 的二次剩余(quadratic residue),指的是 $X$ 的平方 $X^2$ 除以 $p$ 得到的余数。
若存在某个 $X$ 使得 $X^2 \equiv d \pmod{p}$ 成立,则称 $d$ 是模 $p$ 的二次剩余。若对任意的 $X$ 式子 $X^2 \equiv d \pmod{p}$ 均不成立,则称 $d$ 是模 $p$ 的非二次剩余。
例如,2 是模 7 的二次剩余,因为当 $X = 3$ 时 $3^2 \equiv 2 \pmod{7}$ 成立。3 是模 7 的非二次剩余,因为你找不到整数 $X$ 使式子 $X^2 \equiv 3 \pmod{7}$ 成立。
求二次剩余的平方根,即已知 $d$ 和 $p$ 求 $X$,可以使用 Tonelli–Shanks 算法。
当 $p \equiv 3 \pmod{4}$ 时,该算法有简化版本,即
$$X =\begin{cases}d^{\frac{p+1}{4}} \bmod{p} \\[0.5em]-d^{\frac{p+1}{4}} \bmod{p}\end{cases}$$例如,当 $p = 7$ 时,若 $X^2 \equiv 2 \pmod{7}$,则
$$X =\begin{cases}2^{\frac{7+1}{4}} \bmod{7} = 4 \bmod{7} = 4 \\[0.5em]-2^{\frac{7+1}{4}} \bmod{7} = -4 \bmod{7} = 3\end{cases}$$你可以验算一下,发现 $4^2 \bmod{7} = 2$,$3^2 \bmod{7} = 2$,根据公式计算出的 $X$ 是正确的结果。
如果你使用同样的方法,计算 $X^2 \equiv 3 \pmod{7}$,可以得到
$$X =\begin{cases}3^{\frac{7+1}{4}} \bmod{7} = 9 \bmod{7} = 2 \\[0.5em]-3^{\frac{7+1}{4}} \bmod{7} = -9 \bmod{7} = 5\end{cases}$$再验算一下,发现 $2^2 \bmod{7} = 4 \neq 3$,$5^2 \bmod{7} = 4 \neq 3$,这两个解都是错误的。他们当然是错误的,因为 3 是模 7 的非二次剩余,没有这样的 $X$ 能满足方程。
所以在不确定 $d$ 是否是模 $p$ 的二次剩余时,用公式计算出结果后还需要验算。这也是在利用 (2) 式求解 $S$ 后还需要验算 (1) 式是否成立的原因,因为我们不确定 $H$ 是否是模 $n$ 的二次剩余。先记下这个问题:
南北朝时期的数学著作《孙子算经》中提出了下列“物不知数”问题:
有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?
即求解同时满足除以 3 余 2,除以 5 余 3,除以 7 余 2 的整数。中国剩余定理(也被称为中国余数定理和孙子定理)给出了此类问题有解的判定条件,并用构造法给出了在有解情况下解的具体形式。
对于 $x$ 的一元线性同余方程组:
$$\begin{cases}x \equiv a_1 \pmod{m_1} \\[0.5em]x \equiv a_2 \pmod{m_2} \\[0.5em]\quad... \\[0.5em]x \equiv a_n \pmod{m_n}\end{cases}$$若整数 $m_1, m_2, …, m_n$ 任意两数互质,则对任意的整数 $a_1, a_2, …, a_n$,上述方程有解。通解可以用下面的步骤构造得到。
以求解“物不知数”问题为例。根据描述可以列出下列方程:
$$\begin{cases}x \equiv 2 \pmod{3} \\[0.5em]x \equiv 3 \pmod{5} \\[0.5em]x \equiv 2 \pmod{7}\end{cases}$$对照着通解公式,计算过程为:
满足条件的解为 $23 + 105k$($k \in \mathbb{Z}$),最小的整数解为 23。
有了二次剩余和中国剩余定理的知识基础,下面解答公式 (2) 是怎么来的。
根据 Rabin 签名的定义,有
$$
S^2 \equiv H(m||U) \pmod{n} \tag{1}
$$
其中 $n = pq$。注意到,$H(m||U)$ 可能不小于 $n$,令 $H = H(m||U) \bmod{n}$ 来简化书写,有
$$
S^2 \equiv H \pmod{pq}
$$
利用证明问题一时得到的推论,有 $S^2 - H = k \cdot pq = kq \cdot p = kp \cdot q$,其中的 $k$ 为整数($k \in \mathbb{Z}$)。即两数的差是 $p$ 的整数倍,也是 $q$ 的整数倍,所以有
$$
S^2 \equiv H \pmod{p} \tag{A}
$$
$$
S^2 \equiv H \pmod{q} \tag{B}
$$
对 (A) 式使用 Tonelli–Shanks 算法的简化版本,可以得到
$$S =\begin{cases}H^{\frac{p+1}{4}} \bmod{p} \\[0.5em]-H^{\frac{p+1}{4}} \bmod{p} \tag{C}\end{cases}$$同理对 (B) 式,也有
$$S =\begin{cases}H^{\frac{q+1}{4}} \bmod{q} \\[0.5em]-H^{\frac{q+1}{4}} \bmod{q} \tag{D}\end{cases}$$为了使用中国剩余定理,需要保证整数 $m_1, m_2, …, m_n$ 任意两数互质。注意到 $p$ 和 $q$ 都是质数,所以 $p$ 和 $q$ 互质,我们从 (C) 式和 (D) 式中各挑一个解,组成下列方程:
$$\begin{cases}S = H^{\frac{p+1}{4}} \bmod{p} \\[0.5em]S = H^{\frac{q+1}{4}} \bmod{q} \tag{E}\end{cases}$$则该方程有解。
$$
S = (\sum_{i=1}^{n}a_it_iM_i) \bmod{M}= \left[q \cdot H^{\frac{p+1}{4}} \cdot (q^{-1} \bmod{p}) + p \cdot H^{\frac{q+1}{4}} \cdot (p^{-1} \bmod{q})\right] \bmod{n} \tag{F}
$$
如果你还记得费马小定理。对整数 $a$ 和质数 $p$,有 $a^p \equiv a \pmod{p}$,当 $a$ 不是 $p$ 的整数倍时,可以直接写成 $a^{p-1} \equiv 1 \pmod{p}$。你可以把这个等式理解成 $a \cdot a^{p-2} \equiv 1 \pmod{p}$,这正是模逆元的定义(一种求解模逆元的方法)。注意到 $p$ 和 $q$ 都是质数,所以 $p$ 不是 $q$ 的整数倍,$q$ 也不是 $p$ 的整数倍。所以有
$$
p^{-1} \bmod{q} = p^{q-2} \bmod{q}
$$
$$
q^{-1} \bmod{p} = q^{p-2} \bmod{p}
$$
代入 (F) 式,得到
$$
S = \left[q \cdot H^{\frac{p+1}{4}} \cdot (q^{p-2} \bmod{p}) + p \cdot H^{\frac{q+1}{4}} \cdot (p^{q-2} \bmod{q})\right] \bmod{n} \tag{G}
$$
为了获得足够的算法安全性,实际使用 Rabin 签名时选取的 $p$、$q$ 和 $H$ 会非常大,导致 (G) 式中的 $H^{\frac{p+1}{4}}$ 和 $H^{\frac{q+1}{4}}$ 部分无法计算。
注意到 (E) 式中的 $H^{\frac{p+1}{4}}$ 可能不小于 $p$,$H^{\frac{q+1}{4}}$ 可能不小于 $q$,所以 (E) 式实际上等价于
$$\begin{cases}S = (H^{\frac{p+1}{4}} \bmod{p}) \bmod{p} \\[0.5em]S = (H^{\frac{q+1}{4}} \bmod{q}) \bmod{q} \tag{E'}\end{cases}$$观察方程 (E) 和公式 (G) 的形式,我们可以直接根据 (E’) 写出最终的 (2) 式。
$$
S = \left[q \cdot (H^{\frac{p+1}{4}} \bmod{p}) \cdot (q^{p-2} \bmod{p}) + p \cdot (H^{\frac{q+1}{4}} \bmod{q}) \cdot (p^{q-2} \bmod{q})\right] \bmod{n} \tag{2}
$$
其中的 $H = H(m||U) \bmod{n}$。虽然 $H^{\frac{p+1}{4}}$ 和 $H^{\frac{q+1}{4}}$ 无法求解,但计算 $(H^{\frac{p+1}{4}} \bmod{p})$ 和 $(H^{\frac{q+1}{4}} \bmod{q})$ 却很简单。
另外注意到,在构造方程 (E) 时,我们还有其他三种情况没有列出,所以 $S$ 的值还可能为
$$S = \quad\begin{cases}\quad \left[q \cdot (-H^{\frac{p+1}{4}} \bmod{p}) \cdot (q^{p-2} \bmod{p}) + p \cdot (H^{\frac{q+1}{4}} \bmod{q}) \cdot (p^{q-2} \bmod{q})\right] \bmod{n} \\[0.5em]\quad \left[q \cdot (H^{\frac{p+1}{4}} \bmod{p}) \cdot (q^{p-2} \bmod{p}) + p \cdot (-H^{\frac{q+1}{4}} \bmod{q}) \cdot (p^{q-2} \bmod{q})\right] \bmod{n} \\[0.5em]\quad \left[q \cdot (-H^{\frac{p+1}{4}} \bmod{p}) \cdot (q^{p-2} \bmod{p}) + p \cdot (-H^{\frac{q+1}{4}} \bmod{q}) \cdot (p^{q-2} \bmod{q})\right] \bmod{n}\end{cases}$$运行代码 rabin.py 中的例子,可以得到签名 (71, 0x00)。改变 $S$ 的计算公式,你会发现 (27, 0x00)、(50, 0x00) 和 (6, 0x00) 都是合法的签名。如何计算 $-H^{\frac{p+1}{4}} \bmod{p}$ 和 $-H^{\frac{q+1}{4}} \bmod{q}$,留给你思考。
在计算签名时,因为不确定 $H$ 是否是模 $n$ 的二次剩余,所以在利用公式 (2) 计算出 $S$ 后,还需要代入 (1) 式验算,验算失败就要重做。如果我们可以在计算 $S$ 前就确定 $H$ 是模 $n$ 的二次剩余,那么可以减少签名过程中的一些无效运算。
数论中的欧拉准则(也叫欧拉判别法)可以用来确定给定的整数是否是一个质数的二次剩余。
若 $p$ 是奇质数(除了 2 以外的质数)且 $p$ 不能整除 $d$(即 $d$ 不是 $p$ 的整数倍),当且仅当
$$
d^{\frac{p-1}{2}} \equiv 1 \pmod{p}
$$
成立时,$d$ 是模 $p$ 的二次剩余。若 $p = 2$,或 $d$ 是 $p$ 的整数倍,则 $d$ 一定是模 $p$ 的二次剩余,无需判定。
Rabin 签名中的 $p$ 和 $q$ 都是奇质数,但 (1) 式中的模数 $n = pq$ 不是。下面我们来证明:对质数 $p$、$q$,若 $d$ 是模 $p$ 的二次剩余,也是模 $q$ 的二次剩余,那么 $d$ 也是模 $pq$ 的二次剩余。
根据条件及二次剩余的定义,存在整数 $a$、$b$ 满足
$$\begin{cases}a^2 \equiv d \pmod{p} \\[0.5em]b^2 \equiv d \pmod{q}\end{cases}$$根据中国剩余定理,因为 $p$、$q$ 互质,所以存在这样的整数 $x$ 满足
$$\begin{cases}x \equiv a \pmod{p} \\[0.5em]x \equiv b \pmod{q}\end{cases}$$所以 $x^2 \bmod{p} = a^2 \bmod{p} = d \bmod{p}$,即 $x^2 \equiv d \pmod{p}$。也就是说,$(x^2 - d)$ 是 $p$ 的整数倍,即存在整数 $k$ 满足 $x^2 - d = kp$。
同理 $x^2 \bmod{q} = b^2 \bmod{q} = d \bmod{q}$,即 $x^2 \equiv d \pmod{q}$。也就是说,$(x^2 - d)$ 也是 $q$ 的整数倍,即 $kp$ 是 $q$ 的整数倍。
因为 $p$、$q$ 互质,所以 $p$ 不是 $q$ 的整数倍,所以 $k$ 是 $q$ 的整数倍。因此 $x^2 - d = kp$ 是 $pq$ 的整数倍,即 $x^2 \equiv d \pmod{pq}$,证毕。
有了这个结论,我们可以对代码做一些优化,改进后的代码请参考 rabin.py。先用欧拉准则确定 $H$ 是模 $p$ 的二次剩余且是模 $q$ 的二次剩余后,再计算 $S$。
def sign(p: int, q: int, digest: bytes) -> tuple: |
虽然只是篇博客文章,但完成它于我并非易事。
特别感谢 nChain Research Team 的 Wei Zhang,对本文内容的审阅和优化,以及对我理解相关数学原理和密码学知识的指导和帮助。
特别感谢 nChain Research Team 的 Owen Vaughan,对我理解 Rabin 签名的指导和帮助。
感谢中国人民大学的研究生李小满,为文中部分证明提供思路。
感谢感应合约团队的顾露、陈诚、蒋杰和陈耀欢,ShowPay 的王宇,以及 sCrypt 的郑宏峰,对我在使用和实践 Rabin 签名过程中的指导和帮助。
谢谢你们在工作和生活之余挤出宝贵时间,热情而耐心的解答我的问题,无私的分享知识和经验。
CoinGeek 2019 首尔大会时,介绍过 multisig accumulator 多签方案。
ElectrumSV 在 1.3.0b4 版本中实现了该功能,源码如下。
事情发生在 11 月 6 号。
从我第一次使用该方案大额存币,到第二次整理,之间间隔 20 小时,黑客都没有动作。
在我完成第二次操作一小时后,黑客利用漏洞将我的币全部转到了地址 1LcKTzSzpMAwH4bzymGSkbhY2EBpmT7n5J,对应的交易是
20adad8bd4cc694cfed4ccadff911433601e55b0f8779e839bc6579cb8d234f9 |
第二天,7 号晚上,我发现被盗。
输出到 accumulator 脚本的 UTXO,被下面这样的锁定脚本锁定。
按照设计,花费这个 UTXO 的解锁脚本中提供的签名个数,要不小于多签的阈值。
下面这笔交易,盗走了我 600 BSV,使用的解锁脚本是“0 0”。
问题出在最后比较 [需要的签名个数] 和 [解锁脚本中的签名个数] 时,使用了错误的操作码 OP_GREATERTHANOREQUAL。
这个操作码实际在验证 [需要的签名个数] 是否大于等于 [解锁脚本中的签名个数]。很明显,这是一个逻辑错误,应该判断是否小于等于。
详细的脚本执行过程,可以参考 Note.SV 作者李龍的分析文档。
事情发生后,我第一时间联系了 ElectrumSV 的作者 Roger Taylor,他随后确认了这个严重的 bug。
同时,我通知了中文社区,也发了 Twitter,以避免更多的人遇到麻烦。
需要澄清的是,丢币不是因为私钥泄露,也不是使用了有问题的 ElectrumSV 客户端,更不是因为 Bitcoin SV 网络有什么漏洞。
P2PKH 脚本没有问题,传统多签方案 multisig bare 也没有问题,Electrum SV 钱包在其他方面的安全性不受影响。
多签方案 multisig accumulator 的 bug 出自理论原型,Roger 在实现时直接参考了演示代码,也没有做足够多的测试。而我对这样的新技术没有抵抗力,直接使用该方案大量存币,最终酿成悲剧。
最后,衷心感谢每位朋友的关心,经历这样的事情十分痛苦,但你们让我感到温暖。
]]>本文将介绍如何使用拉格朗日插值法,求解这样的多项式。
已知点 $(x_0, y_0)$、$(x_1, y_1)$、…、$(x_t, y_t)$,拉格朗日插值法的思路是寻找多项式 $l_j(x)$,使其在 $x = x_j$ 时的取值为 1,在其他点的取值都是 0。
这样的多项式很好构造。
$$
l_j(x) = \frac{(x-x_0)…(x-x_{j-1})(x-x_{j+1})…(x-x_t)}{(x_j-x_0)…(x_j-x_{j-1})(x_j-x_{j+1})…(x_j-x_t)}
$$
注意到,当 $x=x_j$ 时,$l_j(x_j)$ 的分子和分母相同,所以 $l_j(x_j)=1$,当 $x$ 取其他值时,其分子总是 0,所以结果为 0。
多项式 $l_j(x)$ 就像开关一样,可以让 $y_jl_j(x)$ 满足只有在 $x = x_j$ 时的取值为 $y_j$,而在其他点的取值都是 0。
基于此,可以继续构造多项式
$$
L(x) = y_0l_0(x) + y_1l_1(x) + … + y_tl_t(x)
$$
满足过已知的 $(t+1)$ 个点。
给定点 $(x_0, y_0) = (1, 350)$、$(x_1, y_1) = (2, 770)$ 和 $(x_2, y_2) = (3, 1350)$,求过这三个点的多项式。
先构造 $l_j(x)$,有
$$
l_0(x) = \frac{(x - x_1)(x - x_2)}{(x_0 - x_1)(x_0 - x_2)} = \frac{(x-2)(x-3)}{(1-2)(1-3)} = \frac{1}{2}x^2 - \frac{5}{2}x + 3
$$
$$
l_1(x) = \frac{(x-x_0)(x-x_2)}{(x_1-x_0)(x_1-x_2)} = \frac{(x-1)(x-3)}{(2-1)(2-3)} = -x^2 + 4x - 3
$$
$$
l_2(x) = \frac{(x-x_0)(x-x_1)}{(x_2-x_0)(x_2-x_1)} = \frac{(x-1)(x-2)}{(3-1)(3-2)} = \frac{1}{2}x^2 - \frac{3}{2}x + 1
$$
再构造 $L(x)$,有
$$
L(x) = \sum_{i=0}^{2}y_il_i(x) = 350(\frac{1}{2}x^2 - \frac{5}{2}x + 3) + 770(-x^2 + 4x - 3) + 1350(\frac{1}{2}x^2 - \frac{3}{2}x + 1) = 80x^2 + 180x + 90
$$
求解完毕。
显而易见,我们总能按公式构造出 $l_j(x)$,从而构造出 $L(x)$。
注意到,$l_j(x)$ 的分子是 $t$ 个 1 次多项式相乘,分母是一个整数,所以 $l_j(x)$ 是一个最高次不大于 $t$ 的多项式。
任意个最高次不大于 $t$ 的多项式相加,结果多项式的最高次也不会大于 $t$,所以 $L(x)$ 是一个最高次不大于 $t$ 的多项式。
假设这样的多项式不唯一,对任意两个最高次不大于 $t$ 的拉格朗日多项式 $L_1$ 和 $L_2$。
因为 $L_1$ 和 $L_2$ 都过已知的 $(t+1)$ 个点,所以两者的差 $(L_1 - L_2)$ 在这 $(t+1)$ 个点的取值都是 0。
也就是说,$(L_1 - L_2)$ 一定是多项式 $(x-x_0)(x-x_1)…(x-x_t)$ 的倍数。
若 $L_1 - L_2 \neq 0$,则其次数一定不小于 $(t+1)$。
而任意个最高次不大于 $t$ 的多项式相减,结果多项式的最高次也不会大于 $t$,所以 $(L_1 - L_2)$ 的最高次也不大于 $t$。
矛盾。
所以 $L_1 - L_2 = 0$,即 $L_1 = L_2$。
类Polynomial
用来表示有限域 Secp256k1.n 上的多项式
$$
y \equiv a_0x^0 + a_1x^1 + … a_tx^t \pmod{Secp256k1.n}
$$
并实现了多项式加法、乘法和求值等基本运算。
对于方法interpolate_evaluate
,我们的目标是计算插值得到的多项式在某点的取值,所以并不需要知道 $L(x)$ 的具体形式,可以直接将参数 $x$ 的值带入公式计算最终结果。方法的内层循环用来计算 $l_i(x)$ 的分子和分母的值,外层循环将每个 $y_il_i(x)$(分子和分母)的值保存到数组lagrange
中,最后对所有的 $y_il_i(x)$ 通分并求和,得到 $L(x)$ 的值。
def interpolate_evaluate(points: list, x: int) -> int: |
对 Bitcoin SV 交易的 P2PKH 输出,如果其金额小于 546 聪,则该交易是粉尘交易(dust),不会被网络接受。
从 1.0.5 版本的节点开始,这个限制将变成 135 聪。
之前的文章中提到,交易输出的 dust 阈值由函数GetDustThreshold
计算。
根据定义,如果花费一个 UTXO 时需要支付的手续费超过了这个 UTXO 面值的 1/3,那么这个 UTXO 是 dust,生成该 UTXO 的交易不会被网络接受。
请注意调用IsDust
时的实参dustRelayFee
,找一下变量定义。
花费一笔 P2PKH 的输出需要 182 字节,包括 148 字节的输入和 34 字节的输出。传入的费率是dustRelayFee
,由DUST_RELAY_TX_FEE
的值初始化为 1 聪/字节。所以最终计算出的 dust 阈值是 546 聪。
dustThreshold = 3 * minRelayFee.GetFee(nSize) = 3 * int(1000 * 182 / 1000) = 546 |
创世纪升级调整了全网的默认交易费率。
DEFAULT_BLOCK_MIN_TX_FEE
降为 0.5 聪/字节DEFAULT_MIN_RELAY_TX_FEE
降为 0.25 聪/字节但DUST_RELAY_TX_FEE
当时并未调整,仍为 1 聪/字节。这会造成困扰,因为从函数定义和变量命名来看,dust 阈值跟最小交易传播费率有关。
查看 1.0.5 版本的提交记录,能找到今年 5 月的一次更新。
新代码并未修改 dust 的计算逻辑,只是将DUST_RELAY_TX_FEE
调整为 0.25 聪/字节,与默认的最小交易传播费率保持一致。
所以从 1.0.5 版本开始,P2PKH 输出的 dust 将降为 135 聪。
dustThreshold = 3 * minRelayFee.GetFee(nSize) = 3 * int(250 * 182 / 1000) = 135 |
构造交易包含一个 135 聪的输出,在 WoC 上可以广播成功。
构造交易包含一个 134 聪的输出,广播失败。
原始交易为
0100000001477a5b512c95704c0a8d652b2bce4e4b170bbf2a53d59543e22489d640914f8e000000006a473044022007cda4515046d56836dd704ddc5b9ef766a4a648aa09d90916ad6872421e0d8d022072424611f754b362d1af04e60b7302e89c88ff6c14a72654125d52cc6573221541210272da37fa8c7873db2e57ade507525737f8f67ba3e7a61c7667e27a4cd9c743c7feffffff0286000000000000001976a9144b267d063a80f3e4ae65149c7c80107a5d4f3ce688ac42110000000000001976a914570abc2c148eefccd119fbb2eccf82c3e55280e088ac640c0a00 |
验证结果符合预期。
]]>在比特币中也一样,我可以向你提供下列信息,来证明该地址属于我。
项 | 内容 |
---|---|
地址 | 1AfxgwYJrBgriZDLryfyKuSdBsi59jeBX9 |
消息 | 你好世界 |
签名 | H9DnqMSGQmqi0zIWQqVfPXQsq59Qt11F1rUQDxv+4/iUDrSLJ6xHZ7PUSKvVThVWQAy/lLEpE3JeMpOlwUiVGlo= |
使用钱包工具或第三方服务,任何人都能验证。
验签通过即可证明,该地址对应的私钥确实在我手上。请注意,要签名的消息,可以是你我事先商议好的任何内容,这里使用“你好世界”只是演示。
本文将详述如何使用比特币私钥对任意消息签名。
你注意到没有,文章开头的证明信息里只包含地址、消息和签名,而没有公钥。
但验签操作需要公钥。
能从地址反推吗?不能。地址是公钥哈希的可逆编码,从公钥哈希无法反推公钥,因为哈希是单向运算。所以无法从地址计算公钥。
那么问题来了,没有公钥如何验签?只有一种可能,从 ECDSA 签名可以恢复公钥。
在介绍 ECDSA 时我们强调,签名过程中使用的临时私钥 $k$,保证其绝对私密且生成时足够随机非常重要。
签名和消息是公开的,即已知 $(r, s)$ 和 $e$,如果还知道 $k$,就可以算出私钥。
$$
a = r^{-1}(sk - e) \bmod{n}
$$
进一步的,如果知道的是 $K$,则可以算出公钥。
$$
A = aG = r^{-1}(sk - e) \cdot G = r^{-1}(s \cdot kG - eG) = r^{-1}(sK - eG)
$$
所以从签名恢复公钥的关键,是确定点 $K$ 的坐标 $(x_K, y_K)$。
注意到,对 Secp256k1 的参数 $n$ 和 $p$,有 $n < p$,而 $r = x_K \bmod{n}$。
所以从 $r$ 求 $x_K$,结果不一定唯一。当 $r + n < p$ 时,$x_K = r$ 和 $x_K = r + n$ 都是解。
另外,知道了 $x_K$ 也不能唯一确定 $y_K$。椭圆曲线是上下对称的,若点 $(x, y)$ 在曲线上,则点 $(x, -y \bmod{p})$ 也在曲线上。之前的文章在介绍比特币公钥时提到过,对 Secp256k1,至少要知道点的 X 坐标和 Y 坐标的奇偶,才能唯一确定该点。
让我们总结一下。如果想通过 ECDSA 签名 $(r, s)$ 和消息 $e$ 恢复公钥,还需要知道两个额外的信息:
这个“额外信息”只有四种状态,用 2 个二进制位即可表示。高位是 0 标记 $x_K < n$,低位是 0 标记 $y_K$ 的偶数。
二进制位 | 内容 | 十进制值 |
---|---|---|
00 | $x_K < n$,$y_K$ 是偶数 | 0 |
01 | $x_K < n$,$y_K$ 是奇数 | 1 |
10 | $x_K > n$,$y_K$ 是偶数 | 2 |
11 | $x_K > n$,$y_K$ 是奇数 | 3 |
我们把这个标记,称为recovery_id
或recid
,把包含了此标记的 ECDSA 签名 $(recid, r, s)$,称为“可恢复”签名(recoverable ECDSA signature)或致密签名(compact signature)。
对应代码不难实现,只需在计算 $(r, s)$ 时同时计算recid
即可。
def sign_recoverable(private_key: int, message: bytes) -> tuple: |
用私钥对交易签名,实际上是用私钥对交易摘要签名。类似的,对消息签名时,需要先计算消息的摘要,然后对消息摘要签名。
消息摘要有特定的格式。
项 | 内容 |
---|---|
len(H) | H 的字节长度,VarInt 类型表示,定值 0x18 |
H | 字符串 Bitcoin Signed Message:\n 的 UTF-8 编码 |
len(M) | M 的字节长度,VarInt 类型表示 |
M | 消息 $m$ 的 UTF-8 编码 |
def message_bytes(message: str) -> bytes: |
调用message_digest
方法,就可以得到原始消息的消息摘要。
请注意,我们在计算“消息摘要”时并没有使用哈希方法,所以严格来说这个计算的结果不能叫消息的“摘要”,对整个转换过程更准确的描述应该是“消息的格式化”或“消息的序列化”。之所以也使用“摘要”这个词,是为了将它跟之前在介绍对交易签名时提到的“交易摘要”对应起来,方便你整体理解。
至此,所有的准备工作均已就绪,在展示最终结果前,还需要序列化。
签名 $(recid, r, s)$ 总会被序列化成 65 字节。
字节长度 | 内容 |
---|---|
1 | 前缀信息 |
32 | 整数 r 按大端模式序列化后的字节流 |
32 | 整数 s 按大端模式序列化后的字节流 |
别忘了,比特币公钥有两种表示法,一种以 0x04 开头有 65 字节,另一种以 0x02 或 0x03 开头有 33 字节,即同一个公钥会对应两个 P2PKH 地址。为了能在验签时同时校验恢复出的公钥的哈希,是否与输入的地址相匹配,还需要在前缀信息里标记公钥的表示方式。
前缀的值 | 内容 |
---|---|
27 + recid + 4 | 公钥用 33 字节表示 |
27 + recid + 0 | 公钥用 65 字节表示 |
最后,用 Base64 编码处理序列化后的签名数据,以方便人们复制和转录。
from base64 import b64encode |
我们写一个简单的例子来测试。
if __name__ == '__main__': |
输出为
使用比特币私钥对任意消息签名 aaron67 |
使用钱包工具或第三方服务验证这个输出,可以验签成功。
在验证对消息的签名时,输入变成了比特币地址、原始消息和序列化后的致密签名,所以对应的验签过程也要微调。
recid
和 $(r, s)$,同时明确公钥的表示方式利用recid
恢复公钥时,需要从点的 X 坐标求对应的 Y 坐标。对 Secp256k1 的曲线方程
$$
y^2 \equiv x^3 + 7 \pmod{p}
$$
知道了 $x$ 就等于知道了 $y^2 \bmod{p}$,为了求 $y$,需要“开方”,这很麻烦,因为有模运算。我们可以用费马小定理来改进。
对整数 $a$ 和质数 $p$,有
$$
a^p \equiv a \pmod{p}
$$
曲线 Secp256k1 的 $p$ 是一个质数,所以有
$$
y^{p} \equiv y \pmod{p}
$$
利用同余关系的性质,将上式两边同时放大 $y$ 倍,有
$$
y^{p+1} \equiv y^2 \pmod{p}
$$
即
$$
(y^2)^{\frac{p+1}{2}} \equiv y^2 \pmod{p}
$$
则
$$
y = (y^2)^{\frac{p+1}{4}} \bmod{p}\ \ \ \ 或\ \ \ \ -(y^2)^{\frac{p+1}{4}} \bmod{p}
$$
注意到,$\frac{p+1}{4}$ 是一个整数。也就是说,通过计算 $y^2$ 的整数次幂,就可以求解 $y$。
有了这些知识,不难写出对应的验签代码,请注意求解point_k
时对费马小定理的应用。
def verify_message(p2pkh_address: str, plain_text: str, signature: str) -> bool: |
我们用其他工具生成的签名作为输入,来检验验签代码的正确性。将私钥导入 ElectrumSV 钱包对消息签名,可以得到结果
IGdzMq98lowek10e3JFXWj909xp0oLRj71aF7jpWRxaabwH+fBia/K2JpoGQlFFbAl/Q5jo2DYSzQw6pZWhmRtk= |
调用验签方法测试。
if __name__ == '__main__': |
运行结果为
True |
输出符合预期。
结合丰富的操作码,锁定脚本和解锁脚本的形式拥有广泛的可能性。当锁定脚本为OP_ADD 7 OP_EQUAL
时,5 2
和4 3
都是正确解锁脚本。当蔡明使用比特币收款时,她需要提供一个收款模板(锁定脚本),以确保这些比特币只有自己才能花费。很明显,上述这类锁定脚本与现实世界人的身份毫无关联,虽然蔡明可以想方设法将锁定脚本搞的足够复杂,但这种方式并不通用,更没有安全保障,无法彻底杜绝其他人也能提供正确的解锁脚本。
非对称加密中的公钥可以作为身份标识,签名可以作为身份认证和授权的手段。为了做到这一点,蔡明需要在锁定脚本里关联自己的公钥,并限制只有提供了正确的数字签名才能花费这个 UTXO。数学原理可以保证,只要蔡明的私钥没有丢失或泄露就没有其他人能提供正确的签名。
上述这类交易被统称为 P2PKH 交易(P2PK 的演进版),他们的锁定脚本和解锁脚本格式固定,能方便各类钱包集成。比特币网络中的绝大多数交易都是(郭达付款给蔡明)这样的形式,本文将以 P2PKH 交易为例,详细介绍交易签名的细节。
之前的文章介绍了如何创建 ECDSA 签名。在把 $(r, s)$ 放入解锁脚本前,需要先将其序列化成 DER 格式,结构如下。
字节长度 | 内容 |
---|---|
1 | 格式头 0x30 |
1 | 紧跟其后的所有数据的总长度 |
1 | 整数标志 0x02 |
1 | R 的长度 |
变长 | 整数 r 按大端模式序列化后的字节流 R。当流的起始字节不小于 0x80 时,还需要在流的开头添加 0x00 |
1 | 整数标志 0x02 |
1 | S 的长度 |
变长 | 整数 s 按大端模式序列化后的字节流 S。当流的起始字节不小于 0x80 时,还需要在流的开头添加 0x00 |
根据定义不难写出代码。请注意序列化时对 S 的处理,因为 ECDSA 签名的“对称性”,为了避免“混乱”,会强制 DER 中的 S 不大于
0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0 |
即 curve.n // 2
。
from binascii import hexlify |
我们写一个简单的例子来测试。
if __name__ == '__main__': |
运行结果为
b'3045022100fd5647a062d42cdde975ad4796cefd6b5613e731c08e0fb6907f757a60f44b020220350fee392713423ebfcd8026ea29cc95917d823392f07cd6c80f46712650388e' |
在继续之前,我们需要先实现一些基础方法,限于篇幅,请直接参考完整代码。
int_to_varint
、address_to_public_key_hash
、build_locking_script
等常用方法,这些内容在之前的“学习笔记”系列文章中都有过介绍顺便封装一下交易的输入和输出。
from collections import namedtuple |
验证 ECDSA 签名是否有效,需要三个参数:
如果你还记得 P2PKH 的定义,你会发现,不论是解锁脚本还是锁定脚本,都没有明确被签名的消息是什么。
[签名] [公钥] OP_DUP OP_HASH160 [公钥哈希] OP_EQUALVERIFY OP_CHECKSIG |
请注意,解锁脚本中的签名,其实由两部分构成。
字节长度 | 内容 |
---|---|
1 | 紧跟其后的所有数据的总长度 |
变长 | 序列化后的 ECDSA 签名 |
1 | SIGHASH |
之前的文章提到过,交易中被签名的消息,是交易本身。更准确的说,是通过 SIGHASH 标记区分的、交易中特定的数据子集。
交易本身在签名和验签时是已知的,也就是说,虽然脚本中没有直接存储消息的内容,但存储了能间接推算出消息内容的 SIGHASH。这个“推算出的消息内容”,叫交易的摘要。
我们需要实现一个方法,根据交易和 SIGHASH 来计算交易的摘要。
SIGHASH 有 6 不同的类型:
全网几乎所有的交易都使用 SIGHASH_ALL,这是最简单的一种类型,我们将以此为例。其他类型的 SIGHASH 本文暂不涉及,你可以通过文章 SIGHASH flags 和 BIP-143 探索。
请注意,SIGHASH_ALL 会对所有的交易输入签名,也就是说,对应交易摘要的个数,与交易输入的个数相同。
VERSION = 0x01.to_bytes(4, 'little') |
让我们在之前的例子上继续。私钥
0xf97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62 |
锁定了 3 个 UTXO。
priv_key = 0xf97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a4b62 |
事先使用其他钱包 App,消耗inputs[0]
,向地址1JDZRGf5fPjGTpqLNwjHFFZnagcZbwDsxw
支付 800 聪,对应的交易是
4674da699de44c9c5d182870207ba89e5ccf395e5101dab6b0900bbf2f3b16cb |
基于此场景,让我们开始第一个实验:验证这个已签名交易中的 ECDSA 签名。
公钥已知,为了验签,还需要利用这个交易
开始吧。
tx_inputs = inputs[0:1] |
tx_digest = transaction_digest(tx_inputs, tx_outputs)[0] |
通过区块链浏览器,查询序列化后的交易数据。
图中标注的部分,是序列化后的 ECDSA 签名,通过之前实现的方法反序列化。
serialized_sig = unhexlify('304402207e2c6eb8c4b20e251a71c580373a2836e209c50726e5f8b0f4f59f8af00eee1a022019ae1690e2eb4455add6ca5b86695d65d3261d914bc1d7abb40b188c7f46c9a5') |
print(verify_signature(pub_key, tx_digest, sig)) |
运行结果为
True |
验签成功。
第二个小实验,我们用自己实现的代码,创建交易并对其签名。如果交易广播后比特币网络能正常接受,那么说明我们的代码是正确的。
这个交易会将inputs[1]
和inputs[2]
作为输入,向地址18CgRLx9hFZqDZv75J5kED7ANnDriwvpi1
支付 1700 聪。
开始吧。
tx_inputs = inputs[1:] |
serialized_pub_key = serialize_public_key(pub_key) |
序列化后的交易格式在“学习笔记”系列文章中有过详细介绍,这里也列出来方便你对应代码。
字节长度 | 内容 |
---|---|
4 | 交易结构的版本 |
1~9 VarInt | 交易包含几个输入,非零正整数 |
变长 | 输入数组 |
1~9 VarInt | 交易包含几个输出,非零正整数 |
变长 | 输出数组 |
4 | nLockTime |
def serialize_transaction(tx_ins: list, tx_outs: list, lock_time: bytes = LOCK_TIME) -> bytes: |
将序列化后的交易数据打印出来,广播时会用到。同时计算交易的哈希。
raw = serialize_transaction(tx_inputs, tx_outputs) |
代码的运行结果为
b'463043022053b1f5a28a011c60614401eeef88e49c676a098ce36d95ded1b42667f40efa37021f4de6703f8c74b0ce5dad617c00d1fb99580beb7972bf681e7215911c3648de412102e46dcd7991e5a4bd642739249b0158312e1aee56a60fd1bf622172ffe65bd789' |
其中,第一行和第三行是两个交易输入的解锁脚本,倒数第二行是序列化后的交易,最后一行是这个交易的哈希。
我在 WhatsOnChain 上,正常广播了这个交易。请注意下图标注的部分,浏览器解析后的解锁脚本,跟我们的计算结果是相同的。
至此,实验成功。