关于根因结论怎么读。 Blockstream 至今没有发布事故报告。下文里的每一段都标了出处:标「已核实」的部分,是我自己拿 mempool.space 与 blockstream.info 的接口逐笔对过、或者直接在
ElementsProject/elements仓库里读到的;标「推测」的部分,是我对漏洞的复原——理由很硬,但不是官方结论。等官方报告出来,我会回来标注哪些推断对了、哪些错了。
一条联邦锚定的链,只有一个数字是绝对不能算错的:
流通中的 L-BTC ≤ 联邦多签里锁着的 BTC。
Liquid 提供的其他一切保证——保密金额、一分钟出块、白名单提现——全都架在这个不等式之上。2026 年 9 月 6 日,这个不等式破了。有人造出了从来没有人充值过的 L-BTC,然后把它提成了真的比特币。这个 15 人联邦的硬件安全模块签署了这笔提现,因为站在每一个模块自己的位置上看,账是平的。
这篇文章要讲的,就是账为什么看起来是平的、而实际上不是。两条链上的交易是我自己追的,事发前后那批 Elements 提交也是我自己读的。由于这起事故真正有意思的部分藏在保密交易所依赖的密码学里,我会把这套密码学也认真讲一遍:佩德森承诺到底保证了什么、范围证明为什么不是可选项、以及「资产」这个词在一份证明内部意味着什么。凡是我在推断而不是在引述 Blockstream 的地方,我都会说明。
Liquid 真正信任的是什么
Liquid 是一条由联邦运行的比特币侧链。这里只有两个角色要紧。
功能节点(functionary)负责共识。它们签署 Liquid 区块,并且由其中 15 个里的 11 个共同签署那些从锚定池里放币的比特币交易。每个功能节点都跑着一台硬件安全模块,只有当它自己那台 Elements 节点判定这笔提现有效时,模块才会签名。
提现路径走的是 PAK(Peg-out Authorisation Key,提现授权密钥)体系:功能节点只把币放到 PAK 白名单里的地址,而联邦成员之一 SideSwap 运营着一个公开的提现服务,普通用户都从这里走。Liquid 上的金额是保密的,所以一个输出并不会写着 1.5 L-BTC;它携带的是对金额的一份承诺、一份证明隐藏金额没有出格的范围证明,以及一份证明隐藏资产确实来自本交易输入的满射证明。
把这些拼起来,就是攻击者必须击穿的那条信任链。
HSM 是最后一道防线,但 HSM 对经济学没有任何看法。它检查的是:这笔提现销毁的 L-BTC 与放出的 BTC 是否等量、收款地址是否在白名单里、以及 Elements 节点是否接受了这些交易。它不会去问这些 L-BTC 当初是不是被合法地造出来的,因为它默认共识已经保证过了。整个故事就出在这个默认上。
链上到底发生了什么「已核实」
以下是我从 mempool.space 与 blockstream.info 的接口上复原出来的,时间均为 UTC。
13:53:10,凭空铸币的那笔交易。 Liquid 区块 4,050,336 里有一笔交易 c652a1047ff5…,64 个输入、6 个输出。Blockstream 自家的浏览器接受了这个区块,而 mempool.space 运行的那个独立 Liquid 浏览器没有:写这篇文章时,它的链尖仍然停在区块 4,050,335,时间戳 13:52:10,正好早一个块。两个浏览器,两个链尖,分歧点精确地落在一个区块上。这就是共识漏洞在链上留下的指纹,两边为什么会不一致,后文再回来讲。
14:06:10,提现请求。 Liquid 区块 4,050,349,交易 ce4caece413c…,把 3,996.01834922 L-BTC 销毁进一个提现输出(资产 id 6f0279e9ed04…,即 L-BTC)。这就是那份让联邦放出等量 BTC 的请求。
14:28:56,放款。 比特币区块 965,783,交易 8db751a650ae…b140:83 个输入,动用 4,019.44 BTC,其中一个输出是 3,996.01834922 BTC,去往 bc1qgslsydz…6wt7p。另外 12 个输出是回到锚定池的普通找零。
| 链 | 金额 | 时间 |
|---|---|---|
| 提现中销毁的 L-BTC | 3996.01834922 | 14:06:10 |
| 联邦放出的 BTC | 3996.01834922 | 14:28:56 |
两个金额精确到聪都一样,相隔 22 分钟。从 HSM 的视角看,这就是一笔再普通不过的提现:L-BTC 销毁了,放出的 BTC 等量,收款地址经由 SideSwap 在白名单内。每一道本地检查都通过了。唯一出错的地方在上游——那些被销毁的 L-BTC,是 13 个区块之前凭空变出来的。
储备金。 联邦的锚定地址 bc1qdlld6antmv4xug…wxxr 从大约 4,200 BTC 掉到了 197.47 BTC,约 95% 的储备在一笔交易里离场。
冻结。 Blockstream 关掉了自己的桥节点,暂停了锚定。它的浏览器显示最后一个 Liquid 区块是 4,051,232,时间为 9 月 7 日 04:49,里面只有一笔 coinbase。放款之后,功能节点又签了大约 15 小时的空块,然后彻底停止签名。
追着钱走「已核实」
多数事故复盘写到那笔盗币交易就停了。更有意思的是这些币接下来做了什么。
收款地址随即把 3,995.99999857 BTC 转给了 bc1ql4mfu6aundtkksxklfajs2h3t9nzcd6gyqjlte,把零头 0.01834922 拆去另一个地址——做这件事的人把赃款抹成了整数 3,996 BTC。接下来的 24 小时里,这个归集地址一直躺着约 3,998.5 BTC,没有一聪流向混币器、交易所或剥离链。币就那样明晃晃地停在原地,而双方通过唯一彼此都信得过的渠道对话:比特币上的 OP_RETURN 输出。
这段对话的公开部分,按顺序是:9 月 6 日 18:30,攻击者留言「我们是白帽,链上联系我们」;19:31,Blockstream 回复并给出安全邮箱;20:38,攻击者留下 Signal 账号;9 月 7 日 09:19:46,Blockstream 发出一条 PGP 签名的消息,「桥节点已打补丁,可以安全归还资金」。12:43 攻击者作答,这条消息是我直接从链上解出来的:开头是明文的「请再确认一次,我们把币退回 bc1qdlld6antmv4xug…」,接着是「关于漏洞修复的更多细节:」,之后整段载荷是一个用 Blockstream 公钥加密的 PGP 块。攻击者在一个公开账本上,私下传递漏洞细节。15:31 又跟了两条加密消息。
然后,在 9 月 7 日 16:09:25,交易 a6d697a25266ce3c… 把恰好 3,400 BTC 退回了联邦的锚定地址。18:35 又发出两条加密消息,21:03 该地址广播了一个两字节的 OP_RETURN,解码出来是 :(。截至 9 月 7 日夜间,归集地址上还留着 598.4998 BTC,约为被取走金额的 15%,而联邦地址上是 3,597.47 BTC。留下的这部分究竟是谈妥的赏金还是单方面的截留,公开信息里没有答案。那个哭脸是唯一留在记录上的评论。
还有一个细节我没在任何报道里见过:攻击者的地址在一天之内变成了公共广告牌。陌生人往它上面发粉尘,并附上 OP_RETURN 留言:一则自带混币功能的「免 KYC」兑换服务广告,明摆着是冲着掌握这些币的人去的;一个蹭热度的迷因币发行;关于如何洗掉这笔赏金的不请自来的建议;以及一连串关于幕后主使是谁的匿名指控——那些我不会复述,因为链上没有任何东西支持它们。比特币上最受注视的那个地址,24 小时内成了广告位和涂鸦墙。
本该守住这个不变量的数学
要看清 L-BTC 怎么可能被凭空造出来,先得看清保密交易究竟证明了什么。我打算分三层搭起来,因为漏洞恰好长在第二层与第三层的接缝处。
佩德森承诺:把一个数藏起来,但不丢掉算术
在 secp256k1 曲线上固定两个点:(通常那个生成元)与 ,并且要求没有人知道满足 的标量 。对数值 的佩德森承诺是
其中 是随机的致盲因子。有两条性质让它好用。它是隐藏的:因为 均匀随机, 是一个均匀随机的点,透露不出关于 的任何信息。它是绑定的:想把同一个 打开成另一个数值,就得解出 ,那等于交出 关于 的离散对数,而没有人手上有这个东西。
真正让它成为账本工具的,是它对加法同态。承诺相加,底下的数值与致盲因子也跟着相加:
于是一笔交易可以在不透露任何金额的前提下证明收支相抵。如果输入金额之和等于输出金额之和,而发送方又把致盲因子选得让两边之和也相等,那么
也就是无穷远点,验证者要检查的正是这一条。在 Elements 里,它叫 secp256k1_pedersen_verify_tally。一个公开的、未致盲的金额,无非是 的承诺,所以同一个检查也覆盖了混合交易。
范围证明:光有同态还不够
这里有个窟窿。这些标量活在模曲线阶 的整数里,而 是一个略小于 的数。在这套算术里, 就是 。于是一笔交易,一个输出承诺 ,另一个输出承诺 ,对着总额为零的输入完美平账。验证者看到的是无穷远点,于是满意了。而发送方刚刚印了一百万枚币,把那负一百万停在一个永远不会有人去花的输出里。
范围证明堵上了这个窟窿。它是一份零知识证明,声称 里面的数值落在一个诚实的小区间内——在 Elements 是 ——从而没有任何输出可以偷偷为负。Elements 用的构造把 拆成若干位,对每一位分别承诺,再用 Borromean 环签名证明每个位承诺都取自它那几个被允许的值之一,同时不说是哪一个。这份证明还顺带携带了用收款方密钥加密的数值与致盲因子,因此只有收款方能把它「倒放」出来,知道自己收到了多少。SideSwap 的钱包正是靠这一步,才知道有客户给它转来了 4000 L-BTC。
这份证明被验证时的两个细节,对后文很关键。验证者拿到的不只是 和证明;它拿到的是 、证明、这个数值所依附的生成元 ,以及一份额外承诺——在 Elements 里就是该输出的 scriptPubKey。所以这份证明是一个这样形状的命题:「在这个生成元之下、并绑定到这段脚本, 可以打开成某个落在区间内的 。」换掉生成元或者换掉脚本,这份证明就是在讲另一件事了。
资产绑定:生成元赋予承诺以含义
整个漏洞就绕着下面这个想法转,我尽量说得直白:一份承诺不只是一个点 。 究竟代表多少钱,完全取决于你拿哪个生成元去读它。
Liquid 在一条链上承载多种资产,还要把一个输出持有的是哪种资产藏起来,所以它需要「每种资产一个生成元」。每种资产 都有自己的生成元(把资产 id 哈希到曲线上的一个点,再加以致盲以隐藏资产种类)。于是数值承诺现在是对着资产生成元建起来的:
满射证明负责说明 确实是本交易输入所携带的资产生成元之一,这样一个输出就无法宣称自己是一种从未进来过的资产。资产这一面被覆盖了。金额那一面没有,而这正是关键所在。
取一个固定的点 ,拿两个不同的资产生成元 与 去读它:
同一个曲线点,在一种读法下是「 单位的 」,在另一种读法下是「 单位的 」,而 与 之间可以毫无关系。点 从头到尾没有变。变的是验证者被要求接受的那个关于它的命题。 范围证明的作用,就是把其中一种读法钉死:一份为生成元 做的证明,保证的是「 之下的那个 」落在 里,它对 什么都没说。如果同一份证明在 之下被直接采信而没有重新验证,那么 从来就没有被约束过。
范围证明与它当初所针对的那个生成元之间的绑定,就是这里的承重构件。不过请只带走这个事实、别带走那个结论:同一个点在不同生成元下代表不同数值,并不等于攻击者可以自己决定要它代表哪个数值。他真去试的时候会撞上一堵墙,而后半篇文章大半就是在讲这堵墙。
公开记录能确定什么、不能确定什么「推测」
Blockstream 只说了这些 L-BTC「是通过 Elements 软件中的一个漏洞被创造出来的」,后来又说桥节点已经打了补丁。它没有点名是哪个漏洞。下面是我从公开仓库里做的复原,请把它当成一个很硬的假设,而不是已确认的事实。
收支表逼出来的东西。 收支检查只问金额加起来对不对;而阻止其中某个金额偷偷为负的,是另外一件东西——范围证明。所以任何一次 L-BTC 通胀都会留下同一个脚印:一个被伪造出来的正值输出,以及某处一个偷偷为负、用来吸走它的输出,而后者的范围证明从未被真正执行。这一层是算术逼出来的,跑不掉。 但究竟是哪条代码路径放行了它,并没有定论——本节余下的篇幅,讲的正是公开记录能推到哪一步、又在哪里停住。
验证范围证明很贵,所以 Elements 会把结果缓存起来。src/script/sigcache.cpp 里的 CachingRangeProofChecker::VerifyRangeProof 把证明哈希成一个键,如果这个键已经在缓存里,它立刻返回 true——早于解析承诺,早于调用 secp256k1_rangeproof_verify,也早于那道「可花费的输出不能有零下界」的合理性检查。问题就出在这个键是用什么料做的。
我把 ElementsProject/elements 克隆下来,翻了事故前刚刚发生的改动。在 master 上这个提交是 c26d719c2,摘到 23.3.x 发布分支上是 212c43f47。提交信息是「fix: range proof cache bind to asset and scriptpubkey」,一共三行。修改前:
void ComputeEntryRangeProof(uint256& entry, const std::vector<unsigned char>& proof, const std::vector<unsigned char>& commitment) { CSHA256 hasher = m_salted_hasher_range_proof; hasher.Write(proof.data(), proof.size()) .Write(commitment.data(), commitment.size()) .Finalize(entry.begin());}修改后:
void ComputeEntryRangeProof(uint256& entry, const std::vector<unsigned char>& proof, const std::vector<unsigned char>& commitment, const std::vector<unsigned char>& asset_commitment, // 新增 const CScript& scriptPubKey) { // 新增 CSHA256 hasher = m_salted_hasher_range_proof; hasher.Write(proof.data(), proof.size()) .Write(commitment.data(), commitment.size()) .Write(asset_commitment.data(), asset_commitment.size()) // 新增 .Write(scriptPubKey.data(), scriptPubKey.size()) // 新增 .Finalize(entry.begin());}补丁之前的键是 hash(proof ‖ C):证明的字节,加上数值承诺,再无其他。而验证器本身在 src/confidential_validation.cpp 里,是拿着全部四样东西被调用的:
secp256k1_rangeproof_verify(ctx, &min, &max, &commit, proof.data(), proof.size(), scriptPubKey…, // 额外承诺,被绑进证明里 &tag); // tag 就是资产生成元也就是说,缓存记住了「这份证明对这个 是没问题的」,却忘了它当初是在哪个生成元之下、针对哪段脚本才没问题。先用一份对资产 合法有效的证明把缓存喂热,再把一模一样的证明与承诺配上一个不同的资产承诺递进去,缓存就会说是——而验证器压根没见过那个新的生成元。用上一节的话说:证明与生成元之间那道承重的绑定,根本就不在缓存键里。
在收支这一层,必然发生过什么
收支规则 secp256k1_pedersen_verify_tally 只检查输入承诺之和与输出承诺之和是不是同一个点。它没有「正数」这个概念;而由于数值活在模曲线阶 的世界里, 的行为与 完全一致。所以来走一遍玩具例子。假设攻击者手上有一个真实的、价值 10 L-BTC 的输入。一笔诚实的交易会把它花成总额为 10 的若干输出。一笔制造通胀的交易则造了两个输出。
收支核对看到的每一个数都自洽:输入合计 10,输出合计 10,而且致盲因子也被挑得让两边相消。可是输出 B 是一个完全正常、完全可花的 1,000 L-BTC,其中只有 10 是真的,另外 990 来自虚空。输出 A 就是那 990 的藏身之处:一个为负的输出,唯一的职责是吸走盈余,好让账面平掉。它永远不必被花掉;甚至可以做成不可花费的 OP_RETURN 输出,让它悄无声息地消失。输出 B 才是被带走的钱。
这里面完全不需要发行新资产,而区块 4,050,336 里的那笔交易也确实没有任何发行——这一条我在链上查过。这一层我是有把握的,因为它由算术逼出,而不是从代码里推出来的:只要存在无锚定的 L-BTC,就必须存在某个像 A 这样的输出,而且它也不可能有合法的范围证明—— 就是 ,天文数字般地落在 之外。输出 A 能待在一个区块里,只可能是因为它的范围证明从未被真正执行。
不成立的是接下来那一步;而大多数关于这起事故的分析——包括我自己的初稿——恰恰在这里走得太快了。
范围证明缓存为什么闭合不了这条路径
范围证明只有第一次才贵。CachingRangeProofChecker::VerifyRangeProof 拿 hash(proof ‖ C) 去查表,一旦命中,它根本不调用验证器就直接返回 true。
一旦键命中,资产生成元与脚本就再也到不了验证器面前。顺着这里,一个故事会自己写出来:先用一份在自己生成元下诚实通过验证的证明把缓存喂热,再把一模一样的证明与承诺配上另一个生成元递到输出 A,缓存就放行了。这是我最初写下的版本,也是外面流传的版本。它经不起把数字代进去。
收支这一层很容易:模算术下 。难的是让那个 的输出过关。它需要自己的范围证明被跳过,而缓存只会跳过一个它此前已经从一次诚实验证里存下来的 (proof, C) 组合。于是,要让这个无底洞的点
曾经被诚实地缓存过,它就必须在某个可用的资产生成元 之下通过范围验证,也就是
把两种写法对齐,就逼出 :那个播种资产的 NUMS 生成元 必须是 的一个已知标量倍数。要找到具有这种性质的资产 id,就是离散对数问题——而 NUMS 生成元的全部用意,正是让这件事做不到。所以单靠范围证明缓存漏绑资产与脚本这一条,根本没法把那个无底洞喂进缓存。 伪造满射证明也救不了它:满射证明只改一个输出的资产标签,而佩德森收支核对加的是真实的承诺点,并且对每个生成元分别独立守恒—— 的内容没法从标签那一侧注入进去。
两个并列的候选注入口,都未确认
于是只剩两个候选,都没有被确认。保密交易一侧: 范围证明缓存漏绑,叠加同一次 secp256k1-zkp 升级里修掉的满射证明随机数与 s 值复用(PR #1585 与 #1586)。它与链上形态吻合——一笔 64 输入、没有任何发行的保密转账——但基于公开数据,我没法把它闭合成一条真能造币的路径,原因就是上面那堵墙。充值一侧: 同一批里还发布了 DecomposePeginWitness 与 MerkleBlock 的 SPV 证明解析修复。一笔伪造的充值会直接铸出明文的 L-BTC,路上没有离散对数这堵墙,因此它是闭合的——但它与那笔交易的保密形态对不上。究竟是哪一条,没有 Blockstream 的事故报告就定不了。
有一点横跨两个候选。范围证明缓存只在内存池路径上被写入:AcceptToMemoryPool 以 cacheStore = true 跑验证,而 ConnectBlock 只查缓存、刻意不写入。所以任何一条经过这层缓存的假设,都会推出「一个节点对区块 4,050,336 的判决,取决于它自己的内存池里是否恰好先收到了相关交易」——而这正是我们观察到的形状:mempool.space 的 Liquid 节点停在 4,050,335 之后再没动过,Blockstream 的则继续往前走。分裂本身是事实;无论造成它的究竟是什么,它都意味着有效性取决于节点各自的本地状态——对一条共识规则来说,这大概是最糟糕的性质。
什么能给出定论: Blockstream 在事故报告里点名子系统,或者一次可控的 regtest 复现。仅凭公开的、致盲过的链,加上攻击者那几段 PGP 加密留言,确切的构造是还原不出来的。什么约束着任何答案(这些是我在链上核实过的):那笔交易没有任何发行,也没有任何输出复用了某个输入的承诺——所以若真有喂饱缓存的承诺复用,它只能来自别的交易,而不是这笔铸币交易内部。
时间线上的那道缝「已核实」
这个提交的 git 作者日期是 8 月 3 日,但那只是它在本地被写出来的时间;而那些 cherry-pick 出来的提交对象所携带的 9 月 2 日、9 月 3 日时间戳,同样是本地的——它们记录的是某位开发者生成这些 cherry-pick 的时刻,而不是任何东西变成公开的时刻。真正把一个修复放上公开分支的是合并,这里有三次合并要紧。描述写着「修复若干在 LLM 扫描中发现的小问题」的 PR #1592,在 9 月 1 日把这个修复放上了 master。PR #1595 在 9 月 3 日把它带进 23.x 分支。而 23.3.x 发布分支——功能节点的二进制正是从这里切出来的——直到 PR #1599 于 9 月 6 日 19:20(UTC) 合并才收到它,比 14:28 那笔放款晚了大约五个小时。事故发生时最新的正式版本是 2026 年 4 月的 23.3.3,其中同样不含这个修复。
所以公开窗口是 9 月 1 日到 6 日:整整五天,一个三行的、事关共识的修复以「自动扫描捡出来的小清理」的名义可读地摆在 master 上,而真正给签名者供货的那条发布分支,直到币已经没了之后才拿到它。在一条由 15 个签名者各自按自己节奏升级的联邦链上,「已合并」与「已部署」离得很远,而这个距离对任何读仓库的人都是可见的。这就是开源共识代码那个令人不适、却又完全正常的性质:一个修复就是一次披露。 每一个与安全相关的提交,都在告诉盯着看的人当初错在哪里;而对于任何尚未发布、尚未安装的东西,那份 diff 就是一张地图。一批由 LLM 扫描浮出水面、以例行加固名义合并的修复,从内部看很容易被低估,从外部看却很容易被读懂。这一批里,有把范围证明缓存绑回资产与脚本的,有收紧满射证明随机数的,还有加固充值见证解析的。无论最后要紧的是哪一个,这批东西整体读起来,对盯着看的人来说就是一份「值得试试的地方」清单。
我刻意不去猜是谁用了这个窗口、又是怎么发现这个漏洞的。公开记录回答不了这个问题,而这些事实本身已经够锋利了。这条时间线真正确立的,是一个具体且会反复出现的风险:一个共识修复从落进公开仓库,到抵达真正执行共识的那些机器,中间的间隔就是暴露窗口,而今天让这个窗口变短的,除了签名者的升级速度之外别无他物。Blockstream 那句「桥节点已打补丁」,是在放款之后 19 个小时才发出来的。这已经算快,但它仍然是 19 个小时之后。
一般性的教训:签名者检查了错误的东西
把保密交易那套机械剥掉,这就是好几起跨链桥事故的同一种失败。签名者是健康的:没有密钥泄露——Blockstream 是这么说的,而那个修复也与此一致。每一台 HSM 都完美地执行了自己的本地规则。没有任何签名者执行的,是那个全局不变量:供应量对储备量。因为每一个都默认共识已经做过了,而共识有个洞。
Ronin 的验证者签署了一笔满足签名门限的提现,而门限本身已被攻陷。BNB Bridge 的 IAVL 证明验证器接受了一份伪造证明,凭空铸出了 BNB——那是发行完整性的失败,而不是托管被攻破。Liquid 的 HSM 签署了一笔在本地看来收支相抵的提现,而那些 L-BTC 本身是伪造的。在每一起事故里,钱之所以动了,都是因为持有密钥的一方验证了一个本地谓词,而没有任何一方去验证那个全局的。保密交易让这件事更糟而不是更好:当金额与资产都藏在承诺后面时,「供应量等于储备量吗」就不再是一个人或者一个简单监控能一眼看出来的东西。这个不变量的强度,完全取决于那套代替「看一眼」的证明系统;而在这里,证明系统本身是健全的;出问题的,是决定何时去跑它的那套机械。
本可以抓住它的那道控制
这一类漏洞的每一份复盘都会建议纵深防御,而几乎没有人真的在跑那道最要紧的检查:一个与共识不共享代码的、独立的「供应量对储备量」监控。它并不需要看穿保密性,它只需要两个公开的数字。
# 伪代码:一个共识之外的瞭望塔for each new Liquid block B: lbtc_supply = pegins_total - pegouts_total # 来自锚定事件,累计 btc_reserve = utxo_sum(federation_peg_address) # 来自比特币链 if lbtc_supply > btc_reserve + DUST_TOLERANCE: freeze_peg_signing() # 至少也要:呼叫每一个功能节点 alert("supply exceeds reserve by", lbtc_supply - btc_reserve)微妙之处在于,L-BTC 的供应量本身就是保密的,所以 pegins_total − pegouts_total 必须从充值与提现事件里去追——这两处会在边界上暴露明文金额——而不能靠把隐藏的余额加起来。这是诚实的工程难点,也正是需要做设计的地方。但这道检查很便宜,而且它独立于那条出问题的代码路径;在 9 月 6 日,只要区块 4,050,336 试图把供应量撑过储备量,它当场就会响——早于区块 4,050,349 里的那笔提现,更远早于 HSM 签名。一个在签名之前先去查全局不变量的签名者,与一个默认共识已经查过的签名者,是两种不同的安全模型。
这正是我在一个小的开源项目 bridge-invariants 里要做的事:把近年的跨链事故复现成会失败的不变量测试,再跑一遍那个本可以抓住每一起事故的监控。Liquid 是第一个案例。
文中链上数据于 2026 年 9 月 8 日直接经 mempool.space 与 blockstream.info 接口核对;源码引用对应 ElementsProject/elements 仓库。