FABRKNT
Reth Fundamentals — Your First Steps with Alloy
Working with Alloy
Lesson 2 of 11·CONTENT12 min25 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
Reth Fundamentals — Your First Steps with Alloy
Lesson role
CONTENT
Sequence
2 / 11

Lesson 2 — Alloy primitives and signing

Question

The first toolkit for working with Ethereum in Alloy — Address / U256 / B256 and signing. The type system distinguishes "this is an Address" from "this is a uint256", so you can't mix them by accident.

Principle (minimum model)

  • Three primitives. Address (20 bytes, contract or EOA) / U256 (256-bit unsigned, wei amounts) / B256 (32 bytes, hashes and slot keys).
  • .parse::<Address>(). String → Address, with checksum validation. Returns Err on invalid input.
  • U256 literals. U256::from(1_000_000) (from u64) / "1000000".parse()? (from string) / parse_ether("1")? (ETH units).
  • Signer trait. An abstraction over "holds a key and can sign". PrivateKeySigner::random() creates a fresh key; .address() returns the public address.
  • Ledger / AWS KMS via the same Signer trait. Detailed in Inside Alloy; for now "Signer = something that can sign" is enough.

Worked example + steps

Alloy primitives and signing

Time touch Alloy directly. Alloy is the de facto Ethereum library suite for Rust, and Reth uses it everywhere.

1. Project setup

cargo new hello_alloy
cd hello_alloy

Add to Cargo.toml under [dependencies]:

[dependencies]
alloy = { version = "1.0", features = ["full"] }
tokio = { version = "1", features = ["full"] }
eyre = "0.6"

Tip: versions move quickly. Check crates.io for the latest. eyre gives you nicer error messages.

2. Sign a message — real example

This is the entire sign_message.rs example from alloy-rs/examples:

//! Example of signing a message with a signer.

use alloy::signers::{local::PrivateKeySigner, Signer};
use eyre::Result;

#[tokio::main]
async fn main() -> Result<()> {
    // Set up a random signer.
    let signer = PrivateKeySigner::random();

    // Optionally, the wallet's chain id can be set, to use EIP-155
    // replay protection with different chains.
    let signer = signer.with_chain_id(Some(1337));

    // The message to sign.
    let message = b"hello";

    // Sign the message asynchronously with the signer.
    let signature = signer.sign_message(message).await?;

    println!("Signature produced by {}: {:?}", signer.address(), signature);
    println!("Signature recovered address: {}", signature.recover_address_from_msg(&message[..])?);

    Ok(())
}

Copy this into src/main.rs and run cargo run. You'll see your random signer's address, the signature, and a recovered address that matches.

sequenceDiagram
    participant Signer as PrivateKeySigner
    participant Msg as message bytes
    participant Hash as EIP-191 hash
    participant Sig as Signature
    participant Verify as recover_address_from_msg

    Signer->>Msg: take "hello"
    Msg->>Hash: prefix + keccak256
    Hash->>Sig: sign(privkey, hash)
    Sig-->>Verify: signature + original message
    Verify->>Hash: re-hash with prefix
    Verify-->>Signer: recovered address

3. What this code teaches

PrivateKeySigner::random()

Creates a new keypair via secure RNG. Never use this for real funds — it's for tests and learning. For production, load from environment variable, encrypted keystore, or hardware wallet.

with_chain_id(Some(1337))

EIP-155 wraps the chain ID into the signature so a tx signed for chain A can't be replayed on chain B. This is non-optional in production. 1337 is the typical local Anvil chain ID.

sign_message(message).await

Implements EIP-191 (the "Ethereum signed message" prefix) — what personal_sign over JSON-RPC and window.ethereum.request("personal_sign", ...) produce. The async-ness is because hardware wallets (Ledger/Trezor) take time to respond — even local signers expose the same interface for substitution.

signature.recover_address_from_msg(&message[..])

The verification side. Given a signature and the original message, recover the signing address. This is how you build "sign in with Ethereum" — the server picks a nonce, the user signs it, the server recovers the address. No password.

4. The address! macro

use alloy::primitives::address;

let recipient = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045");

address! is a procedural macro that runs at compile time. If you typo a hex digit or get the length wrong, the program won't compile — not "fail at runtime when the user clicks send." We'll see exactly how this macro is built in the Expert tier.

Why types matter so much

Solidity has address too, but Rust's type system is stricter:

  • A function expecting U256 will refuse a u64 at compile time
  • Address is its own type, not a generic 20-byte array
  • Mixing up Address and B256 produces a compile error
  • This is what gives Rust EVM code its reputation for safety in money-handling logic

Drill

Modify the example to:

  1. Sign the same message with two different chain IDs — print the signatures (they should differ)
  2. Try recover_address_from_msg against a modified message — the recovered address won't match. That's EIP-191's tamper resistance.

Next up: Result, Option, and ? — the error-handling vocabulary you'll need before touching a real Provider.

Summary (3 lines)

  • Three primitives: Address 20-byte / U256 256-bit / B256 32-byte. The type system separates "Address" from "wei" at compile time.
  • .parse() does string→type; U256::from() does numeric; parse_ether("1") does ETH units. Use U256, never f64, for money.
  • Signer abstracts "something that can sign"; PrivateKeySigner::random() makes a fresh key; .address() returns the public address. Next: Result/Option/?.