Hello World
Let's write a simple "Hello World" ExEx that emits a log every time a new chain of blocks is committed, reverted, or reorged.
Create a project
First, let's create a new project for our ExEx
cargo new --bin my-exex
cd my-exex
And add Reth as a dependency in Cargo.toml
[package]
name = "my-exex"
version = "0.1.0"
edition = "2021"
[dependencies]
reth = { git = "https://github.com/paradigmxyz/reth.git" } # Reth
reth-exex = { git = "https://github.com/paradigmxyz/reth.git" } # Execution Extensions
reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth.git" } # Ethereum Node implementation
reth-tracing = { git = "https://github.com/paradigmxyz/reth.git" } # Logging
eyre = "0.6" # Easy error handling
futures-util = "0.3" # Stream utilities for consuming notifications
Default Reth node
Now, let's jump to our main.rs
and start by initializing and launching a default Reth node
use reth_node_ethereum::EthereumNode;
fn main() -> eyre::Result<()> {
reth::cli::Cli::parse_args().run(|builder, _| async move {
let handle = builder.node(EthereumNode::default()).launch().await?;
handle.wait_for_node_exit().await
})
}
You can already test that it works by running the binary and initializing the Holesky node in a custom datadir (to not interfere with any instances of Reth you already have on your machine):
$ cargo run -- init --chain holesky --datadir data
2024-06-12T16:48:06.420296Z INFO reth init starting
2024-06-12T16:48:06.422380Z INFO Opening storage db_path="data/db" sf_path="data/static_files"
2024-06-12T16:48:06.432939Z INFO Verifying storage consistency.
2024-06-12T16:48:06.577673Z INFO Genesis block written hash=0xb5f7f912443c940f21fd611f12828d75b53
4364ed9e95ca4e307729a4661bde4
Simplest ExEx
The simplest ExEx is just an async function that never returns. We need to install it into our node
use reth::api::FullNodeComponents;
use reth_exex::ExExContext;
use reth_node_ethereum::EthereumNode;
async fn my_exex<Node: FullNodeComponents>(mut _ctx: ExExContext<Node>) -> eyre::Result<()> {
#[allow(clippy::empty_loop)]
loop {}
}
fn main() -> eyre::Result<()> {
reth::cli::Cli::parse_args().run(|builder, _| async move {
let handle = builder
.node(EthereumNode::default())
.install_exex("my-exex", |ctx| async move { Ok(my_exex(ctx)) })
.launch()
.await?;
handle.wait_for_node_exit().await
})
}
See that unused _ctx
? That's the context that we'll use to listen to new notifications coming from the main node,
and send events back to it. It also contains all components that the node exposes to the ExEx.
Currently, our ExEx does absolutely nothing by running an infinite loop in an async function that never returns.
It's important that the future returned by the ExEx (my_exex
) never resolves.
If you try running a node with an ExEx that exits, the node will exit as well.
Hello World ExEx
Now, let's extend our simplest ExEx and start actually listening to new notifications, log them, and send events back to the main node
use futures_util::TryStreamExt;
use reth::api::FullNodeComponents;
use reth_exex::{ExExContext, ExExEvent, ExExNotification};
use reth_node_ethereum::EthereumNode;
use reth_tracing::tracing::info;
async fn my_exex<Node: FullNodeComponents>(mut ctx: ExExContext<Node>) -> eyre::Result<()> {
while let Some(notification) = ctx.notifications.try_next().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().num_hash()))?;
}
}
Ok(())
}
fn main() -> eyre::Result<()> {
reth::cli::Cli::parse_args().run(|builder, _| async move {
let handle = builder
.node(EthereumNode::default())
.install_exex("my-exex", |ctx| async move { Ok(my_exex(ctx)) })
.launch()
.await?;
handle.wait_for_node_exit().await
})
}
Woah, there's a lot of new stuff here! Let's go through it step by step:
- First, we've added a
while let Some(notification) = ctx.notifications.recv().await
loop that waits for new notifications to come in.- The main node is responsible for sending notifications to the ExEx, so we're waiting for them to come in.
- Next, we've added a
match ¬ification { ... }
block that matches on the type of the notification.- In each case, we're logging the notification and the corresponding block range, be it a chain commit, revert, or reorg.
- Finally, we're checking if the notification contains a committed chain, and if it does, we're sending a
ExExEvent::FinishedHeight
event back to the main node using thectx.events.send
method.
Sending an ExExEvent::FinishedHeight
event is a very important part of every ExEx.
It's the only way to communicate to the main node that the ExEx has finished processing the specified height and it's safe to prune the associated data.
What we've arrived at is the minimal ExEx example that we provide in the reth-exex-examples repository.
What's next?
Let's do something a bit more interesting, and see how you can keep track of some state inside your ExEx.