Skip to content

Forward-integration: EIP-7805 (FOCIL) into the EIP-8141 frame-tx base + EIP-8369 overlap - #12772

Draft
Marchhill wants to merge 262 commits into
eip8141-frame-txs-devnet7from
frames-focil
Draft

Forward-integration: EIP-7805 (FOCIL) into the EIP-8141 frame-tx base + EIP-8369 overlap#12772
Marchhill wants to merge 262 commits into
eip8141-frame-txs-devnet7from
frames-focil

Conversation

@Marchhill

@Marchhill Marchhill commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Changes

Forward-integration of EIP-7805 (FOCIL, #8003) into the EIP-8141 frame-transactions base (eip8141-frame-txs-devnet7), prepared ahead of FOCIL's merge to master so that the conflict-resolution and feature-overlap work is done and reviewable now. Base: eip8141-frame-txs-devnet7. Head: frames-focil.

The diff is FOCIL merged into the frames base + the commits below. The FOCIL commits themselves are reviewed in #8003 and are not part of this review surface — review the four commits on top of the merge:

  1. fork: reconcile the Bogota fork collision — the fork/version model (see below).
  2. feat(focil): EIP-8369 Profile classifier (Nethermind.Core/Eip8369.cs) — the overlap classification.
  3. feat(focil): EIP-8369 VERIFY-budget fill in the IL includer (InclusionListBuilder).
  4. feat(focil): frame-aware IL validator (InclusionListValidator).

Fork / version model

Both branches introduced an incompatible Bogota fork (frames: Bogota : Osaka enabling EIP-8141; FOCIL: Bogota : Amsterdam enabling EIP-7805). Reconciled to a single composed fork:

  • Bogota : Amsterdam enables both EIP-8141 frame transactions and EIP-7805 FOCIL inclusion lists (newPayloadV6 / getPayloadV6 / forkchoiceUpdatedV5).
  • Engine ladder stays monotonic — Amsterdam → newPayload V5, Bogota → V6 — each fork mapped to exactly one newPayload version.
  • A frames-only schedule (Bogota not activated) runs frame txs at Amsterdam / V5 via the independent eip8141TransitionTimestamp chainspec parameter; a Bogota schedule runs V6 + inclusion lists carrying frame txs transparently. Both are covered by tests.
  • Chainspec scheduling. Bogota is the single scheduling point that turns on both EIP-8141 and EIP-7805 in the chainspec path — driven entirely by Forks/26_Bogota.cs through the HardforkLabels source generator, which fans bogota (Parity chainspec) / bogotaTime (geth genesis) out to both eip8141TransitionTimestamp and eip7805TransitionTimestamp. No bespoke bogotaTimestamp parameter was added — that would duplicate and conflict with the generator's existing fan-out, and no other fork (Osaka, Amsterdam) uses a dedicated param. The independent per-EIP params are retained, so existing frames devnets (which activate via eip8141TransitionTimestamp) are unaffected, and a FOCIL-only schedule (eip7805TransitionTimestamp) stays possible. Regression tests in ChainSpecBasedSpecProviderTests assert all three: bogota/bogotaTime → both on at/after the boundary and off before; eip8141TransitionTimestamp alone → frames on / FOCIL off; eip7805TransitionTimestamp alone → FOCIL on / frames off.

Deviation to confirm. The task specified enabling EIP-8141 on the Amsterdam fork class. Doing so installs the frame-tx expiry-verifier predeploy into Amsterdam's genesis, shifting Amsterdam's state root away from both parent branches and breaking all 32 newPayloadV5 EIP-7928 BAL consensus test vectors. To keep Amsterdam byte-identical to its parents, EIP-8141 is enabled on the Bogota fork class instead (and, for standalone frames devnets, via the eip8141TransitionTimestamp param). This still keeps IsEip8141Enabled true through Bogota and frame txs running at Amsterdam/V5 in a frames-only devnet. FOCIL's V7 Bogota tests pin no genesis roots, so Bogota carrying the predeploy is clean. Flagging for confirmation of fork placement.

EIP-8369 overlap (informational; ethereum/EIPs#12110)

  • Profile classifier (Eip8369.Classify): Profile 1 = regular non-frame, non-blob (legacy/2930/1559/7702); Profile 2 = frame tx with empty blob_versioned_hashes, one of the four recognized validation-prefix shapes (optional expiry frame ignored for shape matching, no ATOMIC_BATCH_FLAG in the prefix), no VERIFY-mode frame after the prefix, VERIFY cost ≤ MAX_VERIFY_GAS_PER_TX; Outside = blob-carrying / wrong-shape / over-budget. Reuses EIP-8141's frame-shape predicates and verify-gas machinery.
  • Constants MAX_VERIFY_GAS_PER_IL / MAX_VERIFY_GAS_PER_TX = 2²⁰ (distinct from EIP-8141's public-mempool MAX_VERIFY_GAS = 100_000).
  • Includer (InclusionListBuilder): Profile-1 txs pass through freely; Profile-2 txs are metered against MAX_VERIFY_GAS_PER_IL (uncomputable / over-MAX_VERIFY_GAS_PER_TX / over-remaining-budget → skipped, consuming nothing); frame txs outside enforcement excluded.
  • Validator (InclusionListValidator): Profile-1 keeps FOCIL's end-of-payload omission check; Profile-2 gated to the builder-claimed index (default = end of payload).

Marked deferrals

  • AA_VOPS_SLOT_COUNT value — unpinned in the informational EIP (candidate 2–4); pending the Standards-Track EIP-7805 extension + benchmarks. Marked in Eip8369Constants.
  • Full Profile-2 AA-VOPS state-surface enforcement (EIP-8250 keyed nonces, EIP-8272 recent roots — not yet implemented in Nethermind) — the bounded validation replay is deferred; marked in InclusionListValidator.
  • Attester-replay / consensus enforcement of Profile-2 omission — belongs in the future Standards-Track extension; marked in InclusionListValidator.

Note

The frames-devnet genesis generator (external) currently emits bogotaTime to schedule frame txs; under this model a frames-only devnet should emit eip8141TransitionTimestamp (or bogotaTime for the combined frames+FOCIL fork). No source change required here.

Types of changes

  • New feature (a non-breaking change that adds functionality)
  • Other: forward-integration / conflict-resolution prep for two in-flight feature branches

Testing

Requires testing

  • Yes

If yes, did you write tests?

  • Yes

Notes on testing

New: Eip8369Tests (classifier — each shape, blob frame → Outside, over-budget → Outside, wrong shape → Outside, verify-cost); InclusionListBuilderTests EIP-8369 cases (budget exhaustion, over-budget/uncomputable skip, Profile-1 not metered); InclusionListValidatorTests frame-aware cases; updated ForkTests / GethGenesisLoaderTests for the reconciled fork model.

Green: full solution build 0/0; Nethermind.Specs.Test, Nethermind.Core.Test, Nethermind.Consensus.Test, Nethermind.Evm.Test (frame suites), Nethermind.Merge.Plugin.Test (FOCIL EngineModuleTests engine-version + BAL), Nethermind.Blockchain.Test. Code-style build (-p:EnforceCodeStyleInBuild=true) clean on touched projects; dotnet format whitespace clean.

Documentation

Requires documentation update

  • No

Requires explanation in Release Notes

  • No

…focil

# Conflicts:
#	src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/GethGenesisConfigJson.cs
@core-repository-dispatch-app core-repository-dispatch-app Bot added eip new feature taiko related to the taiko alethia rollup labels Aug 11, 2026
@claude

claude Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 6m 19s —— View job


Review: EIP-7805 (FOCIL) → EIP-8141 frame-tx forward-integration

  • Gather context (diff vs origin/eip8141-frame-txs-devnet7, the 4 review-surface commits)
  • Review fork: Bogota reconciliation
  • Review Eip8369 classifier + constants
  • Review InclusionListBuilder VERIFY-budget fill
  • Review InclusionListValidator frame-awareness
  • Review tests
  • Post findings

Reviewed the four commits on top of the merge (f688e14, 9d9d190, ec8d575, 676490e), as scoped in the description — FOCIL's own commits were not re-reviewed.

Overall this is careful work. The classifier faithfully reuses EIP-8141's existing shape/gas predicates instead of re-deriving them, the budget accounting has no underflow or double-charge path (cost is only deducted after admission, and the cost > verifyBudget guard makes the verifyBudget -= cost safe), and the fork reconciliation is well argued. The flagged deviation — enabling EIP-8141 on Bogota rather than the Amsterdam fork class — is the right call: installing the expiry-verifier predeploy into Amsterdam would move its state root away from both parent branches and break the 32 newPayloadV5 BAL vectors, for no benefit that the independent eip8141TransitionTimestamp param doesn't already provide. PredeployInstaller installs idempotently at the first block after activation, so the timestamp-gated frames-only path works without a genesis change. Engine ladder stays monotonic.

Findings below. Nothing Critical or High; four Medium items, mostly around a producer/validator asymmetry and unwired API that the comments claim is wired.


Medium

1. IL under-fill: EIP-8369 exclusion is applied after reservoir samplingInclusionListBuilder.cs:30-35

The reservoir caps candidates at 256 and filters only TxType.Blob; Outside and over-budget entries are dropped later, after they've already consumed a slot. On a frames-heavy mempool — exactly this branch's target — the IL can land well under the 8192-byte cap while eligible Profile-1 txs sit unsampled. The pre-existing blob filter lives in the sampling loop for precisely this reason; the classifier should join it. This is a censorship-resistance regression in the censorship-resistance mechanism.

2. Producer/validator asymmetry on Profile 2InclusionListBuilder.cs:70-90 vs InclusionListValidator.cs:59

The includer admits Profile-2 frame txs into the IL (metered against MAX_VERIFY_GAS_PER_IL), but the validator never enforces them — Classify(...) != FocilProfile.One → continue. So Nethermind attests to ILs containing entries no Nethermind validator will ever check.

Within an all-Nethermind devnet that is merely inert. Against a peer implementing plain EIP-7805 with no EIP-8369 notion, it isn't: that peer applies its ordinary appendability check to the frame tx and can reject a block Nethermind accepts. The deferral is documented on the validator side; the fact that the builder keeps emitting entries into that gap is not. Worth an explicit decision — either hold Profile-2 txs out of produced ILs until enforcement lands, or state the interop assumption (every peer implements 8369) in the builder.

3. Validator comment contradicts the code; the API it names is deadInclusionListValidator.cs:54-59

The comment says Profile-2 omission "is checked by bounded validation replay at the builder-claimed index (Eip8369.DefaultClaimedInclusionIndex…)". It is not checked at all, and DefaultClaimedInclusionIndex has zero production call sites (only Eip8369Tests); Eip8369Constants.AaVopsSlotCount has none anywhere. coding-style.md: "a comment that contradicts the code is worse than no comment"; AGENTS.md discourages unconsumed public surface. State the actual behaviour and drop the unwired helpers until the enforcement lands. Fix this →

4. bogotaTime now silently activates FOCIL for existing frames-devnet genesis files26_Bogota.cs:12-16

bogotaTime fans out to both Eip8141TransitionTimestamp and Eip7805TransitionTimestamp, so an existing frames-devnet genesis (which emits bogotaTime for frame txs only) also flips to newPayloadV6/forkchoiceUpdatedV5 and starts requiring ILs from a CL that may only speak V5 — a stall at the fork boundary rather than a loud failure. Acknowledged in the description, but it depends on an out-of-band generator change landing first; worth sequencing explicitly or failing fast.

Low

5. Two of three documented builder skip reasons are unreachableInclusionListBuilder.cs:71-76. Classify already rejects > MAX_VERIFY_GAS_PER_TX (Eip8369.cs:53), and ValidationWorkGas saturates rather than signalling "uncomputable" — only cost > verifyBudget is live. Excludes_over_budget_frame_tx_but_keeps_profile_one reads as covering this branch but exercises the classifier instead.

6. Frame-shape walk runs twice per classificationEip8369.cs:44-55. TryGetValidationPrefixLength and ValidationWorkGas each call RecognizedPrefixLength. Up to 64 frames per tx, over up to 4096 aggregate IL entries on the newPayloadV6 path. An internal overload taking the resolved prefix length fixes it with no new public surface.

7. Two tautological tests + a coverage gapEip8369Tests.cs:84-96. Profile2VerifyCost_MatchesValidationWorkGas asserts an alias equals its target; DefaultClaimedInclusionIndex_IsEndOfPayload asserts X == X. Neither can fail. No case covers FrameSignatures pushing a tx over MAX_VERIFY_GAS_PER_TX — the one input to the budget beyond frame gas limits. (Classifier shape coverage is otherwise good: all four prefixes, expiry variant, blob frame, wrong shape, atomic-batch flag, at-budget boundary.)

8. Lock file lost its trailing newlinepackages.lock.json. Unrelated churn in the fork commit.

9. TryGetValidationPrefixLength XML doc mixes return and out semanticsFrameTxValidation.cs:271-277. The <summary> opens "The number of leading frames forming a recognized … prefix … or false when …", describing the out param as if it were the return value. Split into <returns> and the existing <param>.

Things I checked that are fine

  • verifyBudget -= cost cannot underflow — guarded by cost > verifyBudget above it, and cost is 0 for Profile 1.
  • Budget is charged only after the byte-cap check admits the tx, so a size-rejected Profile-2 tx consumes nothing. Correct per the fill semantics.
  • Classifier's Outside for blob-carrying frame txs matches the validator's pre-existing SupportsBlobs rejection — no behaviour change for blobs.
  • Null SenderAddress makes a frame tx classify Outside (via IsSelfTargetedVerify), which in the validator is indistinguishable from Profile 2 — both continue — so sender-recovery ordering can't cause divergence there.
  • PredeployInstaller is activation-gated, not genesis-gated, so the eip8141TransitionTimestamp frames-only path installs the expiry verifier correctly mid-chain.
  • Fork.GetLatest() is BPO2, so adding EIP-8141 to Bogota doesn't leak into the latest-mainnet-fork assertion.

I did not run the build or test suites in this environment; the green results reported in the description were not independently verified.


· branch frames-focil

Comment thread src/Nethermind/Nethermind.Consensus/Validators/InclusionListValidator.cs Outdated
Comment thread src/Nethermind/Nethermind.Merge.Plugin/Handlers/InclusionListBuilder.cs Outdated
Comment thread src/Nethermind/Nethermind.Core/Eip8369.cs
Comment thread src/Nethermind/Nethermind.Core.Test/Eip8369Tests.cs
Comment thread src/Nethermind/Nethermind.Runner/packages.lock.json Outdated
Comment thread src/Nethermind/Nethermind.Specs/Forks/26_Bogota.cs
Marchhill and others added 5 commits August 11, 2026 13:49
- InclusionListValidator: the block-gas-fit guard used `block.GasLimit - tx.GasLimit`,
  which underflows for a ulong `tx.GasLimit` above the block gas limit — the guard was
  bypassed, so a tx that can never fit was marked appendable and an honest, fully-valid
  block was falsely rejected as INCLUSION_LIST_UNSATISFIED (weaponizable by an IL member
  + funded sender). Compare against remaining gas instead (`tx.GasLimit > GasLimit - GasUsed`,
  underflow-free since GasUsed <= GasLimit and the block-full case already returned).
  Adds a regression test with GasLimit above the block gas limit.
- InclusionListBuilder: dispose the accumulated ArrayPool-backed buffers if EncodePooled
  throws mid-loop instead of leaking them.
- Remove the unused PayloadStatusV1.InclusionListUnsatisfied static.
- RecoverSignatures: keep the hot skipErrors==false block-tx path on the inlinable Recover
  call; only FOCIL IL recovery uses the try/catch wrapper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Amsterdam)

The frames base and FOCIL each introduced an incompatible `Bogota` fork: frames
had `Bogota : Osaka` enabling EIP-8141 frame transactions, FOCIL had
`Bogota : Amsterdam` enabling EIP-7805 inclusion lists. Reconcile to a single
composed model:

- Bogota inherits Amsterdam and enables BOTH EIP-8141 frame transactions and
  EIP-7805 FOCIL inclusion lists (newPayload V6, forkchoiceUpdated V5).
- EIP-8141 is deliberately kept off the Amsterdam fork class so Amsterdam's
  genesis stays byte-identical to both parent branches — enabling it there
  installs the frame-tx expiry-verifier predeploy at genesis and shifts every
  Amsterdam newPayloadV5 EIP-7928 BAL consensus test vector. A frames-only
  devnet instead activates frame txs via the independent
  eip8141TransitionTimestamp chainspec parameter, staying at Amsterdam / V5.
- Remove the frames Osaka-based `Bogota.cs` (superseded).

The engine version ladder stays monotonic (Amsterdam -> newPayload V5, Bogota ->
V6) with each fork mapped to exactly one newPayload version. ChainSpec-driven
networks remain unaffected: EIP-8141 and EIP-7805 are gated by their own
transition-timestamp parameters.
…verlap

Classifies a transaction into its EIP-8369 FOCIL enforcement profile:
- Profile 1: regular non-frame, non-blob txs (legacy/2930/1559/7702).
- Profile 2: EIP-8141 frame txs with empty blob_versioned_hashes, one of the four
  recognized validation-prefix shapes (optional expiry frame ignored for shape
  matching, no ATOMIC_BATCH_FLAG in the prefix), no VERIFY-mode frame after the
  prefix, and VERIFY cost within MAX_VERIFY_GAS_PER_TX.
- Outside: blob-carrying (incl. blob frame txs), wrong-shape, over-budget.

Reuses EIP-8141's frame-shape predicates (exposed via
FrameTxValidation.TryGetValidationPrefixLength) and its VERIFY-gas machinery
(ValidationWorkGas). Adds Eip8369Constants (MAX_VERIFY_GAS_PER_IL /
MAX_VERIFY_GAS_PER_TX = 2^20) and a TBD AA_VOPS_SLOT_COUNT, plus a
DefaultClaimedInclusionIndex helper (default = end of payload).
…n lists

The includer now meters Profile-2 frame transactions against the per-IL VERIFY
budget (MAX_VERIFY_GAS_PER_IL): a Profile-2 tx whose VERIFY cost is uncomputable,
exceeds MAX_VERIFY_GAS_PER_TX, or does not fit the remaining budget is skipped
consuming nothing; otherwise it is admitted and the cost deducted. Profile-1 txs
pass through freely and never consume budget; transactions outside enforcement
(blob-carrying / wrong-shape / over-budget frame txs) are excluded from the IL.
The IL omission check now gates on the EIP-8369 profile: Profile-1 txs keep
FOCIL's end-of-payload omission check; Profile-2 frame txs are checked at the
builder-claimed index (Eip8369.DefaultClaimedInclusionIndex, default = end of
payload). The full bounded validation replay against the AA-VOPS state surface
(EIP-8250 keyed nonces, EIP-8272 recent roots) is a marked DEFERRAL, so a
Profile-2 entry is not treated as a violation here; transactions outside
enforcement are never enforced.
Assert that the Bogota fork label (and geth-genesis bogotaTime) enables both
EIP-8141 frames and EIP-7805 FOCIL together, while the independent per-EIP
transition timestamps still gate each feature in isolation so existing
frames-only devnets stay unaffected.
EIP-8369 Classify resolved the recognized prefix, then ValidationWorkGas
re-derived it; thread the length through an internal overload so the
frame-shape walk runs once on the includer/validator hot paths.
Remove the never-called DefaultClaimedInclusionIndex and the unconsumed
AaVopsSlotCount constant. Correct the validator comment: Profile-2
enforcement (bounded claimed-index replay) is deferred to a future
EIP-7805 extension, so Profile-2 and Outside entries never fail here.
Replace the two tautological Eip8369 tests with concrete-value checks
that also cover signature gas contributing to the VERIFY budget.
EIP-8369 Outside txs (blobs, wrong-shape/over-budget frame txs) were
filtered only at encode time, after they had already consumed reservoir
slots, under-filling the inclusion list. Classify during sampling so
they never take a slot, and simplify the encode-time budget check to the
one reachable guard (remaining per-IL VERIFY budget).
@Marchhill

Copy link
Copy Markdown
Contributor Author

Merged eip8141-frame-txs-devnet7 (48ae024) forward. No conflicts.

Nethermind.Evm.Test 5188/5188, Nethermind.TxPool.Test 768/768, Nethermind.Blockchain.Test 1679/1679; lint gate clean (128 projects).

EIP-8141 blockchain fixtures from execution-specs#3047 go 73/135 → 59/135. Bisected to #12732, which changed the predeploy install condition from "account code differs from canonical" to "code differs or nonce < predeploy nonce":

bool codeSatisfied = code.IsEmpty || readState.GetCode(...).AsSpan().SequenceEqual(code.Span);
if (codeSatisfied && nonce >= predeploy.Nonce) continue;

The EIP-8141 expiry verifier at 0x…8141 is in the fixtures' pre-state with the canonical code and nonce = 0x00. It used to be skipped; now the nonce test fails every block, so SetNonce(…, 1) is written, the state root diverges from the fixture, the block is rejected and the transaction never executes. That is the shape of all 76 failures — post-state shows the last block missing and the frame tx at nonce 0. 62 of the 76 were already failing here on the devnet-7↔devnet-8 gas skew, so the net loss on this branch is 14.

Isolated on frames-devnet8-integration (135/135 before any of this), merging one devnet-7 commit at a time:

tree fixtures
baseline 135/135
+ frames-focil before this merge 135/135
+ #12656 (8272 validate/charge) 135/135
+ #12659 (8272 expose) 135/135
+ #12732 (8272 predeploy) 59/135

The EIP-8272 predeploy itself is gated on IsEip8272Enabled, which Bogota does not set, so the only reachable effect here is the changed rule applied to the EIP-8141 predeploy. Worth a fix on devnet-7 rather than a workaround here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

eip new feature taiko related to the taiko alethia rollup

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants