目前我們如何建構鏈下基礎設施?
原文:Reth Execution Extensions(Paradigm)
編譯: OpenBuild
封面:Photo by Jigar Panchal on Unsplash
目錄
- 目前我們如何建構鏈下基礎設施?
- 介紹 Reth 執行擴充(ExEx)
- ExEx 是如何運作的?
- 你好,ExEx!
- 使用 ExEx 為 OP Stack 建立索引器
- 使用 ExEx 建構 Rollup
- 我可以使用 ExEx 建構什麼?
- ExEx 的下一步是什麼?
Reth 是一個用於建立高效能和可自訂節點的一體化工具包。最近,我們發布了將 Reth 效能提升超過一百倍的效能路線圖,以及將 Reth 模組化和可擴充性推向極致的測試網 Rollup Reth AlphaNet。
今天,我們非常高興地宣布推出 Reth Execution Extensions(ExEx)。 ExEx 是一個框架,用於建立高性能和複雜的鏈下基礎設施作為執行後掛鉤。 Reth ExEx 可用於實作 Rollup、索引器、MEV bots 等,且與現有方法相比程式碼量減少了 10 倍以上。在這個版本中,我們從頭開始示範了一個可在 20 程式碼行實現的重組追蹤器、250 程式碼行實現的索引器和 1000 程式碼行實現的 Rollup。
區塊鏈是一個時鐘,每隔一段時間就會確認包含交易資料的區塊。鏈下基礎設施訂閱這些定期的區塊更新,並更新自己的內部狀態作為回應。
例如,想像一個以太坊索引器的工作方式:
- 它通常透過 eth_subscribe 或 eth_getFilterChanges 輪詢訂閱區塊和日誌等以太坊事件。
- 對於每個事件,它都會透過 JSON-RPC 抓取所需的附加數據,例如區塊及其交易收據。
- 對於每個有效載荷,它會根據位址或關注的主題等配置對所需日誌進行 ABI 解碼。
- 對於所有解碼數據,它將其寫入 Postgres 或 Sqlite 等資料庫。
這是大型資料管道中常見的標準提取轉換載入(ETL)模式,Fivetran 等公司負責資料擷取,Snowflake 等公司負責將資料載入到資料倉儲,而客戶則專注於編寫轉換的業務邏輯。
我們觀察到,同樣的模式也適用於加密基礎架構的其他部分,如 Rollup、MEV 搜尋者,或更複雜的資料基礎架構,如影子日誌。
以此為動力,我們確定了為以太坊節點建立 ETL 管道時面臨的主要挑戰:
- 資料新鮮度:鏈重組意味著大多數基礎設施通常落後於鏈,以避免在可能不再屬於規範鏈的狀態下運作。這實際上意味著建立即時加密資料產品是一個挑戰,具有高延遲(多個區塊,數十秒)的產品層出不窮就是一個很好的證明。我們認為,之所以會出現這種情況,是因為節點在重組感知(reorg-aware)通知流方面沒有良好的開發體驗。
- 性能:在不同系统间移动数据、转换数据并将其拼接在一起意味着存在不可忽略的性能开销。例如,与其他插入 JSON-RPC 的索引器相比,直接插入 Reth 数据库的基于 Reth 的索引器显示出 1-2 个数量级的改进,这表明通过将工作负载集中在一起并移除中间通信层,性能得到了显著提高。
- 運行複雜性:以高正常運行時間運行以太坊節點已經是一個巨大的挑戰。在節點上運行額外的基礎設施會進一步加劇這個問題,這要求開發人員考慮工作協調 API,或為相對簡單的任務執行多個服務。
我們需要一個更好的應用程式介面來建立依賴節點狀態變化的鏈下基礎架構。這種應用程式介面必須具有良好的效能、「功能齊備」以及重組感知。我們需要一個 Airflow 時刻來建立以太坊 ETL 基礎設施和工作協調。
執行擴充(ExExes)是執行後掛鉤,用於在 Reth 基礎上建立即時、高效能和零操作的鏈下基礎架構。
執行擴展是從 Reth 的狀態派生其狀態的任務。這種狀態派生的例子包括 Rollup、索引器、MEV 提取器等。我們希望開發人員能建立可重複使用的 ExEx,這些 ExEx 以標準化的方式相互組合,類似於 Cosmos SDK 模組或 Substrate Pallets 的工作方式。
用 Rust 術語來說,ExEx 是與 Reth 一起無限期運行的 Future。 ExEx 使用解析為 ExEx 的非同步閉包進行初始化。以下是預期的端到端流程:
- Reth 公開了一個名為 ExExNotification 的重組感知流,其中包括提交到鏈的區塊列表,以及所有相關的事務和收據、狀態變更和 trie 更新。
- 開發人員應透過將 ExEx 寫成派生狀態(如 Rollup 區塊)的非同步函數來使用該流。該串流提供了一個 ChainCommitted 變體,用於向 ExEx 狀態添加內容,以及一個 ChainReverted/Reorged 變體,用於撤銷任何變更。這使得 ExEx 可以在本機區塊時間內運行,同時也提供了一個合理的 API 來安全地處理重組,而不是不處理重組並引入延遲。
- ExEx 由 Reth 的 ExExManager 協調,ExExManager 負責將來自 Reth 的通知路由到 ExEx,並將 ExEx 事件路由回 Reth,而 Reth 的任務執行器則負責驅動 ExEx 完成。
- 每個 ExEx 都透過 Node Builder 的 install_ExEx API 安裝在節點上。
從節點開發人員的角度來看,大致流程如下:
use futures::Future;
use reth_exex::{ExExContext, ExExEvent, ExExNotification};
use reth_node_api::FullNodeComponents;
use reth_node_ethereum::EthereumNode;
// The `ExExContext` is available to every ExEx to interface with the rest of the node.
//
// pub struct ExExContext<Node: FullNodeComponents> {
// /// The configured provider to interact with the blockchain.
// pub provider: Node::Provider,
// /// The task executor of the node.
// pub task_executor: TaskExecutor,
// /// The transaction pool of the node.
// pub pool: Node::Pool,
// /// Channel to receive [`ExExNotification`]s.
// pub notifications: Receiver<ExExNotification>,
// // .. other useful context fields
// }
async fn exex<Node: FullNodeComponents>(mut ctx: ExExContext<Node>) -> eyre::Result<()> {
while let Some(notification) = ctx.notifications.recv().await {
match ¬ification {
ExExNotification::ChainCommitted { new } => {
// do something
}
ExExNotification::ChainReorged { old, new } => {
// do something
}
ExExNotification::ChainReverted { old } => {
// do something
}
};
}
Ok(())
}
fn main() -> eyre::Result<()> {
reth::cli::Cli::parse_args().run(|builder, _| async move {
let handle = builder
.node(EthereumNode::default())
.install_exex("Minimal", |ctx| async move { exex(ctx) } )
.launch()
.await?;
handle.wait_for_node_exit().await
})
}
上述 50 程式碼行封裝了 ExEx 的定義和安裝。它非常強大,允許在無需額外基礎設施的情況下擴展以太坊節點的功能。
現在讓我們來看看一些用例。
執行擴展的 “Hello World”是一個重組追蹤器。下圖的 ExEx 說明了是否有新鍊或重組的日誌。只需解析 ExEx 發出的信息日誌,就可以在 Reth 節點上輕鬆建立重組追蹤器。
在這個範例中,新舊鏈都可以完全存取該區塊範圍內的每一次狀態變化,以及鏈結構中的 trie 更新和其他有用資訊。
async fn exex<Node: FullNodeComponents>(mut ctx: ExExContext<Node>) -> eyre::Result<()> {
while let Some(notification) = ctx.notifications.recv().await {
match ¬ification {
ExExNotification::ChainCommitted { new } => {
info!(committed_chain = ?new.range(), "Received commit");
}
ExExNotification::ChainReorged { old, new } => {
info!(from_chain = ?old.range(), to_chain = ?new.range(), "Received reorg");
}
ExExNotification::ChainReverted { old } => {
info!(reverted_chain = ?old.range(), "Received revert");
}
};
if let Some(committed_chain) = notification.committed_chain() {
ctx.events.send(ExExEvent::FinishedHeight(committed_chain.tip().number))?;
}
}
Ok(())
}
現在我們已經掌握了掛鉤節點事件的基本方法,以下讓我們建立一個更複雜的範例,例如使用 SQlite 作為後端,為普通 OP Stack 鏈中的存款和提款建立索引。
在這種情況下:
- 我們使用 Alloy 的 sol! 巨集載入了 OP Stack 的橋接合約,以產生類型安全的 ABI 解碼器(這是一個非常強大的宏,我們鼓勵開發人員深入研究)。
- 我們初始化 SQLite 連接並設定資料庫表。
- 在每個 ExExNotification 上,我們繼續讀取每個提交區塊的日誌,對其進行解碼,然後將其插入 SQLite。
- 如果 ExExNotification 是用於鏈重組,我們就會從 SQLite 表中刪除對應的條目。
就是這樣簡單,這可能是在 30 分鐘內就能創建的最高效能本地託管即時索引器。請看下面的程式碼,並閱讀完整範例:
https://github.com/paradigmxyz/reth/blob/main/examples/exex/op-bridge/src/main.rs。
use alloy_sol_types::{sol, SolEventInterface};
use futures::Future;
use reth_exex::{ExExContext, ExExEvent};
use reth_node_api::FullNodeComponents;
use reth_node_ethereum::EthereumNode;
use reth_primitives::{Log, SealedBlockWithSenders, TransactionSigned};
use reth_provider::Chain;
use reth_tracing::tracing::info;
use rusqlite::Connection;
sol!(L1StandardBridge, "l1_standard_bridge_abi.json");
use crate::L1StandardBridge::{ETHBridgeFinalized, ETHBridgeInitiated, L1StandardBridgeEvents};
fn create_tables(connection: &mut Connection) -> rusqlite::Result<()> {
connection.execute(
r#"
CREATE TABLE IF NOT EXISTS deposits (
id INTEGER PRIMARY KEY,
block_number INTEGER NOT NULL,
tx_hash TEXT NOT NULL UNIQUE,
contract_address TEXT NOT NULL,
"from" TEXT NOT NULL,
"to" TEXT NOT NULL,
amount TEXT NOT NULL
);
"#,
(),
)?;
// .. rest of db initialization
Ok(())
}
/// An example of ExEx that listens to ETH bridging events from OP Stack chains
/// and stores deposits and withdrawals in a SQLite database.
async fn op_bridge_exex<Node: FullNodeComponents>(
mut ctx: ExExContext<Node>,
connection: Connection,
) -> eyre::Result<()> {
// Process all new chain state notifications
while let Some(notification) = ctx.notifications.recv().await {
// Revert all deposits and withdrawals
if let Some(reverted_chain) = notification.reverted_chain() {
// ..
}
// Insert all new deposits and withdrawals
if let Some(committed_chain) = notification.committed_chain() {
// ..
}
}
Ok(())
}
/// Decode chain of blocks into a flattened list of receipt logs, and filter only
/// [L1StandardBridgeEvents].
fn decode_chain_into_events(
chain: &Chain,
) -> impl Iterator<Item = (&SealedBlockWithSenders, &TransactionSigned, &Log, L1StandardBridgeEvents)>
{
chain
// Get all blocks and receipts
.blocks_and_receipts()
// .. proceed with decoding them
}
fn main() -> eyre::Result<()> {
reth::cli::Cli::parse_args().run(|builder, _| async move {
let handle = builder
.node(EthereumNode::default())
.install_exex("OPBridge", |ctx| async move {
let connection = Connection::open("op_bridge.db")?;
create_tables(&mut connection)?;
Ok(op_bridge_exex(ctx, connection))
})
.launch()
.await?;
handle.wait_for_node_exit().await
})
}
現在讓我們做一些更有趣的事情,以 ExEx 的形式建立一個最小的 Rollup,使用 EVM 運行時和 SQLite 作為後端!
換個角度來看,即使是 Rollup 也是類似 ETL 的管道:
- 提取 L1 上發布的資料並轉換為 L2 有效載荷(如 OP Stack 推導函數)。
- 運行狀態轉換功能(例如 EVM)。
- 將更新後的狀態寫入永久記憶體。
在這個例子中,我們示範了一個簡化的 Rollup,它的狀態來自發佈到 Zenith 的 RLP 編碼 EVM 事務(用於發布 Rollup 的區塊承諾的 Holesky 智能合約),由 init4 團隊構建的簡單區塊生成器驅動。
具體示例如下:
- 配置 EVM 和實例化 SQLite 資料庫,並實作所需的 revm 資料庫特徵,將 SQLite 用作 EVM 後端。
- 過濾傳送到已部署的 Rollup 合約的事務,ABI 解碼 calldata,然後 RLP 將其解碼為 Rollup 區塊,由已配置的 EVM 執行。
- 將 EVM 的執行結果插入 SQLite。
再次強調,超簡單!它同樣適用於 blob!
ExEx Rollup 功能非常強大,因為我們現在可以透過安裝 ExEx,在 Reth 上運行任意數量的 Rollup,而無需額外的基礎設施。
我們正在用 blobs 擴充範例,並提供內建排序器,以提供更完整的端到端演示。如果您想建立這樣的功能,請聯絡我們,因為我們認為這有可能引入 L2 PBS、去中心化/共享排序器,甚至基於 SGX 的排序器等。
以下為範例片段。
use alloy_rlp::Decodable;
use alloy_sol_types::{sol, SolEventInterface, SolInterface};
use db::Database;
use eyre::OptionExt;
use once_cell::sync::Lazy;
use reth_exex::{ExExContext, ExExEvent};
use reth_interfaces::executor::BlockValidationError;
use reth_node_api::{ConfigureEvm, ConfigureEvmEnv, FullNodeComponents};
use reth_node_ethereum::{EthEvmConfig, EthereumNode};
use reth_primitives::{
address, constants,
revm::env::fill_tx_env,
revm_primitives::{CfgEnvWithHandlerCfg, EVMError, ExecutionResult, ResultAndState},
Address, Block, BlockWithSenders, Bytes, ChainSpec, ChainSpecBuilder, Genesis, Hardfork,
Header, Receipt, SealedBlockWithSenders, TransactionSigned, U256,
};
use reth_provider::Chain;
use reth_revm::{
db::{states::bundle_state::BundleRetention, BundleState},
DatabaseCommit, StateBuilder,
};
use reth_tracing::tracing::{debug, error, info};
use rusqlite::Connection;
use std::sync::Arc;
mod db;
sol!(RollupContract, "rollup_abi.json");
use RollupContrac:{RollupContractCalls, RollupContractEvents};
const DATABASE_PATH: &str = "rollup.db";
const ROLLUP_CONTRACT_ADDRESS: Address = address!("74ae65DF20cB0e3BF8c022051d0Cdd79cc60890C");
const ROLLUP_SUBMITTER_ADDRESS: Address = address!("B01042Db06b04d3677564222010DF5Bd09C5A947");
const CHAIN_ID: u64 = 17001;
static CHAIN_SPEC: Lazy<Arc<ChainSpec>> = Lazy::new(|| {
Arc::new(
ChainSpecBuilder::default()
.chain(CHAIN_ID.into())
.genesis(Genesis::clique_genesis(CHAIN_ID, ROLLUP_SUBMITTER_ADDRESS))
.shanghai_activated()
.build(),
)
});
struct Rollup<Node: FullNodeComponents> {
ctx: ExExContext<Node>,
db: Database,
}
impl<Node: FullNodeComponents> Rollup<Node> {
fn new(ctx: ExExContext<Node>, connection: Connection) -> eyre::Result<Self> {
let db = Database::new(connection)?;
Ok(Self { ctx, db })
}
async fn start(mut self) -> eyre::Result<()> {
// Process all new chain state notifications
while let Some(notification) = self.ctx.notifications.recv().await {
if let Some(reverted_chain) = notification.reverted_chain() {
self.revert(&reverted_chain)?;
}
if let Some(committed_chain) = notification.committed_chain() {
self.commit(&committed_chain)?;
self.ctx.events.send(ExExEvent::FinishedHeight(committed_chain.tip().number))?;
}
}
Ok(())
}
/// Process a new chain commit.
///
/// This function decodes all transactions to the rollup contract into events, executes the
/// corresponding actions and inserts the results into the database.
fn commit(&mut self, chain: &Chain) -> eyre::Result<()> {
let events = decode_chain_into_rollup_events(chain);
for (_, tx, event) in events {
match event {
// A new block is submitted to the rollup contract.
// The block is executed on top of existing rollup state and committed into the
// database.
RollupContractEvents::BlockSubmitted(_) => {
// ..
}
// A deposit of ETH to the rollup contract. The deposit is added to the recipient's
// balance and committed into the database.
RollupContractEvents::Enter(RollupContract::Enter {
token,
rollupRecipient,
amount,
}) => {
// ..
_ => (),
}
}
Ok(())
}
/// Process a chain revert.
///
/// This function decodes all transactions to the rollup contract into events, reverts the
/// corresponding actions and updates the database.
fn revert(&mut self, chain: &Chain) -> eyre::Result<()> {
let mut events = decode_chain_into_rollup_events(chain);
// Reverse the order of events to start reverting from the tip
events.reverse();
for (_, tx, event) in events {
match event {
// The block is reverted from the database.
RollupContractEvents::BlockSubmitted(_) => {
// ..
}
// The deposit is subtracted from the recipient's balance.
RollupContractEvents::Enter(RollupContract::Enter {
token,
rollupRecipient,
amount,
}) => {
// ..
}
_ => (),
}
}
Ok(())
}
}
fn main() -> eyre::Result<()> {
reth::cli::Cli::parse_args().run(|builder, _| async move {
let handle = builder
.node(EthereumNode::default())
.install_exex("Rollup", move |ctx| async {
let connection = Connection::open(DATABASE_PATH)?;
Ok(Rollup::new(ctx, connection)?.start())
})
.launch()
.await?;
handle.wait_for_node_exit().await
})
}
這個問題可以重構為 「什麼可以被建模為執行後掛鉤?」有很多東西可以建構!
我們看到了一些應該要建構的有價值的 ExEx:
- 派生管道(例如 Kona),其中 EVM 配置為 L2 使用,類似於 Reth Alphanet 的 EVM 的設定方式。我們也預測,組成 L2 ExEx 將為實現 Stage 2 Rollup 的去中心化提供最快的途徑。我們迫不及待地想在 Reth 上以 ExEx 的形式運行 OP Mainnet、Base、Zora 和其他 Rollup。
- 進程外 ExEx 使用 gRPC 與節點服務緊密整合,為多租戶和 Reth 作為鏈下服務的控制平面鋪平了道路。
- 替代 VM 整合(例如 MoveVM 或 Arbitrum Stylus),或類似 Artemis、SGX Revm 或影子日誌的複雜執行管道。
- 基礎設施與重質押相結合,如預言機和橋,或任何其他主動驗證服務(AVS)。之所以能做到這一點,是因為 ExEx 可以透過以太坊的 DiscV5 P2P 網路相互對等,並且擁有一組選舉出來的參與者,除了節點的「最終」通知之外,還擁有對其狀態的寫入權限。
- 新一代輔助基礎設施,如 AI 協處理器或去中心化/共享排序器。
目前,ExEx 需要透過主函數中的自訂建置安裝到節點上。我們希望將 ExEx 作為插件動態加載,並公開類似於 Docker Hub 的 reth pull API,這樣開發人員就可以輕鬆地將他們的 ExEx 發布給節點運營商。
我們希望將 Reth 打造成一個能為核心節點操作提供穩定性與效能的平台,同時也能成為創新的啟動平台。
Reth 專案有望改變人們對建立高效能鏈下基礎設施的看法,而 ExEx 只是一個開始。我們很高興能繼續在 Reth 上建造基礎設施,並對其進行投資。
如果您想建立 ExEx 項目,請聯絡:
georgios@paradigm.xyz,
或直接在 Github 上提交:
https://github.com/paradigmxyz/reth/labels/A-exex
免責聲明:作為區塊鏈資訊平台,本站所發布文章僅代表作者及來賓個人觀點,與 Web3Caff 立場無關。文章內的資訊僅供參考,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。