-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
docs(book): tracking state in ExExes #8804
Changes from all commits
a33b79e
96d7515
7c0c203
4021553
1c1c38d
21ab44f
615e286
7a35aee
fa78ed0
bb5d140
5d8464d
240e085
1058710
9cfb8f8
5728ad8
f232b1b
bcf40bf
6858f75
99d345a
79e1495
47eb2a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
|
||
<div class="warning"> | ||
|
||
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. | ||
|
||
</div> | ||
|
||
```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<Node: FullNodeComponents> { | ||
ctx: ExExContext<Node>, | ||
} | ||
impl<Node: FullNodeComponents> Future for MyExEx<Node> { | ||
type Output = eyre::Result<()>; | ||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { | ||
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<Node: FullNodeComponents> { | ||
ctx: ExExContext<Node>, | ||
/// First block that was committed since the start of the ExEx. | ||
first_block: Option<BlockNumber>, | ||
/// Total number of transactions committed. | ||
transactions: u64, | ||
} | ||
impl<Node: FullNodeComponents> MyExEx<Node> { | ||
fn new(ctx: ExExContext<Node>) -> Self { | ||
Self { | ||
ctx, | ||
first_block: None, | ||
transactions: 0, | ||
} | ||
} | ||
} | ||
impl<Node: FullNodeComponents> Future for MyExEx<Node> { | ||
type Output = eyre::Result<()>; | ||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { | ||
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::<u64>(); | ||
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 | ||
}) | ||
} | ||
``` | ||
Comment on lines
+97
to
+176
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like that we basically repeat the whole snippet twice. How can we do it better? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we don't fully repeat the snippet but rather change some parts of it. I meant how we can do better at showing what changed in the code without repeating it all over again, but we can solve it later. |
||
|
||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also explain how you'd call out to other functions?
Prob we'd save most of the logic in async functions on the exex that the future loop calls?