作者:Jeremy Rubin

来源:https://rubin.io/bitcoin/2022/09/14/drivechain-apo/

本文大量借鉴了 Zmnscpxj 的精彩文章,他展示了如何使用递归的限制条款(covenants)来实现 drivechain。在本文中,我将展示类似的技巧,可以使用 AnyPrevOut 和一次性的受信任启动仪式,实现类似于 drivechain 的东西。

本文也介绍了可以用在不同类型的限制条款中的通用技巧。

注:我大概在 2022 年 5 月 5 日就写好了这篇文章,并小范围分享过。

Spooky Chains

皮亚诺计数器

我们需要构造的第一部分是一个皮亚诺计数器图(Peano counter graph)。Zmmscpxj 的方案用上了 SHA-256,但我们将使用一个公钥,并开发一个简单的从 1 到 5 的、可以递增递减(inc/dec)的计数器。

假设有公钥 K1 …… K5,并且有一个点具备 NUMS 属性(Nothing-up-my-sleeve number,本意来自魔术师在表演前向观众展示自己袖子里面没有藏东西,在密码学中表示随机取出而非有意选择的数 —— 此中无后门),例如是 HashToCurve(“Spookchains”)。

生成如下的脚本:

<1 || K1> CHECKSIG
...
<1 || K5> CHECKSIG

现在,在 Ki 下使用 sighash 标签 SIGHASH_SINGLE | SIGHASH_ANYONECANPAY | SIGHASH_ANYPREVOUT 生成两个签名(如下详述)。

递增规则

对每一个 Ki,只要 i < 5 ,就创建一个签名,包含一笔这样的交易:

数额:1 聪
公钥:Tr(NUMS, {<1 || K{i+1}> CHECKSIG})

递减规则

对每一个 Ki,只要 i > 1 ,就创建一个签名,包含这样的交易:

数额:1 聪
公钥:Tr(NUMS, {<1 || K{i+1}> CHECKSIG})

这真的是皮亚诺计数器吗?算是吧。虽然传统的皮亚诺数字是以结构化的类型来定义的,例如 Succ(Succ(Zero)) ,这里我们通过一个 Inc/Dec 交易操作码定义了它们,而且我们必须显式地绑定这些皮亚诺数字,因为每个元素都需要一个唯一的公钥。它们在精神上是相似的。

实例化

产生一个由递增规则和递减规则产生的所有签名的列表。

诚实的参与者应该销毁私钥集 k

要创建一个计数器的时候,只需花费输出 C:

数额:1 聪
公钥:Tr(NUMS, {<1 || K1> CHECKSIG})

来自 K1 的签名可以 绑定到 C,并将它 “转化成”(+1)状态:

数额:1 聪
公钥:Tr(NUMS, {<1 || K2> CHECKSIG})

这个输出可以转变成 (+1)状态:

数额:1 聪
公钥:Tr(NUMS, {<1 || K3> CHECKSIG})

这个输出又可以转化成(-1)状态:

数额:1 聪
公钥:Tr(NUMS, {<1 || K2> CHECKSIG})

这个过程可以无限重复。

我们可以把这种技巧从 1 ... 5 推广到 1 ... N

处理任意的 存入/取出

前面展示的设计的一个问题是它无法很好地处理任意的存入。

一种简单的处理办法是为你希望支持的每一个数额实例化协议。

但这样做非常低效,而且需要大量的存储空间。

另一种方法是,按比特将存入的金额除以 2(或者其它基数),并放到计数器 UTXO 里。

对每一个比特,我们创建的输出需要带有 2^i 聪的面额,而不能使用 1 聪的面额。

这时候我们不能只使用 K1 ... KN ,而要使用 K^i_j ,其中 i 表示聪的数量,而 j 表示计数器。每个数额都需要多个公钥,不然签名就会对已经烧掉的资金有效。

分割与合并

对每一个 K^i_j ,允许分割和合并也是有用的。

分割可以通过预签名来实现,对每一个 K^i_j,只要 i!=0 ,就使用 SIGHASH_ALL | SIGHASH_ANYPREVOUT 签名:

输入:公钥 K^i_j 的 2^i 聪
输出:
    - 2^{i-1} 聪,公钥为 K^{i-1}_j
    - 2^{i-1} 聪,公钥为 K^{i-1}_j

合并也可以通过预签名来实现,对每一个 K^i_j,只要 i!=MAX ,就使用 SIGHASH_ALL | SIGHASH_ANYPREVOUT 签名:

输入:
    - 2^i 聪,公钥为 K^{i_j}
    - 2^i 聪,公钥为 K^{i_j}
输出:
    - 公钥 K^{i+1}_j 的 2^i+1 聪

注意:合并允许第三方从外部存入资金,这并不是限制条款的一部分。

分割及合并的动作,意味着 spookchain 的运营者可以将 UTXO 合并以减少 UTXO 的数量,同时允许存入任意数额。

每个区块投一次票

为了强制执行每个区块只允许投一次票的规则,需要保证签名集中所有的输入的序列号(相对时间锁)都设置成 1 个区块。不需要 CSV 是因为签名中已经包含了 nSequence 字段。

终局状态/阈值

当一个计数器到达了第 N 个状态,就表示它在一段时间内已经积累了一定量的工作量、人们已经共识了某一些结果。

这时候就需要一些可用的状态转换方法。

一种解决方案是在此时将资金都发送到一个 OP_TRUE 输出中,而递增状态的矿工需要负责遵守该 Spookchain 的规则。或者,出于便利,也可以将资金指定给一些 管理员公钥/联盟,并且,在 N 个区块后降级到更少的签名者(最终是 0 )(这是为了在联盟死亡时允许恢复资金)。

这看起来就像,来自某一个 K^i_j 的一个签名将资金放到了一个 OP_TRUE 输出中然后立即花掉了它。不然,其它的 spaookchain 矿工就需要孤立这个区块。

开放状态/提议

对于一个状态 K^i_1 ,将之转化为 K^i_2 的交易可以被视为 “特殊的”,而 OP_RETURN 输出类型可以用来承诺(例如)在到达终局状态时必须被创建的输出。这就厘清了 “被投票的是什么” 的问题。

这个方法 不会 在共识层锁定被投票的终局状态是什么。

在特定情况下,不违法一次性启动仪式的约束,如果一个固定的取款地址列表是提前可知的,那么开放状态可以包含给特定参与者的取款,然后这些取款必须得到来自矿工的一定数量的支持票。但是,如果不使用新的密码学元件的话,想实现可以表决任意交易提议似乎是不可能的。

启动仪式变量

xpubs

不是为每一个状态使用随机生成的公钥,而是使用一个 xpub 并根据 k/i/j 的派生路径为每一个 状态/面额 生成公钥。这可以节约一些数据,而且不需要那么多熵。

免信任的数据承诺

使用整个程序的详述的哈希值,作为对 xpub 的调整项,这样其他人就能快速验证自己是否拥有了你(在诚实假设下)需要生成的所有签名。

实现这种方案的一种方法是将一个哈希值确定性地转化成一个层级式确定性钱包 Child Number 的一个列表,并以此调整 xpub。这是一种便利但低效的调整 xpub 的方法,因为对签名设备来说,子密钥拥有一个正常的派生路径。

单方

由一个参与者预签名一个 Spookchain 的所有交易,并销毁自己的 xpriv(私钥)。

你需要相信 TA 已经删除了私钥并已经准备好了所有签名,但你不必信任给你提供 spookchain 数据以及执行所有状态转换的人,因为数据承诺是免信任的。

使用 MuSig 的多方

在参与启动仪式的所有参与者中定义一个 MuSig 公钥,并且是 N-of-N 的。

现在,你只需信任这个一次性启动仪式中至少有一个诚实参与者就可以了!非常棒!

不聚合的多方

这个模式也可以使用不聚合的多签名公钥。复杂度是 O(签名者数量),但是,这意味着你可以像照着菜单点菜那样,让从未 与启动仪式交互/执行启动仪式 的随机参与者聚合启动,只要他们签名了同一份程序详述。

也可以结合多个使用 MuSig 的多方。

这很棒,因为 MuSig 就意味着各参与方在某个点上串通做了一次 MuSig 的启动设置,但不聚合的多签名可以在各方没有任何交集的前提下执行。

软分叉掉信任

假设一个 Spookchain 变得非常流行。你可以配置自己的客户端以拒绝无效的状态转换,或者限制 Spookchain 密钥、使之仅能使用已知的签名。这种软分叉将平滑地更新信任假设。

状态转换规则与 DAG 限制条款的对称性

我们可以通过一个免信任的限制条款实现递增的状态转换规则,也可以通过启动设置实现向后的状态转换。

对于状态 i,其脚本看起来就像这样:

Tr(NUMS, {
    `<sig for state K_{i+1}> <1 || PK_nonsecret> CHECKSIG`,
    `<1 || Ki> CHECKSIG`
})

这样的优化在理论上有很好的效果,因为它意味着计算中 只有 非解构性的(non-destructuring)递归部分是受制于一次性启动的信任假设的;受信任的一次性启动仪式在其它许多协议中也会用到,但在哪些协议中,递归特性只会在(例如)一段时间的超时后解锁(而在 spookchain 中,则每一步都需要用到)。

编译器的作者可以从任意的一个抽象图谱开始,然后有选择地移除例外情形(可以使用一些启发法,例如,最小化对一次性启动设置的依赖,或者最小化开销),直到这个图谱变成一个有向无环图,由一个或多个组件构成并使用被承诺的限制条款编译这些租金,然后使用一次性启动设置的密钥材料补回原先移除的例外情形。

关于信任和限制条款的评论

这是一种限制条款吗?我觉得 “是”。我在自己的 Calculus of Covenants 一文中定义限制条款时,每一种限制条款都有特定的一组假设。

在那个模型下,举个例子,你可以将使用特定已承诺指令的 7-10 多签名称作 4-10 诚实假设(需要至少 4 个签名人保持诚实,才能应对无效的状态转换)以及 4-10 可杀死假设(死亡的签名人达到 4 个就无法再恢复资金)。

至于被预先签名的模拟程序,例如用于模拟 CTV 的变种,那就是另一回事,因为如果你的程序是正确的,而且你预先获得了 N-N 的签名,那么它就是 1-N 诚实假设(只需要 1 个参与者是诚实的,就能防止无效的状态转换),而且是不可杀死的(所有参与者都可以安全删除私钥)。

我认为这些关于活性和诚实性的假设只是不同的 “复杂性类别”。

我想指出的是,上面展示的计数器模型,完全就是一个预先签名的 1-N 诚实假设和不可杀死的限制条款,不要求来自签名者的任何活性。更进一步地说,有了 APO,限制条款的新实例就不需要新的一组签名者,启动设置是真正一次性的。因此,这种类型的限制条款的信任复杂度类型比使用预签名交易模拟的 CTV 甚至更低,因为后者需要在每一个合约实例中动用一个新的联盟来签名。

讲完这些之后,我们再来分析一下这个限制条款:

1)交易内容集合的一个集合(一个家族),可能是递归的或者共递归的(co-recursive)(例如,可以产生的状态转换的类型)。这些内容由可以不用交易本身,而用一种可以生成这些交易的语言来表示。在这个意义上,我们使用 “家族” 而非 “集合” 来称呼,因为要想实例化一个限制条款我们必须选出一些家庭成员。

这个交易内容集合的集合是为了 递增/递减 到一个后代或者祖先,以及减半为两个实例或通过增加资金而使价值翻倍。每一个后代和祖先都是同类型的限制条款,就除了第一代和最后一代 —— 它们有一些特殊的规则。

2)一个验证器生成器,会产生一个函数,(该函数)接收一个交易内容(该内容是内容家族的一个成员的任意元素)以及一个相应的证明,并拒绝其它东西。

这里的验证器生成器就是 APO CHECKSIG 脚本。

3)一个证明器生成器,会产生一个函数,(该函数)接收一个交易内容(该内容是内容家族的一个成员的任意元素)以及一些额外的数据,并返回一个新的证明器函数、一个完成的诚征,或者拒绝(表示这不是一个有效的内容)。

这里的证明器生成器是从给定脚本的一个表格中选出正确的签名。

运行带有私钥的证明器生成器一次,就可以为所有可到达的状态实例化,并缓存相应的签名,然后其中的私钥就可以删除了(为了后续的运行)。

4)一组证明,让证明器、验证器和一组内容可以 “阻抗匹配”,也就是说,证明器可以证明的所有语句,与验证器可以验证的所有语句,是一一对应的;跟内容(一组交易)的一个元素也是一一对应的。

对给定的一个关键状态,可以发生的唯一时间就是签过名的交易,堆栈之外不需要解释其它数据。因此这是完美的阻抗匹配。

5)一组假设,限制条款就在这些假设下得到验证(例如,要求至少 1-N 诚实的多签名限制条款、要求任意 3-N 诚实的多签名限制条款、SHA256 抗碰撞性、离散对数难解、正确的 SGX 模块)。

特别的是,在启动阶段,至少一个私钥被彻底删除了。

为了安全性,也要假设对任何比特币交易来说都需要假设的有用方面。

6)可组合性

如果被任何其它限制条款家族请求,终局状态可以支付给一个预先指定的限制条款。

(完)