当前我们如何构建链下基础设施?

原文:Reth Execution Extensions(Paradigm)

作者:Georgios Konstantopoulos  

编译:OpenBuild

封面:Photo by Jigar Panchal on Unsplash

目录

  • 当前我们如何构建链下基础设施?
  • 介绍 Reth 执行扩展(ExEx)
    • ExEx 是如何工作的?
    • Hello 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。

区块链是一个时钟,每隔一段时间就会确认包含交易数据的区块。链下基础设施订阅这些定期的区块更新,并更新自己的内部状态作为响应。

例如,想象一个以太坊索引器的工作方式:

  1. 它通常通过 eth_subscribe 或 eth_getFilterChanges 轮询订阅区块和日志等以太坊事件。
  2. 对于每个事件,它都会通过 JSON-RPC 抓取所需的附加数据,例如区块及其交易收据。
  3. 对于每个有效载荷,它会根据地址或关注的主题等配置对所需日志进行 ABI 解码。
  4. 对于所有解码数据,它将其写入 Postgres 或 Sqlite 等数据库。

这是大型数据管道中常见的标准提取转换加载(ETL)模式,Fivetran 等公司负责数据提取,Snowflake 等公司负责将数据加载到数据仓库,而客户则专注于编写转换的业务逻辑。

我们观察到,同样的模式也适用于加密基础设施的其他部分,如 Rollup、MEV 搜索者,或更复杂的数据基础架构,如影子日志。

以此为动力,我们确定了为以太坊节点构建 ETL 管道时面临的主要挑战

  1. 数据新鲜度:链重组意味着大多数基础设施通常落后于链,以避免在可能不再属于规范链的状态下运行。这实际上意味着构建实时加密数据产品是一个挑战,具有高延迟(多个区块,数十秒)的产品层出不穷就是一个很好的证明。我们认为,之所以会出现这种情况,是因为节点在重组感知(reorg-aware)通知流方面没有良好的开发体验。
  2. 性能:在不同系统间移动数据、转换数据并将其拼接在一起意味着存在不可忽略的性能开销。例如,与其他插入 JSON-RPC 的索引器相比,直接插入 Reth 数据库的基于 Reth 的索引器显示出 1-2 个数量级的改进,这表明通过将工作负载集中在一起并移除中间通信层,性能得到了显著提高。
  3. 运行复杂性:以高正常运行时间运行以太坊节点已经是一个巨大的挑战。在节点上运行额外的基础设施会进一步加剧这一问题,这要求开发人员考虑工作协调 API,或为相对简单的任务运行多个服务。

我们需要一个更好的应用程序接口来构建依赖于节点状态变化的链下基础设施。这种应用程序接口必须具有良好的性能、「功能齐备」以及重组感知。我们需要一个 Airflow 时刻来构建以太坊 ETL 基础设施和工作协调。

执行扩展(ExExes)是执行后挂钩,用于在 Reth 基础上构建实时、高性能和零操作的链下基础设施

执行扩展是从 Reth 的状态派生其状态的任务。这种状态派生的例子包括 Rollup、索引器、MEV 提取器等。我们希望开发人员能构建可重复使用的 ExEx,这些 ExEx 以标准化的方式相互组合,类似于 Cosmos SDK 模块或 Substrate Pallets 的工作方式。

用 Rust 术语来说,ExEx 是与 Reth 一起无限期运行的 Future。ExEx 使用解析为 ExEx 的异步闭包进行初始化。以下是预期的端到端流程

  1. Reth 公开了一个名为 ExExNotification 的重组感知流,其中包括提交到链的区块列表,以及所有相关的事务和收据、状态更改和 trie 更新。
  2. 开发人员应通过将 ExEx 写成派生状态(如 Rollup 块)的异步函数来使用该流。该流提供了一个 ChainCommitted 变体,用于向 ExEx 状态添加内容,以及一个 ChainReverted/Reorged 变体,用于撤销任何更改。这使得 ExEx 可以在本地块时间内运行,同时还提供了一个合理的 API 来安全地处理重组,而不是不处理重组并引入延迟。
  3. ExEx 由 Reth 的 ExExManager 协调,ExExManager 负责将来自 Reth 的通知路由到 ExEx,并将 ExEx 事件路由回 Reth,而 Reth 的任务执行器则负责驱动 ExEx 完成。
  4. 每个 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 &notification {            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 &notification {            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 链中的存款和取款建立索引。

在这种情况下:

  1. 我们使用 Alloy 的 sol! 宏加载了 OP Stack 的桥接合约,以生成类型安全的 ABI 解码器(这是一个非常强大的宏,我们鼓励开发人员深入研究)。
  2. 我们初始化 SQLite 连接并设置数据库表。
  3. 在每个 ExExNotification 上,我们继续读取每个提交区块的日志,对其进行解码,然后将其插入 SQLite。
  4. 如果 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 的管道:

  1. 提取 L1 上发布的数据并转换为 L2 有效载荷(如 OP Stack 推导函数)。
  2. 运行状态转换功能(例如 EVM)。
  3. 将更新后的状态写入永久存储器。

在这个例子中,我们演示了一个简化的 Rollup,它的状态来自发布到 Zenith 的 RLP 编码 EVM 事务(用于发布 Rollup 的区块承诺的 Holesky 智能合约),由 init4 团队构建的简单区块生成器驱动。

具体示例如下:

  1. 配置 EVM 和实例化 SQLite 数据库,并实现所需的 revm 数据库特征,将 SQLite 用作 EVM 后端。
  2. 过滤发送到已部署的 Rollup 合约的事务,ABI 解码 calldata,然后 RLP 将其解码为 Rollup 块,由已配置的 EVM 执行。
  3. 将 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:

  1. 派生管道(例如 Kona),其中 EVM 配置为 L2 使用,类似于 Reth Alphanet 的 EVM 的设置方式。我们还预测,组成 L2 ExEx 将为实现 Stage 2 Rollup 的去中心化提供最快的途径。我们迫不及待地想在 Reth 上以 ExEx 的形式运行 OP Mainnet、Base、Zora 和其他 Rollup。
  2. 进程外 ExEx 使用 gRPC 与节点服务紧密集成,为多租户和 Reth 作为链下服务的控制平面铺平了道路
  3. 替代 VM 集成(例如 MoveVM 或 Arbitrum Stylus),或类似于 Artemis、SGX Revm 或影子日志的复杂执行管道。
  4. 基础设施与重质押相结合,如预言机和桥,或任何其他主动验证服务(AVS)。之所以能做到这一点,是因为 ExEx 可以通过以太坊的 DiscV5 P2P 网络相互对等,并且拥有一组选举出来的参与者,除了节点的「最终」通知之外,还拥有对其状态的写入权限。
  5. 下一代辅助基础设施,如 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 立场无关。文章内的信息仅供参考,均不构成任何投资建议及要约,并请您遵守所在国家或地区的相关法律法规。