作者:Tadge Dryja
原文为 Tadge Dryja 在 TABConf2025 大会上的演讲的转录稿,由 Garvit-77 通过 review.btctranscripts.com 转录而成。
CISA 简介
我准备讲讲 “后量子的签名聚合”。演讲应该用不了 30 分钟,所以我们后面还有一些时间来回答问题和讨论。现在是对演讲的内容作个介绍。
我准备讲讲 “椭圆曲线的跨输入签名聚合”、“后量子”或者说 “后椭圆曲线的签名以及聚合”、可以考虑的一种新的操作码 OP_CIV 以及钱包的用法。
在座各位可能听过 EliCurCroInpSigAgg ,我记得人们叫它 “SIGSG” 或者 “EC - 跨输入的签名聚合(CISA)”。这不是一个多么新的想法,大约 2018年、2017 年,人们就在讨论它了;那时候的想法是,在 “隔离见证(SegWit)”升级之后,我们就应该实现签名聚合,我们可以用 Schnorr 签名来做。我记得,在人们讨论 “Taproot” 升级的时候,其中一种想法是 Taproot 软件应该也有内置的签名聚合,或者类似的东西。
签名聚合的基本思路是,在一笔标准的交易中,我们有好几个输入、好几个输出,对吧?Alice 要给 Bob 支付,而假设 Alice 的钱包中有好多个 UTXO,她需要添加 3 个 UTXO(作为输入),才够得着给 Bob 支付的金额,然后交易会有一个找零输出。那么我们假设她选择 3 号、7 号 和 8 号 UTXO,然后签名这些输入。她要为每一个输入生成一个签名,并且将这个签名放在输入中。仔细想想,这有点低效率,不是吗?因为它们背后都是同一个人,来自同一个钱包,但她签名了三次、并且将三个签名都放了上去,外面的每个节点都要验证这三个独立的签名,有点傻。
我们能不能只签名一次呢?比如,不需要放上 3 个签名,只需要 1 个签名(这里的 3 号签名),就够了。不是说让签名只涵盖 3 号 UTXO,这样别人可以熔融或者改变 7 号和 8 号 UTXO,而是说,就用 SIGHASH_ALL 签名,你签名的是所有东西 —— 你哈希所有输入、所有输出, 然后签名所有东西。这样,只要有了 3 号签名, 7 号 UTXO 和 8 号 UTXO 也就绑定了;如果你改变了输入(7 号 UTXO 和 8 号 UTXO),签名就会失效。看起来是能够只签名一次的,对吧?
问题是 —— 实际上对 Alice 没有风险 —— Alice 可以找出 Carol 的 UTXO,然后说这些都是我的比特币,我要用 3 号、7 号和 4 号 UTXO 作为交易的输入。但其实 4 号 UTXO 不属于 Alice ,它用到的是完全不同的公钥、根本就是别人的钱。 这就是说 ,我们没有证据证明这三个输入都来自同一个钱包,所以每一个输入都需要一个签名表示同意参加这笔交易、每个签名都要涵盖输出。
而 “椭圆曲线 - 跨输入的签名聚合”(或者说 EliCurCroInpSigAgg)的办法是 —— 我们用最简单的情形来说明 —— 现在有两个输入 A 和 B;两个输入都需要表示同意交易输出。这怎么用一个签名来做到呢?输入 B 需要 同意(sign off) 签名 A,但它里面又没有签名。那么这个办法是 —— 这里有许多的简化 —— 把签名加在一起。使用椭圆曲线的属性,是可以做到的。基本上就是 签名 A + 签名 B,尽管这个 “+” 号意味着许多操作。你要读许多论文、看懂许多公式(才能知道到底是怎么做到的),但确实能够做到,你可以将这些签名都结合在一起 —— 前提是它们签名的是一样的东西,如果不是,这种加法就行不通。
CISA 的好处与局限性
那么它有什么好处呢?很大一部分好处是节约空间;验证时间有可以节约,但要看语境;比如我有 10 个输入,我聚合成 1 个签名,验证者并不是只验证一个东西就够了 —— 它的扩大吞吐量的效果还是有局限性,每多一个输入,验证者就要多做一些工作。但你至少节约了很多空间,对吧?
以上是 “交互式聚合” 的想法。还有一种想法是 “非交互聚合”:矿工或者保存了交易池的人可以 ——假设 TA 在交易池中看到了两笔交易,那么 TA 可以把这两笔交易的签名聚合成一个。Alice 签名了自己的交易、Bob 也签名了自己的交易,而我可以把这两个签名糅合在一起、然后转发给我的对等节点,同时告知:这是两笔交易,而我已经把它们的签名都糅合在一起了。这种效果,就我们所知,在 secp256k1 上无法做到。使用一些别的有趣的东西,比如 BLS 曲线,是能够做到的,那也非常酷,因为你可以将整个区块内所有的签名都聚合在一起。
不过,我要讲的东西只是交互式签名聚合。我认为 —— 这纯粹是我的个人观点 —— 它之所以没有得到那么多的工作投入、没有让人觉得我们真的要把它加进比特币,原因之一是:它节约了空间,但节约又没有那么明显。 椭圆曲线的签名大约是 64 字节,在 Taproot 以前的地址上稍微大一些,但你们都知道, 这些签名已经可以享受 75% 的见证折扣,对吧?只要它是放在见证字段中,体积统计就少算 75%,所有 64 真实字节换算过来是 16 虚拟字节(vByte)。对于普通交易来说,如果以 vByte 为单位,EC 签名聚合只能节约大约 8% 的体积;这个统计数据,来自 Blockstream 研究团队。
(译者注:作者引用的统计数据位于此处。并且作者引用的数字非常准确:作者说明了自己要讨论的是签名相同数据的交互式聚合,正是所应用的统计数据中的 “full aggregation across tx”。)
有一些人在开发 CISA,我也不想打击他们,8% 的手续费节约,积累多年下来也可能是几十亿美元。所以我认为它是非常重要的,也是很酷的研究,但与此同时,8%,听起来也不是很多,对吧?它不像是我们为了可扩展性和节约区块空间而不得不做的当务之急。 所以,在我看来,这可能就是它没有得到那么多推动力的原因。不过现在出现了一场 “量子跳跃”。
后量子签名需要 CISA
今天好像有人讨论过 “量子签名” 了,好吧,应该叫 “后量子签名” —— 实际上,我甚至不喜欢用 “量子” 这个词。
我个人意见是,我并不怎么担心量子计算机。我认为两者都不太可能,但相较之下,更有可能存在的是,我们所用的椭圆曲线中存在某种常规的老式计算机算法漏洞,而不是量子计算机到来然后用 Shor 算法攻破它;不过我认为两者都不太可能发生, 任何一个我都不担心。但是,谁知道呢,也许哪一天某个人就找出了更快运行 “Pollard’s rho 算法” 的办法,能够从这些椭圆曲线的公钥中反算出私钥(译者注:“Pollard’s rho 算法” 是一种分解质因数的算法)。也许一开始只是一篇论文,只有小规模的加速效果,但随后就让你感觉越来越可疑了,然后人们惊呼我们应该换到另一种签名方案上。
不得不迁移到另一种签名算法,这是完全有可能的,不管(足够强大的)量子计算机会不会到来。 我不觉得它会出现,但为防万一,做好准备总是不错的。
的确有一些签名算法并不依赖于椭圆曲线,而且椭圆曲线会在量子计算机出现后被 Shor 算法攻破(译者注:严格来说,打破的是 “椭圆曲线上的离散对数难以计算” 的安全假设)。主要的缺点在于它们的签名体积会大得多,大概是 50 倍,比如 SPHINCS+ 的签名体积是 4KB 左右。可以尝试缩减它,有另一项有趣的研究是,似乎不需要 4KB,也许我们可以做到 3KB ?也许最终我们可以压缩到 2KB,但这就是我们要接受的事情,我看不出有什么办法能够接近 64 字节。
那么,如果我们不得不迁移,情况会有多糟呢?主网的吞吐量将下降到现在的 2%,所以,如果有人以为 1MB 的区块已经太小了,那么现在你得到了 50KB 的区块,甚至更小, 20KB。(译者注:此处并不是说区块的体积真的会缩小,而是说从吞吐量角度看,签名体积变大相当于区块空间变小;同时也是在嘲讽 “大区块主义者” —— 如果 1MB 区块你们都认为已经太小,那变成 50KB 你们岂不是得疯。)在一个区块能够确认的交易数量上会有很大限制,确实非常糟糕。
那么,签名聚合就会变得很有意义,如果把它与后量子签名或者基于哈希函数的签名一起实现,因为,哪怕我们考虑了见证折扣,这些签名也会是 1KvB 大小;这就不是 8% 的节约,哪怕一笔交易仅有 2 个输入,也是 50% 的节约;3 个输入会获得 60% 左右。所以,按虚拟字节计,这是非常大的节约;并且,如果我们真的迁移到后量子签名,可能还需要提高见证折扣(这完全是另一个方面的问题,我在这里不想讨论)。总之,如果有办法可以聚合这些签名,那将是非常有用的。
那就做吧,我们来聚合 SPHINCS+ 签名。我们先来看看如何聚合椭圆曲线签名。
$$g^s = R\prod_{i=1}^{n} X_i^{H_{sig}(L,R,X_i,m_i)}$$
(就算你能看懂,我也不推荐使用指数表达式,我喜欢连乘表达式,不管了)但是我们怎么连乘哈希值呢?这里的一个大大的 $\pi$ 表示你要连乘这些东西,对吧,就算你可以连乘它们,你要对什么求模呢?模 2 的 256 次方?SHA-256 的生成元是什么呢,是这一项吗?没有一项是对的上的。
人们开发了许多年的好东西,比如 FROST 和 ROAST 和 MUSIG2,还有签名聚合、减半聚合,没有一个能够用在基于哈希函数的签名上。(译者注:这些都是基于 Schnorr 签名的方案。)基于格(Lattice)的签名一样。虽然有人认为也许有办法可以聚合格签名,但是我非常怀疑。因为 —— 我还是拿椭圆曲线签名聚合来举例子 —— 我记得在 2018 年,每个人都觉得这很容易:你不就把这些签名加起来就行了吗?把公钥加在一起、把 R 点加在一起,把 s 加在一起,就能搞出来了。然后就过去了很多年(才搞出来)。这就是为什么它叫 “MUSIG2”,因为 “MUSIG1” 方案其实不太行。不管怎么说,它比我们以为的要难得多,所以我认为格签名聚合也是如此,尽管有一些线索表明这是有可能的。如果说人们以为容易的事、只要加法就能做到的事,也花了五年多时间;那么仅仅是有可能的事,也许就要花十年了。
好了,这些技术,没有一个适合基于哈希函数的签名(以及格签名)。那还有没有别的办法呢?
后量子签名的新聚合方式
这就是我的演讲的用意了。我无法确定,但也许有更好的方法。实际上我们不是一定要把这些签名加起来(或者说结合签名),对吧?我们需要的是,你需要输入能够指明(自己同意的)输出。你可以想想 OP_CTV (就是 OP_CHECKTEMPLATEVERIFY),就是说,你先搞清楚一笔交易的完整形状,然后将它的特征放在一个交易输出中,然后它就会生效,对吧?(译者注:OP_CTV 的作用是,当这个输出要被花费时,就只有具备这样的特征的交易才能花费它。OP_CTV 是一种典型的 “限制条款” 操作码,它能约束一个输出被花费的具体方式。)
它是量子安全的,因为它完全是基于哈希函数的,你等于是以一种量子安全的方式承诺一组输出,从这个意义上讲,它就像一种签名。它能用,但是非常死板,为了在你的输出中使用它,你要提前知道这个输出未来要做什么。所以它对普通交易来说并不怎么有用,因为,在你生成一个地址来收款的时候,你并不知道这地址上的钱未来要花到哪儿去,对吧。你肯定想的是,这是一个新地址,把比特币打给我吧,我到时候决定花到哪儿去。如果我不得不现在就决定未来要把它花到哪儿去,即使有可能作出决定,比如说我可以枚举,我知道它可能会去这三个地方,那么我可以在 taproot 脚本上上放 3 个 OP_CTV 脚本;确实可以做到,但是这非常局促,对吧。一般来说,人们想要的都是能够先收取比特币、事后再把它花到自己想要的地方。
所以,基本思路是,要让输入能够指定其他(一起花费的)输入。只要一个输入签了名,所有其它输入就指向这个输入,并表示 “听这个输入的签名的”。这就意味着,在创建一个地址的时候,我们只需要知道其它(将跟它一起花费的)输入,而不需要知道花费它的交易的输出 —— 这完全可行。在你创建一个地址的时候,也许你能知道未来将跟它在同一笔交易花费的其它输入。这就是我们的基本思路。
这是一笔完全签名的比特币交易,有 3 个输入,3 个签名;你把这两个签名拿走,然后在里面放一些小型指针,它表示:这个输入里面不是一个签名,而是一个指针,指向交易的第一个输入:0 号输入(也就是 3 号 UTXO);下面这个输入也是一样。在这笔交易里花费的这两个输入,指向拥有一个真实完整签名的输入。
那么怎么制作这些指针呢?如何让 2 号输入指向 0 号输入,并且不使用签名?我们的做法是使用 0 号输入(3 号 UTXO)的 “输出点(outpoint)”。这些论述比较难,要跟上 —— 输出点指的是 UTXO 的标签,它就是 (TXID, index) 而已。在你创建一笔交易的时候,它会有一个 TXID ;假设这笔交易有两个输出,那么就分别用 (TXID, 0) 和 (TXID, 1) 来标记这两个 UTXO 。这就是输出点。
我们的想法是,将 0 号输入的输出点放到 2 号输入的 taproot 脚本树上;这也意味着, 输入 0 号输入的 UTXO 输入在 2 号输入的地址生成之前就要存在。而强制执行的办法是 <outidx> <txid> op_civ —— 这是一个简化的版本,我会在另外的幻灯片中演示实际的版本 —— 使用一个操作码,取得一个输出点,然后检查这笔交易是否也花费了这个输出点。如果是,那很棒,交易通过;如果不是,交易就失败,它是无效交易。基本上就是这样。
这是一个相当简单的操作码,对吧。确实需要一些设计,实际上,我准备提议的是 —— 我本来想在这场演讲之前就把它写下来、发到邮件组,但是没能如愿;不过我可能在回程的飞机上就会把这次演讲的幻灯片发出来,然后开一个邮件组贴子 —— 给操作码加入两个额外的参数,输入的索引号和一个 nonce (<inidx> <outidx> <txid> <nonce> op_civ)。输入的索引号用来指明要检查这笔交易的哪个输入的输出点(<outidx 和 <txid>);而 nonce 会被丢弃:它只是一个随机数,你只是放在里面,在脚本执行的时候它会被丢弃,它没有共识上的含义,只是用来盲化(blinding)。
我再仔细讲讲这两个额外的参数。
输入索引号用来指明要检查交易的哪个输入。举个例子,假设我们正在验证这笔交易的 1 号输入(它是 Alice 的 7 号 UTXO),它的脚本包含了 0 、输出点还有一个随机数,那么我们就会到交易的 0 号输入搜索 Alice 的 3 号 UTXO 。如果你不指明要检查哪个输入,那么技术上来说计算复杂度就是 O(n^2) —— 想想如果你有 1000 个输入,每个都说要搜索其它输入 —— 技术上来说,每当你增加一个输入,就会增加在验证每一个输入时要搜索的数量,所以基本上就是 O(n^2)。因此我们加上这个,可能只需要 2 字节,差不多吧。
然后是 nonce 。如果你的地址会指向你拥有的其它 UTXO,是有隐私泄漏风险的,对吧?如果你每次都会使用一个新的密钥,那么光看这些地址就看不出来了;这样的盲化看似已经足够了,然而一旦你花费它,人们就会知道你的密钥,然后可以尝试研磨来看看你在这棵 taproot 树上还承诺了什么东西,这可能会被用于区块链分析,也就是把地址去匿名化。在花费的时候,你已经部分去除匿名了,你自己说了这些输入有关联、用的是同一个软件、来自同一个实体或相互协作的实体。但你可能还承诺了其它东西,它们的关联还没有暴露,你希望遮掩它们。所以你放一个 nonce 进去,一个 16 字节的随机数或者别的什么东西。也许不应该是随机的,也许应该是你要承诺的输出点的私钥的哈希值,因为这样容易在事后重新生成出来。
好,那么在你使用它的时候,你就把这些元素(<outidx> <txid> <nonce> op_civ )放到 taproot 树上。你有一个主密钥,假设它是一个后量子的公钥,放在 taproot 树的顶端(好吧,如果你使用的是 BIP 360,也许它并不放在顶端)。一些分支是正常的使用签名的脚本,另一些则承诺了你已经拥有的其它 UTXO。至于输入索引号(<inidx>),就留空,在花费的时候再推入堆栈。在你构建交易的时候,你就通过放入这些索引号来签名它们。
它看起来是能用的。它可以节约许多空间。整个模式也非常简单,实现起来很直接,可能只需要 20、30 行代码。
钱包用法与实现细节
有个比较复杂的地方是钱包。我们区分两种情形:一种是你已经有一些 UTXO 了,另一种是你还完全没有 UTXO 。
你第一次在一个新钱包里创建一个新地址的时候,你就还没法利用这种技术,对吧?你只能说这是一个新的地址,我准备在其中放置一个完整的公钥,用一个完整的签名来花费它。不过只要你开始生成第二个地址,而你已经有一个 UTXO(你还没有花掉它),你就可以承诺它。好,那么钱包要取得这个 UTXO 的信息,然后放到 taproot 树上。
也就是说,(为了生成一个地址)钱包要取得当前拥有的所有(*)UTXO 的信息,然后将这些信息还有一个后量子公钥放到 taproot 树上。你希望这一切是确定性的,你希望它有秩序,但是这样会让钱包复原变得更复杂。如果你只有一个种子词,你想要生成所有的地址,但你的许多地址都承诺了 UTXO,所以你可能需要先生成 100 个地址,然后,在地址 0 里面,可能有一个 UTXO 。可是,地址 1,它可能承诺了这个 UTXO,也可能没有承诺它。这就有了两种可能,技术上来说是 O(2n),这是非常糟糕的。大型钱包一定会出现,假设你有几百个 UTXO,怎么能检查得过来呢? 应该把它限制在 10 什么的。总之,钱包复原的速度会下降 1000 倍;这能接受吗?我不知道,有几千个 UTXO 的交易所和人真的在乎从一个种子词复原钱包的速度吗?总而言之,这是可行的,但需要进一步思考如何设计钱包的模式。
这种办法,今天一样能用,只要启用了这种操作码,你就可以把承诺放到 taproot 树上。只是似乎没有意义,因为现在的签名本来就很小,只有 64 字节;加上配套的东西,可能也就 70 多字节。所以加入这个操作码似乎不会有太大收益。我猜验证速度会稍微快一点,因为验证节点只需要检查另一个输入,不需要再做椭圆曲线操作;但是在节约空间上作用不大。
但是,所有已知的后量子签名方案的签名都比 EC 签名大得多。的确有一些是几百字节,但是我认为人们正在讨论和寻求的、在不得不用时最有可能使用的,就是基于哈希函数的签名方案,因为比特币已经依赖于哈希函数的安全性了。因此(如果使用基于哈希函数的签名),签名的体积将在 3 KB、4KB 量级。
还有一个人们想必会担心的事情是,怎么处理 “重放(replay)” 呢?你毕竟没有签名。你揭晓这种承诺之后,别人能拿它来重播吗?不能,因为它不是使用公钥或者地址。
你先设想另一种情形,就是这个操作码的效果是让一个地址承诺另一个地址。在一个公钥中,你表示,如果签名的是另外一个公钥,那是没问题的,就把那个公钥的签名当成我的签名。这也许也能奏效,但会让你可能遭受重放攻击。我们这种设计都不会有任何的重放问题,因为你指向的是一个 UTXO 。你无法制作出循环,不可能让一笔交易的所有输入都使用 OP_CIV,因为那会形成一个哈希循环:输入 A 里面的哈希值包含 B,B 里面的哈希值又包含 C,C 的又包含 A,这就成了一个循环,使用哈希函数是做不到的。
此外,使用它的时候你也无法重复使用地址。如果你一直重复使用同一个地址,那就根本无法使用,我认为这也是个优点。还有一个有点蹊跷的地方是,如果你一次性生成许多地址,比如马上生成 100 个地址,分别交给 100 个人,那也根本无法使用这种操作码,对吧?因为你的所有 UTXO 都是独立的。你制作一个地址、收款、花掉、然后又制作一个新地址,这样才能好好利用这种技术。所以你也无法在使用扩展公钥(XPUB)的同时使用它。我不知道如果使用后量子密钥,XPUB 这种技术本身还有没有可能存在。所以也不好说这东西是不是把 XPUB —— 给别人一个很小的东西,别人就能从它派生许多公钥 —— 这种技术打破了。也许还有办法,但确实有些棘手。
还有别的一些缺点。没有循环,因为每一个 UTXO 都是独一无二的。
大致就是如此。它可以跟任何签名算法配合。它可以配合椭圆曲线签名,只是意义不大;但 SPHINCS+ 和格签名都可以。大致就是这样。
我并不是在鼓吹我们需要一次软分叉来激活它或者后量子签名,只是看过所有这些后量子签名之后,你会有一种感觉:这里面问题太多了,如果你觉得我们最终是要使用它们的,那么这样一种操作码(或者其他人有更好的想法、提出类似的操作码),是能帮上大忙的。因为签名的体积将变得非常大,见证数据会占据区块空间的 90%,那么把这部分体积降下来总是好事。
谢谢各位,我看我们还有几分钟能讨论一些问题。如有任何问题,你认为它无法工作或者你有更好的想法,请告诉我。谢谢。