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. ReturnsErron invalid input.U256literals.U256::from(1_000_000)(from u64) /"1000000".parse()?(from string) /parse_ether("1")?(ETH units).Signertrait. 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
Signertrait. 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.
eyregives 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
U256will refuse au64at compile time Addressis its own type, not a generic 20-byte array- Mixing up
AddressandB256produces a compile error - This is what gives Rust EVM code its reputation for safety in money-handling logic
Drill
Modify the example to:
- Sign the same message with two different chain IDs — print the signatures (they should differ)
- Try
recover_address_from_msgagainst 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:
Address20-byte /U256256-bit /B25632-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. UseU256, neverf64, for money.Signerabstracts "something that can sign";PrivateKeySigner::random()makes a fresh key;.address()returns the public address. Next: Result/Option/?.