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.
Why a price at all
Section titled “Why a price at all”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.
Every opcode has a price
Section titled “Every opcode has a price”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.
Charge before you act
Section titled “Charge before you act”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:
- 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. - 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.
Revert is structural
Section titled “Revert is structural”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.
The architect’s lens
Section titled “The architect’s lens”- 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.
Check your understanding
Section titled “Check your understanding”- The
ethminigas schedule pricesADDat 3 andSSTOREat 200. What real-world burden justifies making a storage write the most expensive opcode of all? - 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?
- What are the two things an out-of-gas exceptional halt does that a clean
STOPdoes not? ethmini’srunmutates 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.- 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
- 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
SSTOREis a debt the whole network carries in perpetuity, so it is priced highest. - Gas is subtracted before the opcode’s effect runs; the charge is a check that fails with
OutOfGasif 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. - 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 cleanSTOPdoes neither: it reports only the gas actually spent and commits its writes. - The VM writes only into
working = storage.clone(). On a clean halt it returnsworkingfor the caller to commit; on any error it returnsErr, 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. - The estimate is a dry run: the node executes the transaction and sums the cost of every
opcode the path actually touched —
312is 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. anSLOADof a flag steering a cheap vs. expensive branch).