From ff4a114e43af2437288a145e12fc14a7aa8f1f74 Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Mon, 17 Jun 2024 11:42:27 +0100 Subject: [PATCH] docs(book): tracking state in ExExes (#8804) Co-authored-by: Dan Cline <6798349+Rjected@users.noreply.github.com> Co-authored-by: Oliver --- book/SUMMARY.md | 1 + book/developers/exex/exex.md | 1 + book/developers/exex/hello-world.md | 4 + book/developers/exex/tracking-state.md | 193 +++++++++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100644 book/developers/exex/tracking-state.md diff --git a/book/SUMMARY.md b/book/SUMMARY.md index e7fa1ea68048..499b6dd97f9a 100644 --- a/book/SUMMARY.md +++ b/book/SUMMARY.md @@ -76,5 +76,6 @@ - [Execution Extensions](./developers/exex/exex.md) - [How do ExExes work?](./developers/exex/how-it-works.md) - [Hello World](./developers/exex/hello-world.md) + - [Tracking State](./developers/exex/tracking-state.md) - [Remote](./developers/exex/remote.md) - [Contribute](./developers/contribute.md) diff --git a/book/developers/exex/exex.md b/book/developers/exex/exex.md index f0cd08d4a586..b65d3173677b 100644 --- a/book/developers/exex/exex.md +++ b/book/developers/exex/exex.md @@ -27,3 +27,4 @@ and run it on the Holesky testnet. 1. [How do ExExes work?](./how-it-works.md) 1. [Hello World](./hello-world.md) +1. [Tracking State](./tracking-state.md) diff --git a/book/developers/exex/hello-world.md b/book/developers/exex/hello-world.md index c3da13ac4cc8..0f50cacbb9a6 100644 --- a/book/developers/exex/hello-world.md +++ b/book/developers/exex/hello-world.md @@ -160,3 +160,7 @@ and it's safe to prune the associated data. What we've arrived at is the [minimal ExEx example](https://github.com/paradigmxyz/reth/blob/b8cd7be6c92a71aea5341cdeba685f124c6de540/examples/exex/minimal/src/main.rs) that we provide in the Reth repository. + +## What's next? + +Let's do something a bit more interesting, and see how you can [keep track of some state](./tracking-state.md) inside your ExEx. diff --git a/book/developers/exex/tracking-state.md b/book/developers/exex/tracking-state.md new file mode 100644 index 000000000000..5fe8b1c9ef83 --- /dev/null +++ b/book/developers/exex/tracking-state.md @@ -0,0 +1,193 @@ +# Tracking State + +In this chapter, we'll learn how to keep track of some state inside our ExEx. + +Let's continue with our Hello World example from the [previous chapter](./hello-world.md). + +### Turning ExEx into a struct + +First, we need to turn our ExEx into a stateful struct. + +Before, we had just an async function, but now we'll need to implement +the [`Future`](https://doc.rust-lang.org/std/future/trait.Future.html) trait manually. + +
+ +Having a stateful async function is also possible, but it makes testing harder, +because you can't access variables inside the function to assert the state of your ExEx. + +
+ +```rust,norun,noplayground,ignore +use std::{ + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use reth::api::FullNodeComponents; +use reth_exex::{ExExContext, ExExEvent, ExExNotification}; +use reth_node_ethereum::EthereumNode; +use reth_tracing::tracing::info; + +struct MyExEx { + ctx: ExExContext, +} + +impl Future for MyExEx { + type Output = eyre::Result<()>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + while let Some(notification) = ready!(this.ctx.notifications.poll_recv(cx)) { + 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() { + this.ctx + .events + .send(ExExEvent::FinishedHeight(committed_chain.tip().number))?; + } + } + + Poll::Ready(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(MyExEx { ctx }) }) + .launch() + .await?; + + handle.wait_for_node_exit().await + }) +} +``` + +For those who are not familiar with how async Rust works on a lower level, that may seem scary, +but let's unpack what's going on here: + +1. Our ExEx is now a `struct` that contains the context and implements the `Future` trait. It's now pollable (hence `await`-able). +1. We can't use `self` directly inside our `poll` method, and instead need to acquire a mutable reference to the data inside of the `Pin`. + Read more about pinning in [the book](https://rust-lang.github.io/async-book/04_pinning/01_chapter.html). +1. We also can't use `await` directly inside `poll`, and instead need to poll futures manually. + We wrap the call to `poll_recv(cx)` into a [`ready!`](https://doc.rust-lang.org/std/task/macro.ready.html) macro, + so that if the channel of notifications has no value ready, we will instantly return `Poll::Pending` from our Future. +1. We initialize and return the `MyExEx` struct directly in the `install_exex` method, because it's a Future. + +With all that done, we're now free to add more fields to our `MyExEx` struct, and track some state in them. + +### Adding state + +Our ExEx will count the number of transactions in each block and log it to the console. + +```rust,norun,noplayground,ignore +use std::{ + future::Future, + pin::Pin, + task::{ready, Context, Poll}, +}; + +use reth::{api::FullNodeComponents, primitives::BlockNumber}; +use reth_exex::{ExExContext, ExExEvent}; +use reth_node_ethereum::EthereumNode; +use reth_tracing::tracing::info; + +struct MyExEx { + ctx: ExExContext, + /// First block that was committed since the start of the ExEx. + first_block: Option, + /// Total number of transactions committed. + transactions: u64, +} + +impl MyExEx { + fn new(ctx: ExExContext) -> Self { + Self { + ctx, + first_block: None, + transactions: 0, + } + } +} + +impl Future for MyExEx { + type Output = eyre::Result<()>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + while let Some(notification) = ready!(this.ctx.notifications.poll_recv(cx)) { + if let Some(reverted_chain) = notification.reverted_chain() { + this.transactions = this.transactions.saturating_sub( + reverted_chain + .blocks_iter() + .map(|b| b.body.len() as u64) + .sum(), + ); + } + + if let Some(committed_chain) = notification.committed_chain() { + this.first_block.get_or_insert(committed_chain.first().number); + + this.transactions += committed_chain + .blocks_iter() + .map(|b| b.body.len() as u64) + .sum::(); + + this.ctx + .events + .send(ExExEvent::FinishedHeight(committed_chain.tip().number))?; + } + + if let Some(first_block) = this.first_block { + info!(%first_block, transactions = %this.transactions, "Total number of transactions"); + } + } + + Poll::Ready(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(MyExEx::new(ctx)) }) + .launch() + .await?; + + handle.wait_for_node_exit().await + }) +} +``` + +As you can see, we added two fields to our ExEx struct: +- `first_block` to keep track of the first block that was committed since the start of the ExEx. +- `transactions` to keep track of the total number of transactions committed, accounting for reorgs and reverts. + +We also changed our `match` block to two `if` clauses: +- First one checks if there's a reverted chain using `notification.reverted_chain()`. If there is: + - We subtract the number of transactions in the reverted chain from the total number of transactions. + - It's important to do the `saturating_sub` here, because if we just started our node and + instantly received a reorg, our `transactions` field will still be zero. +- Second one checks if there's a committed chain using `notification.committed_chain()`. If there is: + - We update the `first_block` field to the first block of the committed chain. + - We add the number of transactions in the committed chain to the total number of transactions. + - We send a `FinishedHeight` event back to the main node. + +Finally, on every notification, we log the total number of transactions and +the first block that was committed since the start of the ExEx.