構建你自己的Rollup——BYOR 項目一覽

2023-10-18 09:10 登鏈社區


編譯:登鏈翻譯計劃

你是否曾經想要深入了解 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 節點會檢查是否有待發布的內容。手續費較高的交易會優先發送,因此如果你指定了較低的手續費並且有大量交易流量,你可能會遇到較長的等待時間。

工作原理

Rollup 架構圖

技術棧

我們使用以下技術構建了每個組件:

  • 節點: 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
);

這是用於存儲關於 Rollup 的信息的整個數據庫模式。你可能會想當所有必要的數據都存儲在 L1 上,爲什么我們需要一個數據庫。雖然這是正確的,但是將數據存儲在本地可以通過避免重復獲取來節省時間和資源。將在此模式中存儲的所有數據視爲狀態、交易哈希和其他計算信息的備忘錄。

fetcherStates 表用於跟蹤我們在搜索 BatchAppended 事件時獲取的最後一個塊。當節點關閉並重新啓動時,這非常有用;它知道從哪裏恢復搜索。

狀態轉換函數

const DEFAULT_ACCOUNT = { balance: 0, nonce: 0 }

function executeTransaction(state, tx, feeRecipient) {
  const fromAccount = getAccount(state, tx.from, DEFAULT_ACCOUNT)
  const toAccount = getAccount(state, tx.to, DEFAULT_ACCOUNT)
  // Step 1. Update nonce
  fromAccount.nonce = tx.nonce
  // Step 2. Transfer value
  fromAccount.balance -= tx.value
  toAccount.balance += tx.value
  // Step 3. Pay fee
  fromAccount.balance -= tx.fee
  feeRecipientAccount.balance += tx.fee
}

上面顯示的函數是 BYOR 中狀態轉換機制的核心。它假設交易可以安全地執行,具有正確的 nonce 和足夠的余額來進行定義的支出。由於這個假設,在這個函數內部沒有錯誤處理或驗證步驟。相反,這些步驟在調用函數之前執行。每個账戶狀態都存儲在一個映射中。如果一個账戶在這個映射中還不存在,它將被設置爲代碼清單頂部可見的默認值。使用的三個账戶,nonce 被更新,余額被分配。

交易籤名

交易籤名

我們使用EIP-712標准來對類型化數據進行籤名。這使我們能夠清楚地向用戶顯示他們正在籤名的內容。如上所示,當發送一筆交易時,我們可以以用戶友好的方式顯示接收者、金額和手續費。

L1 事件獲取

function getNewStates() {
 const lastBatchBlock = getLastBatchBlock()
 const events = getLogs(lastBatchBlock)
 const calldata = getCalldata(events)
 const timestamps = getTimestamps(events)
 const posters = getTransactionPosters(events)
 updateLastFetchedBlock(lastBatchBlock)
 return zip(posters, timestamps, calldata)
}

爲了獲取新的事件,我們從 Inputs 合約中檢索從上次獲取的區塊开始的所有 BatchAppended 事件。我們檢索的事件數量最多爲最新的區塊或上次獲取的區塊加上批量大小限制。在檢索所有事件之後,我們從每個交易中提取 calldata、時間戳和發布者地址。將我們獲取的最後一個區塊更新爲我們正在獲取的最後一個區塊。然後,將提取的 calldata、時間戳和發布者打包在一起,並從函數中返回以進行進一步處理。

內存池及其費用排序

function popNHighestFee(txPool, n) {
 txPool.sort((a, b) => b.fee - a.fee))
 return txPool.splice(0, n)
}

內存池是一個管理已籤名交易數組的對象。最有趣的方面是它如何確定交易被發布到 L1 的順序。如上所示的代碼,交易是根據它們的費用進行排序的。這讓系統中位數費用價格會根據鏈上活動而波動。

即使你指定了高費用,如果它們需要被附加到當前狀態,交易仍然需要產生一個有效的狀態。因此,你不能僅僅因爲費用高就提交無效的交易。

BYOR 是否真正擴展了以太坊?

樂觀和 ZK rollup 已經建立了系統來證明發布的狀態根與狀態轉換函數和它們提交的數據是一致的,但主權 rollup 沒有。因此,這種類型的 rollup 無法擴展以太坊,這一點可能一开始看起來有些違反直覺。然而,當我們意識到其他類型的 rollup 可以僅使用 L1 來證明發布的狀態根是正確的時,這就變得合理了。要區分主權 rollup 的數據是否正確,我們需要運行一個 L1 節點以及額外的軟件,以形式化 L2 節點來執行狀態轉換函數,從而增加了計算負載。

未來展望

對我們來說,構建這個項目是一次很好的學習經驗,我們希望你也會發現我們的努力有價值。我們希望將來能夠回到 BYOR,爲其添加一個欺詐證明系統。這將使它成爲一個真正的樂觀 rollup,並再次成爲我們日常使用的系統內部工作方式的教訓。

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。

標題:構建你自己的Rollup——BYOR 項目一覽

地址:https://www.sgitmedia.com/article/13293.html

相關閱讀: