Cover photo

KZG 仪式的经验教训

什么是KZG仪式?

这篇文章的作者是**Nico,他是隐私和扩展探索团队 (PSE)的前端开发人员。Nico 总结了他在开发和部署KZG Ceremony**过程中所学到的知识和面临的挑战。

Rollups 和 L2 是一种在不牺牲安全性和去中心化的情况下扩展以太坊的方法。它们将执行抽象到第 2 层,并将结果数据发布到第 1 层。通过降低 L1 数据存储成本,汇总可以大大降低交易费用。

大多数区块链(包括以太坊)使用哈希来链接交易、区块和后续交易。例如,要获得块哈希,您需要对其所有交易信息加上前一个块哈希进行哈希处理。这意味着节点需要存储整个区块链历史记录才能同步有效状态。**如果我们使用多项式承诺(可以在这里**找到一个很好的解释)而不是哈希承诺,我们可以减少在 L1 上存储数据的需要。特定网络 L2 节点仅存储特定信息。

多项式承诺需要加密的秘密才能发挥作用。如果一个人生成一个秘密并对其进行加密,那么该人就可以形成满足多项式承诺的无效证明(又名:为 L1 中发布的数据创建欺诈性证明)。为了防止这种情况,我们可以让 N 个参与者生成自己的秘密,并按顺序将其添加到主秘密中。如果只有一个参与者忘记了他的秘密,那么主要秘密就安全了。这个过程称为仪式,因为我们将使用 KZG 方案,所以我们将其称为“KZG 仪式”。

架构概述

我们需要每个参与者在他们这边(客户端)生成一个秘密。贡献计算必须是连续的,因此我们需要一个中央排序器来控制参与者的队列(谁有当前轮次,谁是下一个,检查贡献是否正确执行等)。尽管排序器是一个集中式 Web 服务器,但它可以执行的唯一恶意攻击就是审查您的参与。所有秘密生成和贡献计算都在客户端完成。总结一下:

  1. 多个客户端程序将生成一个秘密,并轮流将其添加到主要秘密中。

  2. 一个集中排序器,可以协调参与者的队列并检查每个贡献的有效性。

  3. 客户端和服务器的通信将通过 API 完成。主要秘密文件(称为结构化参考字符串:SRS)是具有非常大数值的 JSON。

加密库实施

该过程的核心部分依赖于加密函数来计算贡献并验证其有效性。这些函数是由以太坊核心开发团队用 Rust 编写的,因为 Rust 的默认安全属性及其对使用 WASM 的 Web 浏览器的可移植性。此代码用于定序器和某些客户端实现中。

我们需要 3 个函数:

  1. contribute(previous_SRS, secret)返回new_SRS:将随机生成的秘密添加到 SRS(一堆**组乘法**)中。

  2. contribute(previous_SRS, secret, identity)returns new_SRS:执行前面的函数,并使用秘密作为密钥来签署您的输入身份。通过这种方式,您可以将您的身份与您的贡献联系起来,以便将来您参与本次仪式得到认可。它还可以帮助排序器知道谁已经做出了贡献,谁还没有做出贡献。SRS 贡献有一个专用于此签名的属性。

  3. check(previous_SRS, post_SRS)returns true/false:检查贡献操作是否正确执行。它没有透露秘密,但可以看出参与者使用了之前的SRS作为基础,并且没有发送一些随机值。

为了提高可移植性,我们创建了一个用 Rust 编写的包装器存储库,它使用加密库作为包。这样,加密代码就从客户端使用的包装器/API 代码中抽象出来。它还帮助我们配置所需的工具,以便在浏览器中高效运行 WASM(例如 wasm-pack、rayon 等)。

排序器实现

排序器是一个 Web 服务器应用程序,它使用加密库作为包来检查参与者的贡献。为了防止垃圾邮件/机器人攻击,实施了登录功能。它允许参与者使用他们的以太坊钱包(SIWE 模式,允许多个钱包连接兼容的钱包)或他们的 GitHub 帐户**登录。**在开发阶段,我们认为在特定快照之前要求 3 个或更多事务就足够了,但我们错了。在生产中,许多垃圾邮件机器人试图贡献,因此等待时间呈指数增长。我们相信这些机器人试图种植代币或空投(我们没有,将来也不会)。

在参与者的协调过程方面,我们决定使用大厅策略而不是队列策略。大厅意味着参与者必须登录并在特定时间范围内不断 ping 定序器,直到他们被随机选择参与下一个时段。通过这种方式,我们可以确保参与者(客户程序)处于活跃状态。该仪式收到了超过 110,000 份捐款,因此,如果数千名参与者花费的时间超过预期(约 90 秒),则等待时间可能会呈指数级增长。同时,大厅为每个人提供了被选择进入下一个位置的相同机会。因此,如果参与者客户端突然停止对定序器执行 ping 操作,他们可以重新加入大厅,并且仍然有与以前相同的机会(与先进先出队列机制相反,该机制会将不幸的参与者排到队伍的末尾)。我们预计大多数参与者将在日常计算机上使用浏览器,并且大多数人没有良好的互联网连接。

我们将“恶意”用户定义为在获得参与机会后发送损坏的 SRS(或根本不发送 SRS)的客户端程序。这会浪费时间并延迟其他参与者做出贡献。排序器将能够检测损坏的 SRS,将其列入黑名单,并且之后不会让他们参与,除非他们通过官方渠道(Telegram、Discord、Twitter 甚至 GitHub 问题)明确提出要求。

排序器实现了不同的 API 路由来完成其任务:

  1. /info/current_state:向参与者和任何想要在特定时间检查仪式状态的人提供初始和后续的 SRS。

  2. /lobby/try_contribute:参与者会定期 ping 到此路由以报告活跃度,如果选择,排序器将向参与者发送当前的 SRS,以便他们计算其贡献。

  3. /contribute:它会在特定时间范围内收到SRS(以避免参与者花费太多时间并让其他人等待)并检查其有效性。如果为真,它将保存它并将其传递给下一个参与者。如果为 false,它将忽略新的 SRS,将参与者列入黑名单,并将之前的 SRS 发送给下一个参与者进行计算

  4. /info/status:它将提供有关仪式的信息,例如捐款数量、大厅大小以及用于签署每个参与者捐款后发送的收据的排序器公共地址。

定序器部署在一台强大的机器中,可以处理大量请求和反复发送 SRS 的带宽。为 /current_state 路由添加了 5 秒的缓存,因此显示仪式状态及其记录的浏览器不会导致带宽崩溃。对代理进行了一些更改,以避免大规模垃圾邮件/机器人攻击。

客户端实施

以太坊是由社区构建并为其社区构建的,对于我们来说,创建非专家也可以参与仪式的机制非常重要。这就是为什么官方客户端实现是基于浏览器的。

我们使用 React 作为前端框架,并使用 wasm-pack 将加密库 Rust 代码移植为 WASM,以便在浏览器上运行。Web 应用程序要求参与者的第一件事是通过在屏幕上移动鼠标并将一些“秘密”写入输入元素来生成熵。在幕后,我们将获取鼠标 x,y 位置和实例时间戳加上文本秘密,并将其输入作为随机秘密的种子生成,该随机秘密将进入 WASM 中的贡献函数。

之后,网站将要求参与者登录,并根据方法添加额外的 BLS 签名步骤(仅适用于 SIWE)。这种方法会用参与者的钱包对参与者的秘密进行签名,以便他们在以后证明自己的参与真实性。

然后参与者会进入大厅页面,该页面会显示当时大厅中有多少参与者以及被接受的机会(曾经有一段时间这些机会小于0.5%)。浏览器会时不时地对定序器执行 ping 操作。参与者可以移动到另一个选项卡以继续工作,并且 ping 将会继续,但如果他们的电池耗尽、关闭笔记本电脑、关闭浏览器或从会话中注销,那么 ping 将会停止,他们需要重新执行操作再次处理(包括新的熵生成)。

如果分配了一个槽,客户端将有大约 90 秒的时间来下载文件、执行计算并上传新文件。浏览器将通过包装函数将生成的熵加载到 WASM 代码中,并准备好将新的 SRS 发送到排序器。验证检查将在客户端执行,以防任何功能损坏。如果返回错误值,我们将通知参与者尽快发布 GitHub 问题(这种特殊情况从未发生过)。

我们面临的最大挑战是部署部分。我们不希望任何人信任我们的客户端实现,因此我们决定构建它并将其上传到 IPFS,IPFS 返回可用于访问 Web 应用程序本身的前端内容的哈希值(前端也由**外部公司**)。

在我们的代码中,我们有两个相反的组件:与 SIWE 模式中的自定义钱包相关的第三方弹出窗口和编译的 WASM 代码。浏览器不允许您同时运行这两个程序,因为它存在漏洞风险:第三方代码(您无法控制)可以运行已编译的 WASM(您无法读取)并执行恶意脚本。为了解决这个问题,我们需要在登录页面和贡献页面中设置不同的 HTTP 标头。

这样做的问题是 IPFS 不允许您轻松配置 HTTP 标头(您需要在 IPFS 节点设置上配置它们,而不是在应用程序中)。**杰夫**想出了这个涉及服务人员的有趣技巧:

Service Worker 作为客户端和服务器之间的中间件工作,它们专门设计用于运行离线渐进式 Web 应用程序和设备缓存策略。我们将使用它们来设置不同的 HTTP 标头,然后浏览器将识别它们并正常进行。但由于我们使用的是单页面应用程序,因此每次参与者进入登录或贡献页面时,我们都需要刷新页面。因此,将 Service Worker 和令人耳目一新的功能放在一起,我们能够将前端上传到 IPFS,这将允许用户使用所有 SIWE 模式钱包登录,并允许 WASM 代码计算。