Skip to content

core: implement eip-8141 frames tx - #35666

Draft
lightclient wants to merge 27 commits into
ethereum:masterfrom
lightclient:frames
Draft

core: implement eip-8141 frames tx#35666
lightclient wants to merge 27 commits into
ethereum:masterfrom
lightclient:frames

Conversation

@lightclient

Copy link
Copy Markdown
Member

Still WIP.

Mostly vibe coded to update my old / stale implementation. So far seems to be passing the STEEL test fixtures and able to finalize a local devnet with ethrex and nethermind :)

Squash of the original three-commit series, ported onto the current
master: frame transactions with a derived gas limit (via the new
FrameTxIntrinsicGas alongside the reshaped IntrinsicGas), per-frame
gas budgets atop the runtime state-gas model, the APPROVE-driven
payment flow, frame receipts, and the frame introspection opcodes.
Empty signer bytes on protocol-validated signature entries resolve to
the transaction sender (validation, SIGPARAM 0x00, default code).
Statically require APPROVE_EXECUTION-flagged frames to resolve to the
sender. The default code selects the signature entry by approval
scope: execution scope reads index 0, payment-only reads index 1.
The old 3 was a leftover slot from the deleted APPROVE 0-4 status
table; 2 is unreserved (EIP-658 defines only 0 and 1). Matches the
spec change in EIP-8141.
Empty ARBITRARY entries previously cost nothing (raw-field calldata
pricing leaves the RLP wrapper uncharged), so entries were free to
include. Matches the spec change in EIP-8141.
Per the EIP-8141 charged_gas formula, intrinsic, per-frame, and
signature-verification gas sit outside the floor max and are always
charged. Computing the floor as intrinsic + floored tokens silently
absorbed the per-frame and signature costs whenever the floor
dominated.
P256 verification itself accepts high-s values, so without the bounds
each signature has two valid encodings. Matches the canonicality rule
added to EIP-8141.
A frame clearing state created by an earlier frame earns a state-gas
credit the frame itself never charged, which previously underflowed
the per-frame gas accounting. Track the credit per frame, discard it
when a batch rollback discards the frame, and settle the survivors
against the transaction budget after the frame loop — mirroring how
ordinary transactions net create-then-clear within a single frame.
Widen the fork windows of engine_forkchoiceUpdatedV4,
engine_newPayloadV5 and engine_getPayloadV6 to the Bogota fork, per
the update-the-methods-of-previous-forks rule: frames introduce no
request or response shape changes, so no new method versions are
needed.
Approvals granted in a child call previously survived an ancestor's
revert whenever the frame itself succeeded, diverging from the
reference implementation, which journals approvals per call frame.
Pair an approval-context snapshot with every EVM state snapshot and
restore it wherever the state reverts.
The EIP-8141 static checks ran inside RLP and JSON decoding, so a
structurally well-formed but statically invalid frame transaction
failed to decode at all - surfacing as a hard t8n input error instead
of a rejected transaction, unlike the reference implementation. Keep
decoding structural (nil-field guards only) and run the static checks
in TransactionToMessage via FrameTx.ValidateStatic.
EIP-8141 now requires clients to install the canonical expiry verifier
runtime code at EXPIRY_VERIFIER at activation. Deploy it (nonce 1 plus
the 26-byte runtime) in the pre-execution phase of the first Bogota
block; PreExecution learns the parent timestamp to detect the
transition. Networks activating Bogota at genesis must carry the code
in the genesis allocation instead.
Upstream EELS (forks/amsterdam fork.py) installs only the runtime code at
the Bogota transition, preserving the account's nonce and balance. Drop
the nonce-1 write and pass the active rules to Finalise.
Frame transactions may carry blob versioned hashes. Record their blob gas
and blob base fee in the receipt, and have t8n count their blob gas
against the block's allowance and require the excess blob gas
environment for them.
State test fixtures apply the transactions to the pre-state without any
block-level system operations: no beacon root or history storage
system calls and no post-execution requests. Add --state.test so t8n
can mirror the reference state-test runner instead of the block
transition.
The execution-spec Bogota fork schedules blobs through BPO2, as its
Amsterdam parent does. Activating BPO3 and BPO4 at genesis changed the
blob target and maximum, breaking excess blob gas against the fixtures.
EIP-8141 prices the transaction base at TX_BASE_COST, which EIP-2780
lowers to 12000.
EIP-8141 splits the ARBITRARY signature byte copy out of SIGPARAM into
its own instruction, SIGDATACOPY (0xb5), with CALLDATACOPY stack layout
and gas. SIGPARAM keeps the metadata selectors 0x00-0x03 at GasQuickStep,
and its 0x03 signature length is now ARBITRARY-only: the raw bytes of
protocol-validated schemes, including their length, are not
introspectable.
EIP-8141 restricts the atomic batch flag to DEFAULT and SENDER frames: a
batch can neither contain nor be terminated by a VERIFY frame, and no
frame of a batch, its terminating frame included, may carry approval
scope.
…ctions

Mirror the EIP-8141 rework that gives every frame independent execution
and state gas budgets and settles the transaction from its frame
receipts.

Wire format: frames carry gas_limits = [execution, state] and the fee
fields regroup into a nested fees list; the signature hash preimage
follows. Frame receipts encode gas_used = [execution, state]. The JSON
codecs accept non-canonical hex as emitted by the fixtures and reject
frame values beyond 256 bits.

Gas anchors: the intrinsic cost adds TX_VALUE_COST for every
value-bearing frame with an explicit non-sender target and drops the
recipient component; the calldata floor is anchored on the mandatory
costs. standard_gas_limit = intrinsic + sum of both dimensions and
max_gas = max(standard, floor + total state gas) price the escrow. The
EIP-7825 cap and the floor bind the execution dimension only; the block
pool is reserved per dimension.

Execution: each frame runs against fresh pools holding its declared
budgets. State charges draw from the frame's state pool alone and never
spill into execution gas; the pool rides along child calls. Frame entry
charges the resolved target's access from the execution budget before
warming it, fails the frame on an uncovered value transfer, charges
account revival from the state pool, and dispatches precompiles in every
mode ahead of the default VERIFY code. APPROVE charges sender account
creation from the executing frame's state pool and checks execution
approval before the payer balance. A halting VERIFY frame invalidates
the transaction like a reverting one.

Settlement: the frame context journals its live receipts with call
snapshots; failed frames report zero state gas and an unrolled batch
zeroes the state gas of its frames' receipts. Gas used derives from the
receipts: the standard limit less unused gas, the EIP-3529 refund capped
at a fifth, the floor clamped on the execution dimension, and the block
charged the pre-refund execution gas per EIP-7778. The payer receives
the escrow beyond the actual fee, with blob fees priced at the blob base
fee and the escrow checked for 256-bit overflow.
… charge

EIP-8141 indexes every outstanding storage creation charge of a frame
transaction by the frame that paid it. Clearing the slot later refills
that owner: the executing frame's state pool when it is the owner, the
owner's live receipt otherwise, so the amount returns to the payer at
settlement without granting the executing frame budget it never
declared. The ownership map is journaled with the frame context, so a
failing call or an unrolled batch discards its edits together with the
state changes. Outside frame transactions the refill still recredits the
EIP-8037 reservoir.
Add the EIP-8141 selectors reading the second gas dimension: TXPARAM
0x0c returns the executing frame's remaining state gas, FRAMEPARAM 0x09
a frame's declared state budget, and FRAMEPARAM 0x0a and 0x0b the
execution and state gas used by a completed frame from its live receipt,
halting for the current or a future frame like the status selector.
Execution-spec state tests describe EIP-8141 frame transactions with
explicit chainId, frames and signatures fields. Parse them, build the
frame transaction and derive the message through the regular conversion
so the static constraints and derived gas anchors apply. The numeric
fields are read with arbitrary precision so fixtures probing the field
bounds load and fail where the value is used.
The Bogota transition installed the EIP-8141 expiry verifier code after
the EIP-2935 system call had finalised, which detaches the state's
access list; without a fresh Prepare the code change was never recorded.
Blocks built by geth therefore carried a block access list missing the
install, and every peer that records it (Nethermind, ethrex) rejected
them on the access list hash, while geth rejected its own block on the
BAL-driven import: the state derived from the access list lacked the
code, so the root did not match the builder's.

Run the install at block access index 0 like the other pre-execution
system operations, mirror it in the chain maker, and cover the
transition with a build-then-import test.
Frame transactions were rejected at submission with "transaction type
not supported", so a geth node could validate frame-transaction blocks
built by others but never build one itself. Accept type 6 in the legacy
pool once Bogota is active; the shared validation already prices the
frame intrinsic gas and calldata floor.
Match the layout of the other transaction files: the FrameTx definition
and its TxData methods come first, the frame, gas limit, signature and
fee types it is built from follow, and the gas and signature helpers
close the file. Also reattach the ValidateFrameTxSignatures doc comment,
which had drifted onto ResolvedSigner.
@lightclient lightclient changed the title all: implement eip-8141 frames tx core: implement eip-8141 frames tx Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant