core: implement eip-8141 frames tx - #35666
Draft
lightclient wants to merge 27 commits into
Draft
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 :)