FABRKNT
Inside Reth — Sync, Extensions, and the SDK
The Reth Stack — Sync, Extensions, and the SDK
Lesson 6 of 17·CONTENT15 min30 XP

Treat this page as a workbench, not a blog post. The goal is to extract a reusable mental model from the source and carry it into the rest of the Fabrknt stack.

Course
Inside Reth — Sync, Extensions, and the SDK
Lesson role
CONTENT
Sequence
6 / 17

Lesson 5 — Rust: lifetimes, Box, Arc, dyn Trait

Question

Reading Reth source requires deeper Rust than dapp work. Lifetimes (<'a>), Box<dyn Trait>, Arc<dyn Trait + Send + Sync> — what they mean, when each is right.

Principle (minimum model)

  • Lifetimes (<'a>). Compile-time guarantee that references outlive what they point to. Most uses are inferred; rarely-written.
  • Box<dyn Trait>. Owned trait object on the heap. Used when you need polymorphism but ownership.
  • Arc<dyn Trait>. Shared trait object across tasks. Add + Send + Sync for Tokio. Most Reth components are Arc<dyn ComponentTrait + Send + Sync>.
  • &dyn Trait / &mut dyn Trait. Borrowed; cheaper than Box. Use when you don't need ownership.
  • impl Trait (sugar). Sugar for "anonymous generic". fn foo() -> impl Future<...> returns a Future without naming the concrete type.
  • Generics vs dyn Trait trade-off. Generics monomorphise (compile-time); each concrete type generates its own code. dyn dispatches at runtime via vtable; smaller binary; slightly slower.
  • Reth conventions. Components passed as Arc<dyn ... + Send + Sync>; per-call hot-path uses generics; mid-frequency uses dyn.

Worked example + steps

Rust: lifetimes, Box, Arc, dyn Trait

Open any ExEx source file. The first ten lines will throw Arc<>, 'static, dyn Trait, and Box<> at you. Four "advanced but actually simple" Rust features — and you need every one to read the code Reth ships with. This lesson tests you, not teaches you — if you stumble on the predict prompts, the gap is real and worth closing.

1. Lifetimes 'a

A lifetime annotation tells the compiler how long a borrow lives.

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() >= y.len() { x } else { y }
}
  • 'a is a label for "some lifetime"
  • Both inputs and the output share the same 'a → "the returned reference lives at least as long as both inputs"
  • Often the compiler infers them — most signatures don't need explicit lifetimes

'static

'static means "lives for the entire program." String literals are &'static str:

let s: &'static str = "hello";

Long-running tasks (like ExEx) often require 'static bounds because they can outlive any local scope.

2. Box<T> — heap allocation

When you want a value on the heap rather than the stack, wrap it in Box<T>:

let boxed: Box<i64> = Box::new(42);
println!("{}", *boxed);   // 42

Common reasons:

  • Recursive types (linked lists) need a fixed-size pointer
  • Holding a dynamically-sized value (dyn Trait)
  • Move large values cheaply instead of copying

3. Rc<T> and Arc<T> — shared ownership

Rust's "one owner" rule sometimes gets in the way: you want multiple parts of the program to hold the same value.

TypeUse
Rc<T>single-threaded reference counting
Arc<T>thread-safe (atomic reference counting)
use std::sync::Arc;

let shared = Arc::new(String::from("hello"));
let clone1 = Arc::clone(&shared);   // refcount += 1
let clone2 = Arc::clone(&shared);   // refcount += 1

// Send to another thread — Arc is Send
std::thread::spawn(move || println!("{}", clone1));

Reth and ExEx code is full of Arc<...> — multiple async tasks need to read the same component.

4. Mutex / RwLock — shared mutability

Arc<T> alone is read-only. To mutate shared state, wrap in Mutex or RwLock:

use std::sync::{Arc, Mutex};

let counter = Arc::new(Mutex::new(0));

let c = Arc::clone(&counter);
std::thread::spawn(move || {
    let mut n = c.lock().unwrap();
    *n += 1;
});
TypeUse
Mutexexclusive read/write
RwLockmany readers OR one writer

5. dyn Trait — dynamic dispatch

A trait object — like a TypeScript / Java interface, but you opt into runtime method resolution explicitly.

trait Greet {
    fn greet(&self);
}

struct En;
struct Ja;
impl Greet for En { fn greet(&self) { println!("Hello"); } }
impl Greet for Ja { fn greet(&self) { println!("Hello"); } }

let g: Box<dyn Greet> = if std::env::var("LANG").unwrap_or_default().starts_with("ja") {
    Box::new(Ja)
} else {
    Box::new(En)
};
g.greet();

impl Trait vs dyn Trait

FormMeaning
impl Traitstatic dispatch (concrete type at compile time)
dyn Traitdynamic dispatch (resolve at runtime, needs Box or & )

impl is faster, but dyn lets you have heterogeneous collections like Vec<Box<dyn Trait>>.

6. What you'll see in ExEx code

async fn my_exex<Node: FullNodeComponents>(
    mut ctx: ExExContext<Node>,
) -> eyre::Result<()> {
    while let Some(notification) = ctx.notifications.recv().await {
        // ...
    }
    Ok(())
}
  • Node: FullNodeComponents — trait bound
  • ExExContext<Node> — generic over the node bundle
  • Internally uses Arc<...> for shared components
  • Lifetime annotations are elided but 'static is required

Cheat sheet

FeatureOne-liner
'a / 'statichow long borrows live
Box<T>heap allocation
Rc<T> / Arc<T>shared ownership (Arc is thread-safe)
Mutex<T>safely mutate shared data
dyn Traitruntime method dispatch

Final check: close this tab. Write the ExEx my_exex signature from memory. If you can't, you don't yet own the vocabulary — open it back up. The next lesson reads ExEx in detail; you'll need each of these without the cheat sheet.

Summary (3 lines)

  • Lifetimes (<'a>) + Box<dyn Trait> + Arc<dyn Trait + Send + Sync> + &dyn Trait + impl Trait. Each has a specific use case.
  • Reth conventions: components as Arc<dyn ... + Send + Sync>; hot-path generics; mid-frequency dyn.
  • Reading Reth needs this fluency. Next: ExEx buildup.