Skip to content

Gas — A Price on Every Opcode

The last three pages built a machine. The EVM overview framed it as a world computer; a stack machine of 256-bit words gave it an instruction set; stack, memory, storage, and calldata gave it places to keep data. But a machine that can JUMP backwards can loop, and a machine that can loop can loop forever. On your laptop that is your problem. On a chain where thousands of untrusting strangers must each re-run your code to agree on the result, one infinite loop would hang every node on Earth.

This page is about the one mechanism that makes running arbitrary, untrusted bytecode safe on a shared computer: gas. Every opcode has a price. You buy a budget up front, the machine spends it as it runs, and the instant the budget hits zero the machine halts and throws away everything it did. That is the whole idea. The rest of this page is why that shape — charge before you act, halt-and-revert on empty — is exactly the right one, and what it costs to get wrong.

Recall the throughline of this book: how do untrusting strangers agree on the state of a shared world computer — and what does it cost to run one? Gas is the literal answer to the second half. Every opcode a contract executes is work that every full node must redo to verify the block. That work is real: CPU cycles to add two numbers, disk reads to fetch a storage slot, and — worst of all — permanent bytes of state that every node keeps forever.

Without a meter, a transaction could ask the network to do unbounded work for free. With a meter, work has a price, and the sender pays it in advance. Two problems dissolve at once:

  • Termination. A program that would run forever instead runs until its budget is gone, then halts. You do not need to prove a contract terminates — you bound it.
  • Fair pricing. A transaction that touches a lot of state costs more than one that adds two numbers, because it is more work. The price is proportional to the burden imposed on the network.

Gas quietly converts a famous unsolvable problem into a solved one. Deciding whether an arbitrary program halts is the halting problem — provably undecidable in general. The EVM does not decide it. It sidesteps it: we will not ask whether your program halts; we will only let it run as far as your gas takes it. A logic question with no answer becomes an economic question with a very simple answer — you get exactly what you paid for, and not one opcode more.

The price list is not arbitrary. Costs are ordered by real resource burden: the more work an opcode forces on every node, the more it costs. Our companion VM, ethmini, keeps a deliberately simplified schedule that preserves that shape even though the exact numbers differ from mainnet:

// rust/ethmini/src/vm.rs — the gas schedule, EVM-inspired and *ordered* like the real one.
pub fn gas_cost(&self) -> u64 {
match self {
Op::Stop | Op::Return => 0, // halting is free
Op::JumpDest => 1, // a landing pad; does nothing
Op::Push(_) | Op::Pop | Op::Add | Op::Sub => 3, // cheap arithmetic
Op::Mul => 5,
Op::Jump => 8,
Op::JumpI => 10,
Op::SLoad => 100, // read a storage slot — dear
Op::SStore => 200, // write a storage slot — dearest
}
}

Read that schedule top to bottom and you are reading a ranking of how much each operation hurts the network:

opcode cost why it costs what it does
--------- ---- ------------------------------------------------------
ADD / SUB 3 pure CPU: pop two words, push one. Trivial.
MUL 5 still CPU, a hair more work than addition.
JUMP 8 control flow; must be re-validated against JUMPDESTs.
SLOAD 100 a *read* from persistent storage — a disk/trie lookup.
SSTORE 200 a *write* to persistent storage — grows state FOREVER.

The single most important line is the last one. SSTORE is the dearest opcode because it is the only one that grows permanent state — a slot that every full node, present and future, must store and carry forever. Arithmetic is transient: it happens, it produces a word, the word is discarded when the call ends. A storage write is a debt the whole network takes on in perpetuity. The price reflects the burden, and the biggest burden is the one that never goes away.

When the meter is debited matters as much as the price itself. The rule is: charge before you act. Gas for an opcode is subtracted before the opcode runs. If the remaining budget cannot cover the cost, the op simply does not happen and the machine halts out of gas.

Here is the metering closure from the VM’s interpreter loop:

// rust/ethmini/src/vm.rs — inside run(): pay first, then act.
let charge = |gas: &mut u64, cost: u64| -> Result<(), VmError> {
if *gas < cost {
Err(VmError::OutOfGas { used: gas_limit, limit: gas_limit })
} else {
*gas -= cost; // debit BEFORE the op's effect
Ok(())
}
};
loop {
if pc >= code.len() { break; } // running off the end is an implicit STOP
let (op, next) = Op::decode(code, pc)?;
charge(&mut gas, op.gas_cost())?; // <-- pay here, or bail with OutOfGas
match op {
// ... only now do we mutate the stack / storage ...
}
}

Because the charge happens first, the meter can never go negative and no half-paid opcode can ever mutate state. A contract that cannot afford its next SSTORE never performs the SSTORE. There is no partial write, no “it ran but didn’t finish paying” — the op either executes in full because it was paid for in full, or it does not execute at all. This is what lets us reason about the machine’s cost as a clean sum: every opcode that happened was paid for, and the receipt’s gas_used is exactly that sum.

Under the hood — out of gas is an exceptional halt

Section titled “Under the hood — out of gas is an exceptional halt”

Not all halts are equal. A STOP or RETURN is a clean halt: the program finished, its storage writes commit, its receipt reports success. Running out of gas is an exceptional halt: the program was cut off mid-flight, and the machine treats that as a failure.

An exceptional halt does two things that a clean halt does not:

  1. It consumes the entire gas budget. Notice the error above reports used: gas_limit, not the gas actually spent so far. When you run out of gas, you forfeit all of it. Why? Because the network already did the work of running your program up to the point it died — verifying nodes still burned those cycles. You pay for the work done, and the deterrent against submitting a transaction that gambles on running out is that a failed gamble is not refunded.
  2. It reverts every state change. Which brings us to the mechanism that makes any of this safe.

This is the answer to the question every newcomer asks: what stops someone from deploying an infinite loop and freezing the chain? Nothing stops them from deploying it. What stops the damage is that calling it runs exactly gas_limit opcodes and then halts — out of gas, budget forfeited, state reverted. ethmini has a test that proves it:

#[test]
fn infinite_loop_is_bounded_by_gas() {
// JUMPDEST ; PUSH 0 ; JUMP -> jumps back to pc 0 forever.
let code = assemble(&[Op::JumpDest, Op::Push(0), Op::Jump]);
let err = run(&code, &empty(), 1000).unwrap_err();
assert!(matches!(err, VmError::OutOfGas { limit: 1000, .. }));
}

The loop is genuinely infinite. It still returns in microseconds, because gas — not logic — decides when it stops.

An exceptional halt reverts all state changes. But how — mechanically — does a machine “undo” everything a half-run program did? The trick is beautiful and it is not an undo at all. The VM never mutates the real storage in the first place. It mutates a clone, and only hands the clone back on a clean halt:

// rust/ethmini/src/vm.rs — run() operates on a copy, commits only on success.
pub fn run(code: &[u8], storage: &BTreeMap<u64, u64>, gas_limit: u64)
-> Result<Execution, VmError>
{
let mut working = storage.clone(); // <-- mutate a COPY, never the original
// ... the interpreter writes into `working` on every SSTORE ...
// On a clean halt we return the working copy for the caller to commit:
Op::Stop => return Ok(Execution { gas_used: gas_limit - gas,
return_value: None,
storage: working }),
// On ANY error we return Err — and `working` is simply dropped.
}

There is no rollback log, no journal to replay in reverse. If the run throws — out of gas, stack underflow, a bad opcode — the function returns Err, the working clone falls out of scope and is discarded, and the caller still holds the original storage, untouched. State changes are all-or-nothing: they exist only on the clone until the program cleanly commits them. This is atomicity as a structural property, not a feature bolted on afterward.

original storage ──clone──▶ working copy
│ │
│ SSTORE writes land HERE
│ │
┌────┴────┐ ┌─────┴─────┐
clean halt? exceptional halt?
STOP/RETURN OutOfGas / underflow / bad op
│ │
commit working drop working
(original replaced) (original untouched)

The layer above the VM applies the same idea one level up. In ethmini’s state machine, a transaction that reverts still consumes the sender’s nonce — so it can never be replayed — even though every other change is rolled back. That deliberate split is why a failed transaction is still included in a block but leaves storage as it was; we develop it fully in Execution context and the CALL family and lean on it again in why every node computes the same result.

A gas estimate is really an execution trace

Section titled “A gas estimate is really an execution trace”

Wallets show you a gas estimate before you sign. It can feel like a black box — some oracle guessing a number. It is not a guess. A gas estimate is a dry run: the node executes the transaction against current state, tallies the cost of every opcode the execution actually touched, and reports the sum. The counter’s 312 is not a formula anyone wrote down — it is the sum of the specific path the machine walked this time.

That “this time” is the catch, and it is why estimates can be wrong. Gas is the cost of the path taken, and the path depends on state. A contract whose first SLOAD reads a flag might take a cheap branch or an expensive one depending on what that flag currently holds. Estimate against today’s state, execute against tomorrow’s, and the path — and therefore the cost — can differ. The estimate was an honest trace; the world simply moved between the trace and the signature.

The practical lesson: gas is a property of an execution, not of a contract. There is no single “cost of calling this function.” There is only the cost of the path a particular call, against a particular state, actually walks — which is exactly the number you get by summing the opcodes it ran.

  • Why does it exist? Because a shared, public VM must run untrusted, possibly non-terminating code that every node re-executes. Gas exists to bound that execution and to price the real work it imposes, so no one can make the network do unlimited work for free.
  • What problem does it solve? Termination and fair cost in one stroke. It converts the undecidable halting problem into a decidable budget (“run until the gas is gone”) and makes the price of a transaction proportional to the burden it puts on every node.
  • What are the trade-offs? The gas schedule must track real resource cost or it becomes an attack surface (see Tangerine Whistle). Users must reason about a fluctuating cost, estimates can be wrong when state shifts under them, and “charge before you act, revert on empty” means a failed transaction still costs gas.
  • When should I avoid it? You never opt out of gas on a public chain — it is load-bearing. What you can avoid is treating it casually: never assume a call’s cost is fixed, and never put unbounded loops over user-controlled data in a contract, because the gas ceiling will halt them mid-write.
  • What breaks if I remove it? Everything the throughline depends on. Remove gas and one infinite loop hangs every node; remove the charge-before-you-act rule and a half-paid opcode can mutate state; remove the revert-on-empty rule and a program cut off mid-flight leaves storage in a corrupt, partially-written mess. Gas is what makes untrusted code safe to share.
  1. The ethmini gas schedule prices ADD at 3 and SSTORE at 200. What real-world burden justifies making a storage write the most expensive opcode of all?
  2. Explain “charge before you act.” Why does debiting gas before an opcode runs guarantee the meter can never go negative and no half-paid opcode can mutate state?
  3. What are the two things an out-of-gas exceptional halt does that a clean STOP does not?
  4. ethmini’s run mutates a clone of storage and only returns it on a clean halt. Explain how this one design choice gives you all-or-nothing revert with no rollback log.
  5. The counter contract costs exactly 312 gas per call. In what sense is a gas estimate really an execution trace, and why can two calls to the same contract cost different amounts?
Show answers
  1. A storage write grows permanent state — a slot that every full node, now and forever, must keep. Arithmetic is transient (the word is discarded when the call ends), but an SSTORE is a debt the whole network carries in perpetuity, so it is priced highest.
  2. Gas is subtracted before the opcode’s effect runs; the charge is a check that fails with OutOfGas if the remaining budget is smaller than the cost. So the budget is only ever reduced by an amount it could cover, never below zero, and an op that cannot pay simply does not execute — there is no partial, half-paid mutation.
  3. It consumes the entire gas budget (reports used = gas_limit, forfeiting all of it, because nodes already did the work), and it reverts every state change the call made. A clean STOP does neither: it reports only the gas actually spent and commits its writes.
  4. The VM writes only into working = storage.clone(). On a clean halt it returns working for the caller to commit; on any error it returns Err, and the clone is simply dropped when the function returns. The original storage was never touched, so “undo” is free — there is nothing to roll back, because nothing real was ever written until the commit.
  5. The estimate is a dry run: the node executes the transaction and sums the cost of every opcode the path actually touched — 312 is the tally of the counter’s exact opcodes, not a formula. Two calls can differ because gas is the cost of the path taken, and the path depends on current state (e.g. an SLOAD of a flag steering a cheap vs. expensive branch).