Lesson 6 — Wiring a custom opcode — and the failure modes
Question
Wire a custom opcode into the table + understand the failure modes (network forking, gas mispricing, etc.).
Principle (minimum model)
- Choose an unused opcode byte. 0xCL or 0xC0-0xCF range is convention for custom. Avoid Ethereum-reserved.
- Implement.
fn my_opcode<H: ?Sized, IT: ITy>(interp: &mut Interpreter<H, IT>) { gas!(interp, MY_GAS); /* stack ops */ }. Same shape as standard opcodes. - Wire into table.
instructions[0xCL] = my_opcode;. Replaces the INVALID default. - Failure mode 1: network forking. Your custom opcode means anyone running your chain sees different state than mainnet. Hard fork required.
- Failure mode 2: gas mispricing. Underprice → DoS attack vector. Overprice → unusable. Benchmark thoroughly.
- Failure mode 3: undocumented behaviour. Other clients don't implement; can't verify your chain. Coordinate with implementations.
- Production examples. OP Stack adds
L1MESSENGERopcode for L1 → L2 messages. HyperEVM adds order-book opcodes. - Tests. Spin up Revm with custom opcode; deploy a contract using it; assert correct behaviour + gas charge.
Worked example + steps
Wiring a custom opcode — and the failure modes
Hyperliquid runs perpetuals on its own EVM and added a handful of order-book-specific opcodes — direct calls into native code, dispatched as a single byte instead of a 200-instruction Solidity function. That's what a custom opcode buys you: a 100× shortcut on your own chain. The wiring is three lines. The shortcut is real. The reason most chains don't ship 50 of them is three caveats that are not optional.
You've built up revm's instruction table — a 256-slot array of Instruction structs, baked at compile time. Now slot in your own opcode.
This lesson is half mechanics (short) and half caveats (not short). The mechanics fit on a notecard. The caveats are why "Hyperliquid picked Revm because it's modular" is not a free lunch.
The mechanics — three lines
Pick an unallocated byte. Slot in your function:
const HYPER_FAST_SWAP: u8 = 0x0C;
let mut table = standard_table();
table[HYPER_FAST_SWAP as usize] = Instruction::new(my_hyper_fast_swap);
Where my_hyper_fast_swap follows the exact add shape from two lessons ago:
pub fn my_hyper_fast_swap<IT: ITy, H: ?Sized>(context: Ictx<'_, H, IT>) -> Result {
popn_top!([amount_in, pool_id], amount_out, context.interpreter);
*amount_out = compute_swap_native(*amount_in, *pool_id);
Ok(())
}
That's it. You took the standard table, copied it, overwrote one slot. The dispatch loop now routes byte 0x0C to your function.
flowchart LR
Std[standard_table — 256 slots] -->|copy| Mine[my fork's table]
Mine -->|override 0x0C| Custom[my_hyper_fast_swap]
Bytecode[bytecode 0x0C ...] -->|interpreter dispatch| Mine
Mine --> Custom
Custom --> Result[result on stack]
What this actually buys you
Two compounding wins:
- No interpreter loop overhead per inner step. A complex Solidity function might be 200 EVM instructions; one custom opcode is 1 dispatch.
- SIMD, FFI, or pre-computed tables in Rust. None of those are available to bytecode.
A complex options pricer can drop from 500K gas in Solidity → 5K gas as a single custom opcode. That's why Hyperliquid added perp-specific opcodes; that's the kind of compression payment-layer chains (Tempo, etc.) explore for stablecoin operations.
Caveats — these aren't optional
1. Consensus compatibility
Deviating from standard EVM means you can't share blocks with other Ethereum clients. Valid only on your own chain. Fork mainnet with this opcode and try to peer with go-ethereum → instant disconnect on the first transaction that touches 0x0C.
🔍 Reason about an experiment you could run. If you spun up a Reth node with your custom opcode, then pointed a stock geth at the same chain head: at what point does geth disconnect? (Answer: as soon as it tries to execute a block containing
0x0C. The block fails state-root validation because geth executes0x0Cas INVALID and your Reth executed it as a swap.) The "you can't share blocks" claim is something you should feel, not just read.
2. Gas pricing is not optional
A powerful shortcut needs a properly priced gas cost — otherwise it's a DoS vector.
A defensible methodology:
- Benchmark the worst case. Run the opcode against pathological inputs (max-size pool ID, max amount). Measure wall-clock time.
- Convert to a gas budget. Pick a target throughput (say, 1 second per block of pure-opcode load). Divide the budget by worst-case time.
- Add safety margin. 2–3× for variance, future hardware changes, and the gap between your benchmark and an attacker's benchmark.
If your three-sentence answer wasn't shaped like that, your opcode is a DoS waiting to happen.
3. Provability — if you want ZK
If your chain wants ZK proofs (a real concern for app-chains aiming at L2 settlement), every new opcode needs to be made provable inside your zkVM. That's potentially weeks of additional work per opcode.
This is why "we picked Revm because it's modular" doesn't translate to "we ship 50 custom opcodes." Each one carries:
- Consensus risk (you fork on every implementation bug)
- Pricing risk (DoS vector if mis-priced)
- Provability cost (weeks of zkVM integration if you want proofs)
The right number of custom opcodes for most chains is 0–3. Hyperliquid added a small handful. Most production app-chains exploring this end up with a similarly small footprint.
Recall before the quiz
Without scrolling:
- The mechanics of slotting in a custom opcode are three lines. Sketch them from memory.
- The "modular" pitch hides three caveats. What are they?
- What's the rough order-of-magnitude gas savings for compiling complex logic into a custom opcode? (And why?)
- If you wanted to ship a custom opcode that does pairing-friendly elliptic curve operations, which caveat hits hardest?
Next: a quiz that gates progression, then a drill where you actually wire one in a fork.
Summary (3 lines)
- Wiring = pick byte + implement + write into table[byte]. Custom opcode lives in your fork.
- Three failure modes: network fork + gas mispricing + undocumented behaviour. Each is fixable but requires discipline.
- Production examples: OP L1MESSENGER + HyperEVM order-book opcodes. Next: quiz.