ethrex · hegota-devnet

The Hegotá EIP stack, explained

Seven EIPs that together replace Ethereum’s transaction signature with a small program, and one that decides whether the network is allowed to censor the result. This is a study guide to what each one does, why it exists, and how they lean on each other.

The one idea everything hangs off

An ordinary Ethereum transaction proves who sent it with a single ECDSA signature. The protocol checks that signature, charges the sender for gas, and runs the call. Authentication is fixed, and the person who authenticates is always the person who pays.

Frame transactions replace that signature with a sequence of small execution steps called frames. Instead of the protocol verifying a signature, the transaction carries code that runs on-chain and calls an opcode, APPROVE, to say “yes, this is authorised, and here is who pays.”

Once authentication is a program rather than a fixed algorithm, three things fall out that were previously impossible:

  • Any verification scheme works. A passkey, a multisig, a social-recovery rule, a zero-knowledge proof. If you can express it in EVM code, it can authorise a transaction.
  • The sender and the payer can differ. One account proves identity, a different account settles the gas bill. This is what people mean by “sponsored” or “paymaster” transactions, done natively instead of bolted on.
  • Several operations become one atomic unit. Approve a token and swap it in a single transaction that either fully happens or fully does not.

Everything else in this document is either an extension of that idea, a piece of plumbing it needs, or a rule about how the network is permitted to treat the result.

Naming

Hegotá is the fork name on the execution side. The consensus side calls the same fork heze. They activate at the same moment; you will see both names in configs and they are not two things.

How they fit together

The eight EIPs are not a flat list. There is a base, a set of extensions that widen it, a separate axis about censorship resistance, and a layer of pre-existing machinery they all stand on.

Dependency map of the Hegotá EIP stack EIP-8141 sits at the centre. EIP-8250, EIP-8272 and EIP-7906 each extend it, though only 8250 and 8272 run on this chain. EIP-7805 and EIP-8369 form a separate consensus axis that constrains it. EIP-7928, EIP-8037 and EIP-7843 sit underneath as existing machinery. THE GROUND · already in Amsterdam 7928 block access lists · 8037 two-dimensional gas · 7843 slot number EIP-8141 · Frame Transactions the base: type 0x06, frames, APPROVE EIP-8250 keyed nonces EIP-8272 recent roots EIP-7906 tx assertions EIP-7805 · FOCIL forced inclusion, anti-censorship EIP-8369 · eligibility which omissions are enforceable constrains
Solid boxes activate together at Hegotá. Dashed boxes are scheduled separately or not yet binding. The FOCIL pair runs on a different axis: it does not extend frame transactions, it limits what a block builder is allowed to leave out.

Read it in four groups:

  • The base. EIP-8141 defines the new transaction type. Nothing else in the stack means anything without it.
  • The extensions. 8250 and 8272 each add a field to that base, and are live here. 7906 would have added a frame kind of its own; it does not run on this chain.
  • The other axis. 7805 and 8369 are about censorship, not authentication. They say what a builder must include and what happens when it does not.
  • The ground. 7928, 8037 and 7843 already shipped in the previous fork. The stack borrows their machinery rather than reinventing it.
EIP-8141 spec b75cbe6115 Base · activates at Hegotá

Frame Transactions

The problem: a transaction’s signature check is hard-coded, and whoever signs must also pay. The fix: a new transaction type whose authentication is a small program you supply.

What a frame transaction looks like

It is transaction type 0x06. Where a normal transaction has v, r, s, this one has a list of frames:

0x06 || rlp([chain_id, nonce, sender, frames, signatures,
             max_priority_fee_per_gas, max_fee_per_gas,
             max_fee_per_blob_gas, blob_versioned_hashes])

each frame = [mode, flags, target, gas_limit, value, data]

A frame is one execution step. It has its own gas limit and its own target, and the mode byte says what role it plays:

ModeNameWhat it is for
0DEFAULTOrdinary execution. Also used for a “deploy” frame that installs code at the sender.
1VERIFYAuthentication. Runs before anything else and must call APPROVE.
2SENDERThe actual work, run as the sender.
3—Unassigned on this chain. EIP-7906 would have put its POST_TX result check here; that EIP is not in the build, and a frame declaring mode 3 is rejected.
4—Reserved for EIP-8288’s DEP_VERIFY, which is deferred. Also rejected.
5 and above—Reserved. Rejected, like 3 and 4.

The validation prefix, and why it matters everywhere else

The leading VERIFY frames are called the validation prefix. This is the part that establishes two facts before any real work happens: that the transaction is authorised, and who the payer is.

The prefix must match one of exactly four recognised shapes. This restriction is not cosmetic; it is what lets a node reason about an unfamiliar transaction cheaply.

ShapeMeaning
self_verifyOne VERIFY frame. The sender authorises and pays for itself.
deploy | self_verifySame, but first installs the sender’s verification code.
only_verify | payOne frame authorises, a second frame gets a sponsor to pay.
deploy | only_verify | payBoth of the above.

Remember the prefix. Nearly every hard question later in this document is really a question about it: how much gas it is allowed to burn, what state it is allowed to read, and whether a validator can afford to re-run it.

The opcodes it adds

ByteOpcodeDoes what
0xAAAPPROVEDeclares the transaction authorised, and collects the payer’s maximum possible cost up front.
0xB0TXPARAMReads a field of the transaction envelope.
0xB1–0xB2FRAMEDATALOAD / COPYReads the current frame’s data.
0xB3FRAMEPARAMReads a field of a frame.
0xB4SIGPARAMReads the supplied signature material.
Detail worth holding on to

APPROVE charges the payer the maximum cost, max_gas × max_fee_per_gas, not the eventual actual cost. The difference comes back as a refund after execution. Any shortcut that estimates the real cost instead will declare a payer solvent who then reverts in practice. This exact mistake was found and removed from a draft spec; see EIP-8369 below.

Receipts change too

One frame transaction produces one receipt containing a receipt per frame, plus the payer’s address. So you can see that frame 1 succeeded, frame 2 reverted, and frame 3 never ran, all inside one transaction.

EIP-8250 spec f3079a09e8 Extends 8141 · activates at Hegotá

Keyed Nonces

The problem: one account has one nonce counter, so its transactions must execute in a strict line. The fix: give an account many independent counters.

A nonce stops replay attacks by forcing transaction n+1 to follow transaction n. That is fine for a personal wallet, and painful for anything shared. A sponsor paying for thousands of users, or a contract acting for many people, has one counter and therefore one queue: everything serialises behind everything else.

EIP-8250 replaces the single nonce field with two:

nonce_keys : 1..16 strictly increasing uint256   // which counters
nonce_seq  : u64                                 // the value they must all be at

Each key is its own independent replay domain. Two transactions from the same sender on disjoint key sets have no ordering relationship at all and can be included in either order, or simultaneously.

Where it livesDetail
Key 0The account’s ordinary nonce. Backwards compatible.
Any other keyStored in the NONCE_MANAGER predeploy at 0x…8250.
Slot formulakeccak256(pad32(sender) || pad32(key))
Extra costA one-off charge the first time a given key is used.
Read this before you write code

This chain follows EIP-8250 at revision f3079a09e8: the TXPARAM indices are the spec’s (0x0D–0x10), and the first use of a keyed nonce costs state gas, one storage set (97,920) per fresh key, taken from the approving frame’s limits.state. A VERIFY frame that consumes two fresh nullifier keys must declare 195,840 of state budget; with none it halts and the transaction is invalid. The prefix’s state budgets together may not exceed 500,000.

There is no TXPARAM 0x11: EIP-8272 claims no index any more. A wallet built for the first launch that reads it gets an exceptional halt, which is the loud failure you want.

nonce_keys_hash is keccak256(be32(len) ‖ be32(k₀) ‖ …) over the whole key set. There is deliberately no per-index accessor: pass the key you care about in frame.data and check it against the digest, which commits to the entire set.

ValueToday’s EIP-8250This chain
pre-state legacy sender nonce0x0D0x0C
len(nonce_keys)0x0E0x0D
nonce_keys_hash0x0F0x0E
nonce_keys[0]0x100x10
Testing these

Every frame opcode halts with reason=Invalid Opcode when it runs outside a type-0x06 transaction — the same message an unassigned byte gives. An eth_call of 0x600eb0 (TXPARAM, a working opcode) proves it. Test frame opcodes inside a real frame transaction, or through ethrex_simulateFrameTransaction; a bare eth_call cannot tell “not implemented” from “wrong context”. To ask only whether a byte exists, send it with no operands: an implemented opcode reaches its stack check and answers Stack Underflow, an unassigned one answers Invalid Opcode.

Note the address convention, which recurs: a predeploy for EIP-N lives at 0x…N. It makes the constants self-documenting.

EIP-8272 spec 824cbc0b0e Extends 8141 · activates at Hegotá

Recent Roots

The problem: verification code often needs to check a proof against a recent commitment, but reading another contract’s mutable storage during validation is dangerous. The fix: a canonical verifier frame that proves the commitment before any application code runs.

Consider a privacy protocol. To spend, you prove membership in a Merkle tree whose root lives in some contract’s storage. Your validation code needs that root.

Reading it directly creates a problem the whole network feels. That storage slot is shared: it changes whenever anyone else uses the protocol. So a single change can invalidate every pending transaction that read it at once, which is exactly the mass-invalidation behaviour a mempool cannot tolerate.

EIP-8272 lets the transaction carry the roots it depends on, as the data of a canonical VERIFY frame that leads the transaction (first, or second behind an expiry verifier):

frame 0  VERIFY  target 0x…8272  flags 0  limits.state 0
         data = (source_id ‖ slot ‖ root) × 1..16        // 72 bytes each

The frame runs the RECENT_ROOT_ADDRESS predeploy at 0x…8272, which keeps a ring buffer of recent commitments and now has a validation operation: every tuple must name a slot before the current one and at most 8191 slots old, and its entry must be stored. A failing tuple reverts the frame, and a reverting VERIFY frame invalidates the transaction, so application code that runs afterwards can read the proven tuples back with FRAMEDATALOAD and check the frame’s status with FRAMEPARAM, never touching shared storage itself. The mempool checks the tuples natively before simulating anything, counts the frame’s gas toward MAX_VERIFY_GAS, and refuses a verifier frame anywhere but the leading position.

Why the ring buffer is bounded

Roots expire so the predeploy’s storage cannot grow forever. Roughly 8191 slots is about a day. Long enough that a transaction sitting in the mempool stays valid, short enough that the state stays bounded.

EIP-7906 Extends 8141 · NOT on this chain

Transaction Assertions

Not on this chain

EIP-7906 is described here for context only. It is not part of this network’s rule set and is absent from the binary the nodes run, so POST_TX frames (mode 3) are rejected and 0xB6, 0xB7 and 0xB8 are unassigned bytes that halt with an invalid-opcode error. The four EIPs this chain does run are 8141, 8250, 8272 and 7805.

The problem: you cannot tell what a transaction actually did until it is already mined. The fix: attach a check that runs afterwards and can undo the work.

A wallet simulates a transaction, sees a fair-looking swap, and submits it. Between simulation and inclusion the world moves, and what lands is worse. Or a contract sneaks in a token approval the user never intended.

EIP-7906 adds POST_TX frames (mode 3): trailing frames that run read-only after the transaction body, can inspect everything it did, and can revert it.

Three opcodes let them see the result:

ByteOpcodeReads
0xB6TXTRACEGas context and execution facts.
0xB7EVENTDATACOPYEvents the transaction emitted.
0xB8TXDIFFThe state changes it made, per address.
The subtle part

A POST_TX revert reverts the body, not the transaction. The transaction is still included, gets status = 0, and the payer is still fully charged. The validation prefix stays committed. That is the correct behaviour: the authentication really did happen and consumed real work, so it is paid for even though the intended effect was rolled back.

EIP-7805 spec 9a345f96c2 Consensus axis · FOCIL

Fork-Choice Enforced Inclusion Lists

The problem: a block builder can quietly refuse to include your transaction forever. The fix: let a committee publish a list of transactions the next block must contain.

Everything above this point is about the execution layer. FOCIL is about the consensus layer, and it answers a different question: not “is this transaction valid” but “is the network allowed to ignore it”.

Each slot, a committee of 16 includers each publish an inclusion list. The next block builder is expected to include those transactions. Attesters check afterwards, and a builder that left one out without a good reason does not get their votes.

“Without a good reason” is the whole design. An omission is excused when the transaction genuinely could not have been added: it would not fit in the remaining gas, its nonce is wrong, its balance is too low. Those checks are cheap for ordinary transactions, which is why FOCIL works today.

The collision

Frame transactions break that assumption. Their validity depends on running a program, so “could this have been included?” is no longer a cheap arithmetic check. Every attester would have to re-execute somebody else’s validation code, for every omitted transaction, before the attestation deadline. Resolving that is what the next EIP is for.

EIP-8369 spec 51dc7b939a Consensus axis · informational

VOPS Profiles for FOCIL Eligibility

The problem: checking whether a frame transaction was unfairly excluded could cost more work than validators can afford. The fix: only enforce the subset that is cheap enough to check.

The answer is to sort transactions into profiles. If a transaction fits a profile, its omission is enforceable. If it does not, it can still be listed and included, but nobody is punished for leaving it out.

ProfileCoversOmission judged
1 · BaseOrdinary transactions, no blobsAt the end of the block, as FOCIL already does.
2 · AA-VOPSFrame transactions inside a fixed state surfaceAt a position in the block the builder names.
—Everything else, including anything with blobsNever. Always excused.

The three limits that make it affordable

A bounded state surface. A qualifying validation prefix may read the sender’s and payer’s balance and nonce, and only their first few storage slots, a count called AA_VOPS_SLOT_COUNT. Reading anything else does not make the transaction expensive, it makes it ineligible. That numeric bound is also what excludes mapping entries, whose slots are keccak hashes and therefore enormous numbers.

A gas budget. Each transaction may declare at most 220 gas of validation work, and each inclusion list may hold 220 in total. Across 16 includers that is about 16.8M gas of replay per slot, roughly 28% of a 60M block. The EIP states that figure and openly flags that it is not yet a settled number.

A named index. Instead of forcing builders into a quadratic “try appending at every position” loop, the builder simply commits to a position and attesters check there. If the builder names nothing, the default is the end of the block.

What the index costs you

The party choosing the block contents also chooses the state the transaction is judged against. So the guarantee only fully holds for transactions whose validity is position-stable, meaning it cannot be flipped by reordering. Single-use nonces qualify. A shared sponsor’s balance does not: another sponsored transaction can drain it first. The EIP accepts this openly rather than papering over it.

EIP-8369 is Informational, which means it describes the model but binds nobody. Actual enforcement needs a further Standards Track EIP that does not exist yet, and AA_VOPS_SLOT_COUNT has no agreed value.

EIP-7928 · 8037 · 7843 The ground · already shipped

The machinery underneath

These arrived in the previous fork. The stack leans on them heavily, and several things above only make sense once you know they exist.

EIP-7928 — Block Access Lists

Every block carries a structured record of the state it touched: which accounts, which storage slots, what changed. It was built for parallel execution and stateless clients. FOCIL reuses it for something else entirely: reconstructing what the world looked like at a given position inside a block, which is exactly what judging an omission at a named index requires.

EIP-8037 — Two-dimensional gas

Gas splits into an execution dimension and a state-growth dimension, each with its own limit. A block runs out of state room before it runs out of compute, so anything cheap in state raises real throughput rather than just saving fees.

EIP-7843 — Slot number

The execution layer can read the current consensus slot. EIP-8272’s expiry window is expressed in slots, so recent roots depend directly on this.

One transaction, end to end

Here is a sponsored, passkey-authenticated, privacy-preserving payment that uses five of the eight at once. Follow the ordering, because the ordering is the design.

type 0x06 frame transaction
  nonce_keys  [0x9f3a…]        ← 8250: an independent replay lane

  frame 0  VERIFY  0x…8272       ← 8272: proves the commitment its proof needs
  frame 1  VERIFY  only_verify   ← the validation prefix
  frame 2  VERIFY  pay             establishes authorisation + payer
  ──────────────────────────────
  frame 3  SENDER                ← the body: the actual transfer
  frame 4  POST_TX               ← 7906: check nothing unexpected happened
  1. Before any code runs, the protocol checks the keyed nonces (8250) and, at admission, the recent-root tuples (8272) against the predeploy’s storage.
  2. Frame 0 runs the recent-root predeploy’s validation operation over the tuples. If any is stale or unknown the frame reverts and the transaction is invalid; nothing after it runs.
  3. Frame 1 verifies the passkey signature and reads the proven tuples back from frame 0 with FRAMEDATALOAD, never from shared storage, so no other user’s activity can invalidate it.
  4. Frame 2 runs the sponsor’s logic and calls APPROVE. The sponsor is now the payer and is charged the maximum possible cost.
  5. Frame 3 does the real work as the sender.
  6. Frame 4 inspects the finished result with TXDIFF. If an invariant broke, it reverts frame 3 — but frames 0 to 2 stay committed and the sponsor still pays.
  7. Separately, FOCIL (7805) may have listed this transaction. Whether a builder can be punished for omitting it is decided by 8369: frames 0 to 2 must fit the bounded state surface and the gas budget.

Notice that the validation prefix, frames 0 and 1, is doing three jobs at once. It authenticates, it decides who pays, and it is the thing FOCIL has to be able to afford to re-run. Every difficult constraint in the stack lands on those two frames.

What this branch changes

These are all draft EIPs, so parts collide or are simply blank. hegota-devnet records every place it had to decide something the specs do not settle.

WhereSpec saysThis branch doesWhy
AA_VOPS_SLOT_COUNTunset, “2 to 4, pending benchmarks”4, as a config value Top of the range is the worst case for validator work, so a result that passes at 4 passes at 2 and 3. Configurable so the range can be measured.
Storage rule when a node does both jobssilentWhichever check is running picks its own rule The mempool rule and the FOCIL rule overlap without either containing the other. Raised upstream as an open question.
NONCEKEYLOAD at 0xB9no such opcodeno such opcode Was an ethrex-only convenience for reading an individual nonce key; removed before this chain launched, so 0xB9 is unassigned and using it halts with an invalid-opcode error. An opcode no other client implements is consensus-visible in its byte, its gas and its out-of-range behaviour, so a transaction using it would split this chain. Read keyed-nonce state through TXPARAM instead: 0x0D for len(nonce_keys), 0x0E for the key-set digest, 0x10 for nonce_keys[0].

Two of these are worth understanding as a pattern rather than trivia. The mode 3 collision happens because two draft EIPs picked the same byte without a shared registry. The blank AA_VOPS_SLOT_COUNT happens because the spec author correctly refused to guess a number that needs measurement. Both are the ordinary texture of implementing drafts, and writing the decision down is what keeps the divergence honest.

How it switches on

Almost everything here activates together at the Hegotá fork, using one timestamp. Two things do not.

FeatureSwitchNotes
8141, 8250, 8272, 7906hegotaTimeOne fork, all at once. Also called heze on the consensus side.
7805 FOCILhegotaTimeSame fork, but it is the one feature that is not purely execution-layer.
The one that bites

Every other feature here is invisible to the consensus client: it schedules the fork and the execution layer does the rest. FOCIL is different. It needs a consensus client that builds and gossips inclusion lists and calls new engine methods. So turning FOCIL on means upgrading both layers at the same moment, with no working state in between. That, rather than any of the code, is the real obstacle to running it.

A useful habit

Giving a feature its own future timestamp, rather than folding it into a fork, means every block already produced re-executes identically. A running chain can adopt the feature without starting over. It is a technique worth reaching for whenever a change is not yet certain.