作者:ZmnSCPxj
来源:https://delvingbitcoin.org/t/towards-a-k-of-n-lightning-network-node/2395
引言
最近,随着《嵌套的MuSig2》论文的出版,我们开始思考:我们能够制作出一种集成了闪电网络的多签名自主保管钱包吗?
具体来说,我们希望的是,在 “简单 Taproot 通道”(PR995)中,我们可以使用 “嵌套的 MuSig2”,制作出天衣无缝的多签名闪电网络节点。
但是,先想想:一个终端用户说的 “我想要一款 k-of-n 的多签名钱包”,究竟是什么意思?
我认为,终端用户的核心需求是这样的:
关于 “k-of-n 多签名”,我的愿望是,我持有 N 台设备,只要不到 K 台被劫持,我的资金就是安全的。即使我不可挽回地弄丢了一些设备,只要不到 (N - K + 1) 台,我就依然能花费我的资金。
这种愿望,我们该如何转化到(现实场景中的) “闪电网络钱包” 中?
一个区块链内的钱包可以直接用脚本来表达这种 k-of-n 的要求,相当简单,但闪电网络钱包不行。闪电网络通道有额外的复杂性。这些额外的复杂性,需要闪电网络 BOLT 协议发生变更,不仅仅是为了 k-of-n 的闪电网络钱包自身,也是为了其通道对手(可能是,也可能不是 k-of-n 多签名的)。
这意味着,一个想要使用 k-of-n 多签名闪电钱包的终端用户,必须:
- 等待修改过的闪电网络 BOLT 协议获得广泛采用,或者
- 接受来自网关节点的服务(他们 “提早” 实现了修改后的闪电网络 BOLT 协议,代价是损失隐私性,以及更少的转发费收入。
K-of-n 多签名的闪电网络节点的一个主要应用场景,是让手握大量资金的 HODLer 可以使用自己的储蓄来给闪电网络提供流动性,随时保证自己是安全的、可复原的。
(译者注:“HODLer” 是一个有文化意味的词,来自 “holder(囤币者)”。下文都翻译为 “囤币者”。)
然而,在这套修改后的闪电网络 BOLT 协议得到广泛采用之前,这样的大型的 k-of-n 多签名节点都只能连接到网关,由后者在修改后的协议与尚未升级的网络之间架起桥梁;可以理解,这样的网关节点会为此收一笔费用,只是收费方式或此或彼而已。任何不收取费用的网关节点都会发现,其可用流动性会被这样的重仓囤币者很快用尽;他们自己是没有那么多的流动性的,除非他们自己也是重仓囤币者;但如果他们是,则(根据我们的假设)他们本身就会想要使用一个多签名装置,也就是同样需要使用支持这套修改后的协议的网关。
因此,为了支持我们说的这个应用场景,协议必须 “提早” 修改,以驱动修改后的协议得到采用,这样重仓囤币者才能使用自己的储蓄来提供流动性。
信任最小化的通道对手
有人可能会反驳:“闪电网络实际上更加安全,因为必须也攻破通道对手,才能让受害者弄丢自己的密钥。”
这种想法也是非常错误的。
正确的想法是:“通道对手占据了坑害你的最佳位置,所以你应该格外担心你的通道对手。”
具体来说,设想这样一种情形:如果你是一个重仓囤币者,希望向闪电网络提供流动性(换取手续费收入),你希望任何人都能与你开设通道。其他人跟你开设通道,意味着你有更多机会赚取路由手续费!
如果你不允许互联网上的陌生人跟你的大型路由节点开设通道,只允许少量值得信任的实体跟你开设通道,那么我打赌:
因为这样或那样的原因,这些精挑细选的 “值得信任” 的通道对手以流动性租金的形式,把你赚到的手续费又赚回去;或者,你会发现自己的流动性利用率低下。
事实是,你必须允许互联网上的陌生人,比如 YaiJBOjA(事先声明,绝对不是我哈)跟你开设通道,于是这也意味着,你允许潜在的窃贼成为你的通道对手。你允许别人瞄准你。
因此,你必须假设,只有你本地的签名器(们)是值得信任的,同时免去对通道对手的筛选。你的安全武装必须足够强大,即使别人瞄准你的后背也不怕,这样才能吓退盗贼,而且不应该依赖通道对手,因为通道对手正是最好的盗贼。
你的对手,如果哪怕纯用自己的资金与你开设通道,也可以把自己在通道内的余额都(通过你)发给他们自己控制的另一个节点,然后再偷盗通道中的资金(在他们用尽自己的余额之后,显然,通道中的资金都是你的)。如果他们能够攻破足够多的签名器,或者利用你的多签名方案中的漏洞,就可以盗窃你的资金。
因此,你的 “莫顿叉子” 是这样的:
- 如果你不注重签名器安全性,那么互联网上的陌生人就有可能窃取你的财产。
- 如果你不允许互联网上的陌生人靠近,那么你的流动性就得不到网络的有效利用(首先你也赚不到钱,而且还把自己的钱放在了一个联网的热钱包中,却不能给你或者任何人带来好处)。
(译者注:“莫顿叉子” 指的是横竖都是死的两难局面。)
因此,需要有比 “在我所有的闪电网络通道中都使用一个根签名器” 更好的安全体态。
多签名是一个额外的维度
你可能会说:“但是我的签名器是放在一个 ‘受信任的执行环境(TEE)’ 中的;这个执行环境运行在一个 ‘安全芯片(SE)’ 中;而这个安全芯片是用一个 ‘物理不可仿制功能(PUF)’ 来实现的!!!”
首先(也是最重要的),一个 PUF 仅仅是一个安全的掷骰子程序,用来生成一个嵌入其中的私钥。如果你能够访问 SE 的调试(debug)接口 —— 这是每一家芯片制造商都必然预留了的,因为芯片的制造并非一个完美的过程,总会有瑕疵 —— PUF 就没有任何防护作用。能够检测瑕疵的同一个 debug 接口,也可以用来抽取使用嵌入的私钥运行的计算的中间结果;毕竟,也需要检查这些中间计算电路以验证它是正确制造的。出于实用性,你没法用一条电路计算一切,所以,你需要 “触发器(flip-flops)” 来保存这些中间结果(不然,你就需要一个巨大的,因此也是昂贵且缓慢的电路,因为触发器中存储的中间结果,正是让同一个通用电路,比如乘法器或者加法器,可以在计算的不同部分重复使用的东西),并且,这些触发器也被用作调试接口的一部分(叫做 “扫描链”,自己搜索一下),在制造商那里用来检查制造好的芯片的误差。
这些存储中间计算结果的触发器不可能位于 PUF 中:因为你希望这些触发器以及写入其中的电路,跟你的设计完全一模一样,不然这就是一个不可靠的芯片;而 PUF 是你故意提高其故障率、从而让人们无法实用地克隆它的区域!
因此,这些中间计算结果,可以用来减少你在抽取嵌入其中的私钥时不得不猜测的比特的数量。
此外,提醒一句,制造商可以访问上述 “制造商调试接口/扫描链”,不然这个术语中就不会出现 “制造商” 这个词。你真的相信制造商没有能力读取你如此敬畏的 PUF 中嵌入的私钥吗?你最好还是使用一款不带 “安全芯片” 的 Trezor 签名器,因为置入其中的私钥是你真的可以用投硬币、掷骰子来生成的,你可以使用硬币、骰子以及其它你真的可以控制的硬件,完整那些你真正知道而且理解的操作。
“受信任的执行环境” 只是 “挡住你的小妹妹”,不是 “挡住威权政府”。
(信息来源:我曾经设计过 LCD(液晶面板)显示驱动 ASIC(专用芯片)。在此之前,我曾经逆向工程过 ASIC,我们曾经磨碎每一层金属(字面意思)、用显微镜拍照来搞清楚电路。你知不知道,台湾的大牌芯片制造商只接受你使用前三大 verilog 模拟器软件设计出来的电子逻辑电路(Mentor、Synaptic,第三名是谁你就别管了,你实在要问,我们曾经是一家 Synaptic 的商店);如果你不提供来自其中一家的仿真结果,他们就不会理会你的制造请求。他们完全不信任 Icarus Verilog,只要你提到这个词,他们甚至都不会回复你。)
因此,占有硬件始终等同于占有私钥,无论 “受信任执行环境”、“安全芯片”、“物理不可仿制的功能”,都不改变 “十桩诉讼,占有九胜(possession is 9/10 of the law)”。硬件一脱手,私钥即脱手。
译者注:“possession is 9/10 of the law” 直译过来是 “占有是法律的 9/10”,讲的是在所有权诉讼中,当前占有者大概率会赢得诉讼。作者在这里化用它来强调 “占有硬件(签名器)” 的重要性:既然实际上没有机制可以绝对防止敌人在获得你的签名器后抽取出其中的私钥,那么最重要的就是牢牢把它放在安全的地方,不让敌手得到。
而且,不论如何:即使你拥有一个 ”完美“ 的电子设备,比如一个用现成的零件凑出来的简单 Trezor 签名器(这些零件你真的随处都可以买到,而且不会引人怀疑,因为它们实在是太常见了,没人会想着替换你的供应链,因为这些老旧无聊的微控制器的供应链实在太多了),使用多签名方案依然对你更好:因为窃贼需要偷盗多个这样的电子设备才能窃取你的资金。
多签名给你的装置加入了额外的维度,不论你的装置是一个基于 “受信任执行环境” 的 “安全” 装置(供应商承诺不会盗窃你的私钥),还是一个完全由你占用的真实设备。
多签名,如本文引言的第一部分所说的,意味着 “要让我的资金不安全,必须攻破 k 个设备” —— 只要我们能实现它。
不那么多签名的替代品
除了要求闪电网络发生广泛的变更,还有别的办法可以获得多签名的一些好处。
比如说,每一条闪电通道都有一对专门的公钥,两端各一个。
闪电网络协议自身并不指定从 “根” 密钥(或者从节点 ID)派生出用于各通道的公钥的具体方案。
因此,有可能做到:准备 N 个签名器,不论什么时候,一有通道开启,就选出其中一个签名器来指定用在这条通道中的公钥。因为你用在通道中的公钥跟你的节点 ID 没有什么关系,你可以自由地为各个通道选择任何一个签名器。
这跟现在的闪电网络是完全兼容的,不需要变更协议,甚至不需要等待 PR995 合并。
在这种方案下,如果你有 N 个签名器,而其中一个被劫持了,只会导致你的通道中的 1/N 条失盗。比起只设一个签名器(让所有通道处于风险之中)依然是一项提升。
这里给出了这一方案的例子:rotating-signer-provider 。
这种方案在当下就能部署,也很实用,因为至少它分散了风险。它类似于使用多个节点,区别只在于它会让你形成一个巨大的节点、用多个独立的签名器来管理资金。通常来说,大节点在路由效率上有优势,并且被网络上许多节点偏爱,因此可以增加你的路由费收益。
BOLT 变更:撤销密钥
为了实现真正 k-of-n 多签名的闪电节点,BOLT 的绝对必要的变更是:取消使用 shachain(哈希链条)为远端(remote side,对方)生成各承诺交易的撤销密钥(revocation key)的要求。
为什么这是必要的呢?
回忆一下你的承诺交易所控制的资金,它的实际链内合约是什么样的:
- 二选一:
- 都满足:
- 遮蔽承诺交易已经深度确认(通常是长达两个星期)
- 你的签名
- 都满足:
- 对手的签名
- 撤销密钥
- 都满足:
这里的第二部分就是重点。
你的承诺交易是你可以追回自己的钱的唯一办法。比如说,一个窃贼可以用 TA 自己的钱跟你开设一条通道、把通道中的余额都发送到他们控制的另一个节点,然后关停自己的盗窃节点;这时候,他们已经没有什么可以损失了(除了 1% 的保证经,但这可以算作盗窃的成本)。你必须使用你的承诺交易;不然,你的资金就不安全:变得不可花费。
什么是 “撤销”?
闪电通道的许多术语可能让人头昏眼花:
- 惩罚 指的是你的对手方使用旧状态 单方面关闭 通道时你可以作出的反击。前提是你的对手方广播了一笔承诺交易并让它得到了确认,而这笔承诺交易已经被 撤销 了。
- 单方面关闭 指的是你广播一个状态,并断言它就是这条通道的最新状态。如果你以前已经 撤销 过这个状态(意思是它实际上并非最新状态,因此你的断言是错的),那么它可能会被 惩罚。
- 撤销 指的是你同意一个状态已经是过时的了。它会交给对手方足够多的信息,从而,如果你使用一个已经撤销的状态来 单方面关闭 通道,对手可以稍后 惩罚 你。
所以,这些事都是什么时机发生的?
- 撤销 :
- 每当状态变更的时候就发生,也即:每当在通道中添加或移除一个 HTLC 的时候
- 比如说,你们当前的状态编号为 N。你和对手方都同意移动到 N + 1 。处理方式是 “双手交替”:
- 你的对手签名状态 N + 1 。
- 在这时候,状态 N 和状态 N + 1 都是有效的,两者都可以用来单方面关闭通道而不会遭受惩罚。
- 用 “双手交替” 这个比喻来说,你现在是两只手都抓在绳子上,一只手在上、一只手在下,就这样向上攀爬 —— 在通道内推进状态也是一样的。
- 你撤销旧状态 N 。
- 这个操作将你 不可逆转地锁定 到状态 N + 1,因为现在它是唯一有效的状态了。
- 用 “双手交替” 这个比喻来说,在撤销过程中你松开了下面这只手。
- 你的对手签名状态 N + 1 。
- 任何时候,最多只有两个未撤销的状态;大多数时候,只有一个未撤销的状态。
- 单方面关闭 :
- 发生在你决定签名某一笔承诺交易 —— 让你自己的签名加入你的对手方的签名,那是你在正常的双手交替状态推进过程中得到的 —— 并广播它的时候。
- 没有硬性的密码学机制阻止你使用旧的状态交易(及其签名),只有一种经济激励:只要你使用了旧的承诺交易(你已经撤销过的),你会被惩罚:损失在通道中的所有资金。
- 惩罚 :
- 在你注意到你的对手方用一个旧的(已经撤销的)状态来关闭通道的时候。
问题出在 “撤销” 这个操作上,不是在 “单方面关闭” 或者 “惩罚”上。
撤销 需要传递叫做 “撤销密钥” 的东西给你的对手。然后对手才能使用自己的签名和这个撤销密钥来惩罚旧状态单方面关闭。
K-of-N 撤销密钥的问题
现在,想想:
- 假设你有一个 k-of-n 多签名装置。
- 你如何为每一笔承诺交易生成撤销密钥?
- 你的对手要攻破多少设备,才能偷盗最新的这个撤销密钥?
背后的大问题是:撤销密钥究竟是如何生成的?
答案是:看 BOLT 规范。BOLT 规范指明了:要使用 shachian 。
问题在于,顾名思义,shachain 使用的是 SHA2(“sha”)的迭代应用(“chain”)。
不可能创建一种 k-of-n 的 SHA2 函数啊。SHA2 完全不是线性的,所以不可能创造出 k-of-n 的 SHA2,哪怕 k = n 都不可能。
即使有可能通过一些神秘的密码学魔法创造出一种 k-of-n 的 SHA2 函数(涉及虚拟电路(virtual circuits),将比特 0 和 1 编码成 “同态承诺”),持有 shachain 的一个中间结果(即迭代过程中的 一次 SHA2 的输出)也一样糟糕,因为同样是 shachian 迭代中的 中间 结果 成了 撤销密钥。
这样的虚拟电路通过使用迭代来实现,从而,让中间结果可以被参与虚拟电路的多个参与者知晓;这些虚拟电路方案只保护根输入,不保护中间结果。
问题在于,shachain 的中间结果就是撤销密钥!虚拟电路使用 0 和 1 的同态加密来保护撤销密钥链条的根,这根本无关紧要,因为必须揭晓迭代的中间结果,这些结果 就是 撤销密钥序列。作为一个序列,其中一个密钥就是最新状态的撤销密钥。
为了保护这些中间结果,你需要将整个 shachain 迭代展开并实现为一个巨大的虚拟电路。甚至更坏,每当你必须从 shachain 计算中间结果时,都要展开一个不一样的虚拟电路;而这样的计算次数,根据 BOLT 规范,至少 2(2 的 1 次幂)次,可以多达 2 的 48 次幂。
所以,多方计算的虚拟电路方案也许并不可行。
(警告:我并不是密码学家。关于上述内容请咨询真正懂得同态密码学和多方计算的密码学家。即使栓此,我也不会在 shachain 中使用多方计算,因为我认为那行不通,而且我的密码学造诣找不出行得通的办法。)
再次回顾:
关于 “k-of-n 多签名”,我的愿望是,我持有 N 台设备,只要不到 K 台被劫持,我的资金就是安全的。
如果 shachain 迭代的根值被所有签名器知晓,那么,其中任何一台被攻破 —— 只要一台被攻破 —— 通道对手就能直接窃取所有通道资金,从而打破上述期望。
如果 shachain 迭代的根值不被所有签名器知晓,而是分散在 k 个签名器之间,那么:哪个设备来运行重复的 shachain 迭代、给对手提供撤销密钥呢?只要这一个持有根密钥(甚至是一个中间状态)的设备被攻陷了,就同样等价于持有最新的撤销密钥,也就允许盗窃资金。
再说一遍,“k-of-n” 的意义在于让盗贼必须攻陷至少 k 台设备。即使只有一台设备持有撤销密钥,哪怕只是暂时的,攻破这一台设备也将允许盗窃。因此,从道德上来说我们不能管这叫 “k-of-n 多签名”,因为它并不是终端用户说“k-of-n” 时期待的那种东西。
因此:shachain 必须从 BOLT 规范中移除。
BOLT 修改提议
我提议添加一对 “特性比特”,称为 “no_more_shachains”,可以是 globalfeatures(全局特性)也可以是 localfeatures(局部特性):
- 奇数比特:我不会对对手执行 shachain 验证,但依然会给对手提供 shachain 有效的撤销密钥。
- 这为依然期待传统 shachain 的不升级节点提供了后向兼容性。
- 这意味着我会存储所有的撤销密钥,而不是使用 shachain 加速结构将存储空间压缩到 O(1) 量级。
- 这让不使用 shachain 的节点可以跟我开设通道,也让我可以桥接他们以及要求 shachain 的节点。
- 偶数比特:我不会提供 shachian 有效的撤销密钥,也不会验证对手提供的撤销密钥的 shachain 有效性。
这个 globalfeatures 比特让 k-of-n 多签名节点可以识别出自己可以安全地与之建立通道的其他节点。注意,“创建 BOLT8 连接” 并不意味着 “创建一条通道”,而一个 k-of-n 节点可以建立 BOLT8 连接、下载 gossip 图,然后搜索 globalfeatures 以寻找 no_more_shachains 。
然后,一个 k-of-n 闪电节点可以启用偶数比特,而网关节点可以启用奇数比特。最终,我们希望所有节点都至少启用奇数比特并且我们可以之间使用偶数比特,哪怕是 1-of-1 的闪电节点。
以上设计尊重了 “可以保持奇数位” 的哲学,传统节点依然可以跟启用了奇数比特但没有启用偶数比特的节点互操作。启用了奇数比特的节点也可以跟需要使用偶数比特的节点互操作。k-of-n 节点需要启用偶数比特,但可以跟启用了奇数比特的节点互操作。
请注意,即使仅仅启用奇数比特,也会让具有较长历史的通道的存储体积变成三倍,虽然通道拼接允许你修剪这些数据。
空间占用变成三倍的原因是:
- 使用带锚点的承诺交易,唯一的状态变更形式是添加和移除 HTLC 。
- 因此,每一个 HTLC 都触发 2 次状态变更。
- 每一个历史 HTLC 都是大约 32 字节的哈希数据。
- 每一次状态变更都需要一个额外的撤销密钥。
- shachain 方案只要求常量的存储空间,但抛弃它之后,我们就需要线性增长的存储空间。
- 每一个撤销密钥都是大约 32 字节的数据(一开始是公钥,但在撤销之后,我们可以用私钥来替代公钥)。
- 因此,一个 HTLC 将需要 32 字节来存储哈希值、 32 字节为 添加 这个 HTLC 的承诺交易存储撤销密钥,再有 32 字节为 移除 这个 HTLC 的承诺交易存储撤销密钥。
- 以前,使用 shachain,我们只需要 32 字节来存储这个哈希值;因此,最终通道历史的存储空间会膨胀为 3 倍。
并非问题:MuSig2 Nonce 通知
MuSig2 是一套两轮的协议:
- 第一轮,交换对
R的投入。 - 然后,交换碎片签名
s。
具体到 Taproot 通道,这个 “交换碎片签名 s ” 的过程实际上只是一方给另一方发送碎片签名 s;另一方把这个碎片签名 s 存在硬盘中。如果另一方决定使用这个状态单方面关闭通道,TA 会生成自己的 s、将两者加在一起,生成最终的签名 R, s ,然后将签名和交易广播到区块链。
嵌套的 MuSig2 很大程度上也是相同的两轮协议,嵌套签名器们在内部完成每一轮、将他们的结果汇总、组合,然后执行在它们之上的层级的信息交换。
因为有两轮,PR995 指明了使用当前交换 s 的时机来发送用在 下一次 签名会话中的 R 的 nonce 投入。这减少了通信往返的次数,对于我们身在澳大利亚的同事来说,考虑到其中的延迟,这很重要。
K-of-N 的 MuSig2 ?
虽然 MuSig2(以及 “嵌套的 MuSig2”) 都被设计成 n-of-n 的签名协议,我要指出, “k-of-n” 的 FROST 协议实际上是一套可验证的 Shamir 秘密分割方案,在实际签名过程中使用了 MuSig2 签名协议的许多部分。也就是说,并非使用 MuSig/MuSig2 密钥组合函数(与 MuSig2 签名协议不同),FROST 使用自身的多方计算方案来生成一组你必须存储的 “碎片”,每个碎片对应你的一个联合签名人,再加上你自己的密钥碎片,以及一个集体公钥。
在签名的时候,FROST 基本上使用的是跟 MuSig2 非常相似的东西(也正因此,从机制上看,结合 FROST 和 “嵌套 MuSig2” 应该 是有可能的; 然而,还不能保证 “嵌套 MuSig2” 的安全证明可以延申到 FROST-in-MuSig2 !)
在签名的时候,你要找出在线的签名器(凑出 k 个联合签名器),然后所有在线的签名器根据 MuSig2 签名方案,生成第一轮的对 R 的投入、相互交换,然后生成第二轮的碎片签名 s。
问题在于,在 PR995 提议中,每次我们要为本次签名会话发送第二轮的碎片签名 s 时,也要 发送为下一次签名会话准备的对 R 的投入。
所以,比如说,假设:
- 我们有三台签名器,Alice、Bob 和 Carol,组成一个 2-of-3 的闪电节点。
- 在当前的签名会话中,Alice 和 Bob 在线,它们生成了对
R的投入、处理了,然后将组合结果发给了对手方。 - 然而,在下一次签名会话以前,Alice 就死在了终于到来的机器人起义的流弹中。
- Bob 唤醒了 Carol 。
- Carol 并不知道 Alice 使用的 nonce 秘密值,因此无法使用结合好的 Alice+Bob nonce 来签名。
幸运的是,虽然 PR995 提议指明了对手方应该记住 nonce 投入直至下一次签名挥发,如果丢失了 BOLT8 连接,这个 nonce 会被丢弃。在重新连接是,发送一条 channel_reestablish 消息,就可以发送新的 nonce 。
所以,在上面这种情形中,Bob 和 Carol 可以不管机器人起义,继续签名:直接重新连接对手方,然后强迫使用一个新的组合好的 Bob+Carol nonce 。
因此,nonce 投入轮,对我们的 k-of-n 多签名也不是一个问题。