# 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.