本文介绍了 Reth Execution Extensions (ExExs),它是一种运行在 Reth 执行客户端内部的插件,允许你实时处理区块链数据。文章详细阐述了 ExExs 的架构、生命周期,并提供了一个创建、构建和部署交易计数器 ExEx 的详细教程,展示了如何使用 systemd 服务在生产环境中部署 ExExs。
Reth Execution Extensions (ExExs) 是在 Reth 执行客户端内部运行的插件,允许你在区块提交时实时处理区块链数据。与轮询 RPC 端点的外部应用程序不同,ExExs 接收来自以太坊执行引擎的直接通知,这使得它们对于索引和自定义处理逻辑非常有效。
本指南将引导你创建、构建和部署一个交易计数器 ExEx,该 ExEx 演示了构建自定义扩展所需的核心概念和模式。
重要提示:ExExs 不是单独的进程。它们编译到 Reth 二进制文件中并在其中运行,直接与执行引擎共享内存。这意味着你不能针对现有的 Reth 节点运行 ExEx,你必须重新构建包含你的 ExEx 的 Reth。
Reth Execution Extensions 是 Rust 模块,它们直接连接到 Reth 执行客户端的区块处理管道。当新的区块被提交、回滚或重新组织时,ExExs 会立即收到通知,并完全访问区块数据、交易、收据和状态更改。
方面 | ExEx | 外部 RPC 应用 |
---|---|---|
延迟 | ~1ms (直接内存) | 50-500ms (网络 + 处理) |
数据访问 | 完整的区块数据、收据、状态 | 受公开的 RPC 方法限制 |
资源使用 | 共享进程内存 | 单独的进程 + 网络 |
部署 | 重新构建 Reth 二进制文件 | 独立部署 |
可扩展性 | 单进程扩展 | 水平扩展 |
ExExs 不是通过网络连接到 Reth 的单独程序。相反,ExExs 直接编译到同一个 Reth 二进制文件中,并与节点一起运行,共享内存和处理能力。这种集成是 ExExs 能够以如此低的延迟处理区块链数据的原因。
当你运行你的 ExEx 时,你实际上是在运行一个修改后的 Reth 版本,其中包括你的自定义逻辑。该节点正常处理区块,但现在它还会向在同一进程中运行的 ExEx 代码发送通知。以下是通知类型:
ExExs 接收三种类型的通知:
我们现在将介绍生命周期,以及我们将如何实现通知类型(特别是 ChainCommited
)来计算区块中的交易。
// 1. 初始化
async fn exex_init<Node: FullNodeComponents>(
ctx: ExExContext<Node>,
) -> eyre::Result<impl Future<Output = eyre::Result<()>>> {
// 设置数据库、连接等。
Ok(exex(ctx))
}
// 2. 主处理循环
async fn exex<Node: FullNodeComponents>(mut ctx: ExExContext<Node>) -> eyre::Result<()> {
while let Some(notification) = ctx.notifications.try_next().await? {
// 处理通知
handle_notification(¬ification).await?;
// 发出完成信号以进行修剪
if let Some(committed_chain) = notification.committed_chain() {
ctx.events.send(ExExEvent::FinishedHeight(
committed_chain.tip().num_hash()
))?;
}
}
Ok(())
}
稍后你将看到,我们构建的交易计数器示例将仅使用 exex
函数,因为我们不需要随着链的进行而保持状态。但是,对于未来的 ExExs,当你需要执行以下操作时,可以使用 exex_init
函数:
现在我们对 Reth 的 ExExs 有了更好的了解,让我们开始本指南的技术编码部分。
在本指南中,我们将演示构建一个交易计数器 ExEx,该 ExEx 跟踪和计算每个区块的交易数量。这将是一个简单的例子,但它将成为构建未来 ExExs 的基础。
重要提示:在继续之前,请确保你有一个正在运行并已同步的 Reth 全节点。在本指南中,我们将使用已同步的 Hoodi 节点。
首先,验证你的环境:
## 检查 Rust 版本(需要 1.86+)
rustc --version
## 检查你是否有一个正在运行的 Reth 节点
systemctl status reth.service
## 验证 Git 安装
git --version
你还需要确保你的 Reth 节点已同步并位于链的顶端(例如,检查 eth_syncing
,或者从你的本地节点获取最新的区块并与区块浏览器进行比较)
mkdir transaction-counter-exex
cd transaction-counter-exex
Cargo.toml
文件来初始化 Cargo 项目:touch Cargo.toml
然后将以下代码包含在该文件中:
[package]
name = "transaction-counter-exex"
version = "0.1.0"
edition = "2021"
rust-version = "1.86"
license = "MIT OR Apache-2.0"
publish = false
[dependencies]
## Core Reth dependencies
reth = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.6.0" }
reth-exex = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.6.0" }
reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.6.0" }
reth-node-api = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.6.0" }
reth-tracing = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.6.0" }
reth-primitives = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.6.0" }
## Ethereum primitives
alloy-primitives = { version = "1.0", default-features = false }
alloy-consensus = { version = "1.0", default-features = false }
## Async and utilities
futures = "0.3"
eyre = "0.6"
[dev-dependencies]
tokio = { version = "1.0", features = ["full"] }
核心依赖项包括用于 ExEx 框架的 reth-exex
,用于以太坊特定功能的 reth-node-ethereum
和用于日志记录的 reth-tracing
。 Alloy 库提供标准的以太坊类型,例如地址和区块结构,而 futures
处理接收区块链通知所需的异步流处理,而 eyre
在出现问题时提供更好的错误消息。 dev-dependencies 部分中的 tokio
依赖项仅用于测试,并提供异步运行时。
src
) 目录和 main.rs
文件:mkdir src
touch src/main.rs
现在让我们实现一个交易计数器,该计数器按类型对交易进行分类。
在 src/main.rs
中,添加以下代码:
use futures::{Future, TryStreamExt};
use reth_exex::{ExExContext, ExExEvent, ExExNotification};
use reth_node_api::{BlockBody, FullNodeComponents};
use reth_node_ethereum::EthereumNode;
use alloy_consensus::BlockHeader;
use reth_tracing::tracing::info;
##[derive(Debug, Default, Clone)]
struct TransactionCounter {
/// Total number of transactions processed
total_transactions: u64,
/// Number of blocks processed
total_blocks: u64,
/// FUTURE USAGE EXAMPLES BELOW
/// Number of ETH transfers (transactions with no input data)
/// eth_transfers: u64,
/// Number of contract calls (transactions with input data to existing contracts)
/// contract_calls: u64,
}
/// The initialization logic of the ExEx is just an async function.
async fn exex_init<Node: FullNodeComponents>(
ctx: ExExContext<Node>,
) -> eyre::Result<impl Future<Output = eyre::Result<()>>> {
Ok(exex(ctx))
}
/// An ExEx that counts transactions and blocks.
async fn exex<Node: FullNodeComponents>(mut ctx: ExExContext<Node>) -> eyre::Result<()> {
let mut counter = TransactionCounter::default();
while let Some(notification) = ctx.notifications.try_next().await? {
match ¬ification {
ExExNotification::ChainCommitted { new } => {
// Count blocks and transactions in the committed chain
for block in new.blocks_iter() {
counter.total_blocks += 1;
// Count transactions in this block
let tx_count = block.body().transactions().len() as u64;
counter.total_transactions += tx_count;
info!(
block_number = block.number(),
block_hash = %block.hash(),
tx_count,
total_transactions = counter.total_transactions,
total_blocks = counter.total_blocks,
"Processed block" // 处理的区块
);
}
info!(
committed_chain = ?new.range(),
total_transactions = counter.total_transactions,
total_blocks = counter.total_blocks,
"Processed committed chain" // 处理的已提交链
);
}
ExExNotification::ChainReorged { old, new } => {
info!(
from_chain = ?old.range(),
to_chain = ?new.range(),
"Received reorg" // 收到重组
);
// For simplicity, we just log reorgs
// In production, you might want to adjust counters
}
ExExNotification::ChainReverted { old } => {
info!(reverted_chain = ?old.range(), "Received revert" ); // 收到还原
// For simplicity, we just log reverts
}
}
if let Some(committed_chain) = notification.committed_chain() {
ctx.events
.send(ExExEvent::FinishedHeight(committed_chain.tip().num_hash()))?;
}
}
Ok(())
}
fn main() -> eyre::Result<()> {
reth::cli::Cli::parse_args().run(|builder, _| async move {
let handle = builder
.node(EthereumNode::default())
.install_exex("TransactionCounter", exex_init)
.launch()
.await?;
handle.wait_for_node_exit().await
})
}
保存该文件。让我们回顾一下上面的代码。
此代码创建一个简单的 Reth ExEx,用于实时计数交易和区块。 TransactionCounter
结构包含两个计数器:total_transactions
和 total_blocks
。
主 exex
函数运行一个无限循环,侦听区块链通知。当它收到 ChainCommitted
事件时,它会遍历每个新区块,使用 block.body().transactions().len()
计算该区块中的交易,添加到运行总计中,并记录结果。它还通过记录来处理重组和还原事件。
该代码遵循 Reth 的标准 ExEx 模式, exex_init
返回主处理,而 main
使用节点构建器将 ExEx 安装为 TransactionCounter
并启动 Reth 节点。在处理完每个链后,它会将 FinishedHeight
事件发送回 Reth 以发出完成信号。
cargo check
如果你遇到编译错误,请确保所有导入均正确,并且你的 Rust 版本为 1.86+。
cargo build --release
注意:初始构建可能需要 10-15 分钟,因为它会将整个 Reth 代码库与你的 ExEx 一起编译。
对于生产部署,你将希望将你的 ExEx 作为 systemd 服务运行。这可确保自动启动、故障时重新启动以及正确的日志记录。
如果你想将 ExEx 作为单独的服务运行(建议用于测试):
## 创建服务文件
sudo nano /etc/systemd/system/reth-exex.service
添加以下配置:
> 注意:将 /path/to/transaction-counter-exex
替换为你的实际项目目录路径,将 YOUR_USERNAME
替换为你的系统用户名,并将 /path/to/jwtsecret
替换为你的 JWT 密钥文件位置。
[Unit]
Description=Reth Ethereum Client with Transaction Counter ExEx
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=YOUR_USERNAME
Group=YOUR_USERNAME
ExecStart=/path/to/transaction-counter-exex/target/release/transaction-counter-exex node --chain hoodi --http --http.addr 0.0.0.0 --http.port 8545 --authrpc.addr 127.0.0.1 --authrpc.port 8551 --authrpc.jwtsecret /path/to/jwtsecret --full
WorkingDirectory=/path/to/transaction-counter-exex
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=reth-exex
KillMode=mixed
KillSignal=SIGINT
TimeoutStopSec=30
## Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=false
ReadWritePaths=/path/to/transaction-counter-exex
ReadWritePaths=/path/to/.local/share/reth
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
[Install]
WantedBy=multi-user.target
部署该服务:
## 重新加载 systemd
sudo systemctl daemon-reload
## 停止现有的 Reth 服务(如果正在运行)
sudo systemctl stop reth.service
## 启动 ExEx 服务
sudo systemctl start reth-exex.service
sudo systemctl enable reth-exex.service
## 监控日志
sudo journalctl -u reth-exex.service -f
让我们验证我们的交易计数器是否能够准确地中继信息。为此,请打开一个新的终端窗口并运行以下命令:
journalctl -u reth-exex.service --since="1 minutes ago" | grep -E "(TransactionCounter|Processed block)"
此命令将返回过去一分钟内处理的块。请注意,如果你使用的是 Hoodi 以外的网络,则可能需要更新此设置。
你应该会看到如下所示的响应,表明我们的 TransactionCounter
正在运行。
2025-09-18T15:20:01.947825Z INFO exex{id="TransactionCounter"}: Processed block block_number=1238975 block_hash=0x99acbc7b5892f32afbdce65871a7ee8691789179eaf20eed8643776b91b40eb7 tx_count=39 total_transactions=10135 total_blocks=259
Sep 18 15:20:01 ip-172-31-6-79.us-east-2.compute.internal reth-exex[2796328]: 2025-09-18T15:20:01.947862Z INFO exex{id="TransactionCounter"}: Processed committed chain committed_chain=1238975..=1238975 total_transactions=10135 total_blocks=259
接下来,运行下面的 cURL 命令从你的 Reth 节点获取最新的区块,我们将使用它进行测试。
curl -s -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \
http://localhost:8545
如果你在与
8545
不同的端口上运行,请根据需要进行更新。
你将收到类似于 {"jsonrpc":"2.0","id":1,"result":"0x12e7bf"}
的响应。我们需要将十六进制输出转换为十进制。我们可以使用以下命令来完成:
echo $((0xYourHex))
通过该响应,让我们再次调用 cURL 以解析出我们的测试区块中的交易数量(例如,)。
curl -s -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0xYourHex",false],"id":1}' \
http://localhost:8545 | jq '.result.transactions | length'
响应显示为 39
,我们可以通过查看该特定区块号的 explorer link 来确认我们的 ExEx 正在准确地记录信息。
如果你不想处理管理自己的节点或构建端到端管道的麻烦。请查看下面的一些强大的解决方案和 API:
Streams 将区块链数据直接推送到你的目标位置,无需复杂的 ETL 管道。它提供实时和历史数据,并具有内置的重组处理功能。
主要特点:
使用 QuickNode Webhooks 为链上事件设置一键式提醒,直接传送到你的服务器。
主要特点:
无需构建和管理自定义基础设施,你可以探索 QuickNode Marketplace 以利用已经构建并可以使用的大多数自定义 API,例如:
恭喜!你已成功学习如何构建、部署和管理 Reth Execution Extensions。 ExExs 提供了一种强大的方式来扩展 Reth 的功能,同时保持高性能和可靠性。
无论你是在构建分析平台、MEV 策略、索引服务还是自定义业务逻辑,ExExs 都为高性能区块链应用程序奠定了基础。
订阅我们的 newsletter 以获取更多关于以太坊基础设施和开发的文章和指南。如果你有任何反馈,请随时通过 X 与我们联系。你始终可以在我们的 Discord 社区服务器上与我们聊天,其中包含你将遇到的最酷的一些开发人员!
Let us know 如果你有任何反馈或对新主题的请求。我们很乐意听取你的意见。
- 原文链接: quicknode.com/guides/inf...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!