FABRKNT
Inside Revm — Reading the EVM Engine
Inside Revm
Lesson 5 of 17·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
Inside Revm — Reading the EVM Engine
Lesson role
CONTENT
Sequence
5 / 17

Lesson 4 — Drill: prove you can read interpreter source

Question

Drill: read 3 more opcodes — mul / sub / exp — without help. Verify each uses the same macro pattern + understand what differs.

Principle (minimum model)

  • mul. Same shape as add. wrapping_mul instead of wrapping_add. Identical macro pattern.
  • sub. Same shape. wrapping_sub. Identical macros.
  • exp is different. Uses gas!_dynamic! because the cost depends on the exponent's byte length. Read the cost calculation.
  • Static-cost opcodes. Reuse the same gas!(interp, OPCODE_GAS) macro for fixed costs.
  • Dynamic-cost opcodes. Use gas!_dynamic!(interp, cost_fn) where cost_fn(state) computes the cost. Memory-expansion-aware.
  • Why this drill. If you can read mul / sub / exp unaided, you can read the rest of arithmetic.rs unaided. Pattern transferred.
  • Hot-path discipline. Every arithmetic opcode is hot. Macros + inline + wrapping_* are not optional. Production parallel: every Rust EVM uses the same discipline.

Worked example + steps

Drill: prove you can read interpreter source

You've read add and its macro. Now prove you can read the rest of the file without the lesson holding your hand. Three drills, run in a real revm checkout with cargo open in another window. Every one is do, then write down what you observed — not "read about."

Setup

git clone https://github.com/bluealloy/revm
cd revm
cargo build  # warm the toolchain

If cargo build failed, fix that before proceeding. The remaining drills assume a working build — there is no version of these drills that's "read along."

Drill 1 — Find mul, prove the shape

Open crates/interpreter/src/instructions/arithmetic.rs. Find the mul function. Compare it line-by-line to add.

The answer: both are 2-stack-in, 1-stack-out, fixed-gas, no-side-effect opcodes. Anything matching that profile compiles to the exact same control-flow shape: popn_top!([a], b, ctx.interpreter); *b = a.OP(*b); Ok(()). The differences are the OP (wrapping_add vs wrapping_mul) and the gas charge (both happen to be 3, in current Ethereum).

If your written answer was less specific than that, you didn't earn this drill — re-read.

Drill 2 — Find exp, find the dynamic gas charge

exp is in the same file. It's longer than add and mul. Two reasons:

  1. The math is more complex (it's exponentiation, not a single op).
  2. The gas charge is dynamic — it depends on the size of the exponent.

🔍 Find the gas! macro call inside exp that charges per byte of the exponent. Read its arithmetic.

The answer: dispatch can charge a fixed cost up-front, but exp's cost depends on a runtime value (the size of the exponent operand). You can only know that cost inside the function body, after you've inspected the operand. So exp charges itself.

This generalizes. Any opcode whose cost is shaped by an operand has to charge mid-function. File this — you'll meet the same pattern in sha3, mload, and the call-family opcodes.

Drill 3 — Break consensus on purpose

This is the canonical "prove you understand" drill. You won't believe how brittle consensus is until you've broken it on purpose.

  1. Open crates/interpreter/src/instructions/arithmetic.rs.
  2. Find add. Change wrapping_add to saturating_add. Save.
  3. From the repo root: cargo test -p revm-interpreter.
  4. Watch tests fail. Read at least one failure message — note that the failure is a numeric mismatch, not a panic.

What you've just done: patched a single library function call, and your client now disagrees with every other Ethereum client in the world about the result of 0xFFF...FF + 1. The first transaction that overflows ADD would fork your node off mainnet.

🔧 Now revert your change and confirm the tests pass again:

git checkout crates/interpreter/src/instructions/arithmetic.rs
cargo test -p revm-interpreter

The point isn't the change. It's the empirical proof that consensus is one library function call away from being lost — and now you've felt that.

If you didn't actually run cargo test and watch real output, you skipped the drill. The drill is the running. There's no version of this drill where you read it and "got it."

Drill 4 — Instrument add and watch the data flow

Reading source is one mode. Watching data move through the source you read is a stronger one.

  1. Open crates/interpreter/src/instructions/arithmetic.rs.

  2. Add eprintln! at the top and bottom of add:

    pub fn add<IT: ITy, H: ?Sized>(context: Ictx<'_, H, IT>) -> Result {
        popn_top!([op1], op2, context.interpreter);
        eprintln!("ADD: {:#x} + {:#x} = ?", op1, *op2);  // ← add this
        *op2 = op1.wrapping_add(*op2);
        eprintln!("ADD result: {:#x}", *op2);              // ← and this
        Ok(())
    }
    
  3. Re-run with cargo test -p revm-interpreter -- --nocapture. The --nocapture flag tells cargo to show eprintln! output.

  4. Read the output. Count how many times ADD ran across the test suite.

This is the production reality compressed to your terminal: every Ethereum mainnet transaction containing 0x01 causes this exact function to run, with these exact operands, producing this exact result. Across years, on millions of nodes.

🔧 Revert when done. git checkout crates/interpreter/src/instructions/arithmetic.rs. The eprintln! is for learning; production code never instruments hot paths this way (it uses the existing tracing integration — covered in Inside Reth).

After watching ADD execute, the "the interpreter is just a Rust function loop" framing stops being theoretical.

End-of-lesson recall

Without scrolling, in your own words on paper:

  1. What is the mechanical property of add and mul that makes their source identical?
  2. Why does exp charge gas inside its body instead of via dispatch?
  3. What single change converts add from EVM-compliant to EVM-incompatible — and what test failure mode did you observe?

If any answer is shaky, the lesson isn't done with you. Re-run the drill or re-read.

After this, you've read more EVM source than 99% of Solidity developers ever will — and you've proven it by making the chain disagree with itself, then putting it back. The next lesson zooms out from one opcode to the table that dispatches all 256.

Summary (3 lines)

  • Drill: read mul / sub / exp unaided. mul/sub use same shape as add; exp uses gas!_dynamic!.
  • Static vs dynamic cost gates are the differentiator. Same macro pattern throughout arithmetic.rs.
  • Pattern transferred. Next: custom opcodes module.