金色早报 | SBF曾会见前美
构建你自己的Rollup——BYOR 项目一览
编译:登链翻译计划
你是否曾经想要深入了解 Rollup 的运作原理?理论很好,但亲身实践经验总是更可取的。不幸的是,现有的项目并不总是让人轻易地查看内部情况。这就是为什么我们创建了 BYOR(Build Your Own Rollup:构建你自己的Rollup)。它是一个具有最小功能的主权 rollup,重点是使代码易于阅读和理解。
我们这个项目的动机是让人们(无论是外部人员还是内部人员)更好地理解我们周围的 rollup 实际上在做什么。你可以在 Holesky 的已部署的BYOR上玩耍,或者阅读GitHub 上的源代码。
BYOR是什么?
BYOR 项目是一个简化版本的主权 rollup。与乐观和零知识证明的 rollup 相比,主权 rollup 不会在以太坊上验证状态根,只依赖于以太坊上的数据可用性和共识。这样可以防止 L1 和 BYOR 之间的信任最小化桥,但极大地简化了代码,非常适合教育目的。
代码库由三个程序组成:智能合约、节点和钱包。当它们一起部署时,它们允许最终用户与网络进行交互。有趣的是,网络的状态完全由链上数据确定,这意味着实际上可以运行多个节点。每个节点也可以作为排序器(Sequencer)独立地发布数据。
下面是 BYOR 中实现的完整功能列表:
费用排序
将状态发布到 L1 并从 L1 获取状态
丢弃无效的交易
查看账户余额
发送交易
查看交易状态
使用钱包
在钱包应用中,它充当网络的前端,用户可以提交交易,并检查账户的状态或交易的状态。在登陆页面上,你会看到一个概览,其中提供了有关 rollup 当前状态的一些统计信息,然后是你的账户状态。很可能,这里仅有一个按钮用来连接你选择的钱包,并有关于代币水龙头的消息。在下面,有一个搜索栏,你可以粘贴某人的地址或交易哈希来探索 L2 的当前状态。最后,有两个交易列表:第一个是 L2 内存池中的交易列表,第二个是发布到 L1 的交易列表。
要开始,请使用 WalletConnect 按钮连接你的钱包。连接后,你可能会收到一个通知,提示你的钱包连接到了错误的网络。如果你的应用程序支持网络切换,请点击“切换网络”按钮切换到 Holesky 测试网络。否则,请手动切换。
现在,你可以通过提供接收者的地址、要发送的代币数量和所需手续费来向某人发送代币。发送后,钱包应用程序会提示你签署消息。如果成功签署,消息将被发送到 L2 节点的内存池中,等待被发布到 L1。交易被捆绑到批次发布中所需的时间可能会有所不同。每 10 秒,L2 节点会检查是否有待发布的内容。手续费较高的交易会优先发送,因此如果你指定了较低的手续费并且有大量交易流量,你可能会遇到较长的等待时间。
工作原理
技术栈
我们使用以下技术构建了每个组件:
节点: Node.js, TypeScript, tRPC, Postgres, viem, drizzle-orm
钱包: TypeScript, tRPC, Next.js, WalletConnect
代码深入解析
BYOR 代码专门设计成通过查看代码库就能轻松理解。请随意探索我们的代码库!首先阅读README.md,了解项目结构请阅读ARCHITECTURE.md文件。
以下是代码中的一些有趣亮点:
智能合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Inputs {
event BatchAppended(address sender);
function appendBatch(bytes calldata) external {
require(msg.sender == tx.origin);
emit BatchAppended(msg.sender);
}
}
这是唯一需要的智能合约。它的名称源于这个事实:将输入存储到状态转换函数中。该合约的唯一目的是为了方便地存储所有交易。序列化的批次作为 calldata 发布到这个智能合约,并且它会发出一个带有批次发布者地址的 BatchAppended 事件。虽然我们可以设计系统,使其将交易直接发布到 EOA 而不是合约,但通过发出事件可以轻松通过 JSON-RPC 获取数据。这个智能合约的唯一要求是它不应该从另一个智能合约中调用,而应该直接从 EOA 中调用。
数据库模式
CREATE TABLE `accounts` (
`address` text PRIMARY KEY NOT NULL,
`balance` integer DEFAULT 0 NOT NULL,
`nonce` integer DEFAULT 0 NOT NULL
);
CREATE TABLE `transactions` (
`id` integer,
`from` text NOT NULL,
`to` text NOT NULL,
`value` integer NOT NULL,
`nonce` integer NOT NULL,
`fee` integer NOT NULL,
`feeReceipent` text NOT NULL,
`l1SubmittedDate` integer NOT NULL,
`hash` text NOT NULL
PRIMARY KEY(`from`, `nonce`)
);
-- This table has a single row
CREATE TABLE `fetcherStates` (
`chainId` integer PRIMARY KEY NOT NULL,
`lastFetchedBlock` integer DEFAULT 0 NOT NULL
);