Spec-level review of EIP-8105 & EIP-8184: block-validity dilemmas, key-vote algebra, unspecified value flows, and a nonce-reuse hazard

> Submission for the “Encrypt The Mempool!” POIDH bounty (poidh.xyz/mainnet/bounty/9).

> Wallet address (used to add funds to the pool): `0x34015a2EE7f71341883876933AFc1DE977Cf7eC8`

> Name/pseudonym: **vellichorlabs** · Role: independent protocol reviewer

> All quotes are from the specs as they stand on `ethereum/EIPs` master as of 2026-07-20.

## Scope

Key-withholding economics, the trust graph’s centralization pull, FRAME integration, and PTC bribery have already been covered well in this venue and on Magicians (Bonus’s submission here; ashaqeela, doodlemax and 0xwans on thread 28804; dataalways on the Hegota thread). I deliberately do not re-tread that ground. This review instead goes through both specs line by line and reports what I believe are new findings: two block-validity ambiguities that adversaries can trigger cheaply, an under-determined vote-counting rule that hands the builder exactly the per-commitment free option the design works so hard to price, a cryptographic footgun in LUCID’s nonce derivation, several unspecified value flows that decide whether builders profit from non-reveal, and a tie-breaking rule that quietly breaks the authors’ own proposed mechanism for benign backrun monetization. I close with concrete fixes for each and with what I think both EIPs get importantly right.

## Part 1 — EIP-8184 (LUCID)

### 1.1 IL-forced standalone STs create a block-validity dilemma (cheap griefing attack)

LUCID’s block processing charges every ST ticket with hard assertions (“Block processing”):

```python

assert ticket.signer.balance >= ticket_fee

assert ticket.signer.nonce == ticket.nonce

```

For **bundles**, the `executable` bitfield exists precisely so that a ticket that can no longer pay, or whose nonce was consumed, is skipped rather than invalidating the block. But for **standalone STs**, “`executable` is empty” (“Extended payload bid”) — there is no skip mechanism. A standalone ST whose ticket fails either assert makes the payload invalid.

Now combine this with the IL rules (“Inclusion lists with STs and bundles”). The exhaustive list of conditions under which a timely IL entry *may* be excluded is: (a) aggregate gas above `tob_gas_limit_per_il`, and (b) impossibility of insertion under the ordering rule without violating `max_preceding_commitments`. **“Ticket has become unpayable / nonce-invalid” is not on the list.** FOCIL itself handles this with a post-execution exemption for invalid transactions; LUCID’s ST-specific eviction conditions do not replicate it.

The attack: broadcast a standalone ST with an attractive `tob_fee` so includers pick it up into ILs, then send an ordinary type-2 transaction from the same account and nonce directly to builders. Whatever the builder does, the block is contested: include the ST-commitment → the ticket assert fails → invalid payload; exclude it → attesters following the IL-audit rule “vote against the block.” The same dilemma arises *organically* whenever an honest user cancels a pending ST the only way currently possible — by burning the nonce with a regular transaction. Cost of the attack: one 21k-gas self-transfer; cost to the network: a contested or missed slot.

**Fix:** add an eviction condition mirroring FOCIL — a standalone ST-commitment may be excluded if, at its position under the ordering rule, the ticket fails the balance or nonce check — and make the charging step for standalone tickets skip-not-assert (i.e., give standalone STs a one-bit `executable` too). Also state explicitly against *which* state `executable` is evaluated at bid time (post-state of the parent block including the decrypted STs scheduled for the current payload — see §1.6).

### 1.2 The key-timeliness decision rules are not mutually exclusive: one late vote hands the builder a sanctioned per-key option

“Key timeliness” defines when the next builder may treat a key as missing:

* `2 * timely_1 <= (timely_1 + timely_0 + late_0 + equivocated_votes)` (M)

and when it may treat a key as observed:

* `2 * (timely_1 + late_1 + equivocated_votes) > (timely_1 + timely_0 + late_1 + equivocated_votes)` (O)

Simplify: (M) ⟺ `timely_1 <= timely_0 + late_0 + E`; (O) ⟺ `timely_1 + late_1 + E > timely_0`. These can hold **simultaneously**. Example with 9 relevant PTC votes: `timely_1 = 4, timely_0 = 4, late_1 = 1`: (M) 4 ≤ 4 holds, (O) 5 > 4 holds. The builder may then *validly* choose either treatment for that key, and the next-slot PTC has no ground to vote against either choice (“If this test fails for any key…” — in the overlap, no test fails).

This matters because a single PTC member can *manufacture* the overlap at zero cost: simply hold your vote until just after `lucid_key_vote_deadline`. Late voting carries no penalty, and equivocation on `LucidKeyTimelinessVote` is flagged but explicitly unslashed (votes equivocated are excluded from the bitfields and widen both conditions symmetrically). Around any near-tie — which an adversary can steer toward by timing its own key release near the deadline — the builder receives a **protocol-sanctioned free choice over whether a specific decrypted transaction executes**. That is precisely the per-commitment reveal optionality the `tob_fee` burn is designed to price, except here it is free, deniable, and per-key rather than the builder’s single coarse “withhold the whole payload” bit acknowledged in the Rationale (“The builder also has one bit of reveal optionality”).

**Fixes:** (i) make the two conditions complementary — define one threshold predicate and its negation, e.g., “treat as observed iff `2 * (timely_1 + late_1 + E) > total_votes`, else missing,” so exactly one treatment is valid in every view; (ii) count a member’s *absence* of a timely vote as `late_0`-equivalent rather than leaving it out of both sides; (iii) consider making late key-timeliness votes simply inadmissible (the deadline exists; late votes only add ambiguity). If discretion in gray zones is genuinely wanted for liveness, it should be priced — e.g., builder forfeits a fraction of the affected commitment’s `tob_fee` when exercising the gray-zone choice.

### 1.3 Where does the money go? Escrow, burn-vs-coinbase, and whose gas limit — all unspecified, and the answers decide builder incentives

The charging pseudocode debits `ticket_fee = base_fee + key_publication_fee + priority_fee + tob_fee` from the signer, and the only credits in the spec are the success-path refund and the `key_publication_fee_recipient` credit “upon successful decryption.” Everything else is silent:

1. **Priority fee destination.** Which coinbase — block *n* (where the ticket is charged) or block *n+1* (where the PT executes)? Never stated.

2. **ToB fee escrow.** `tob_fee` is charged in block *n* and 127/128 refunded in block *n+1*. Held where? An implicit protocol escrow needs an explicit account/state description.

3. **Forfeitures on non-reveal.** The Rationale calls non-reveal “a form of probabilistic MEV burn,” but the Specification never says the forfeited `tob_fee` (or the never-refunded base-fee-on-unused-gas, or the undelivered `key_publication_fee`) is *burned*. This is not pedantry: **if any forfeited amount reaches the coinbase of block *n+1*, the builder earns money whenever keys are treated as missing** — directly rewarding the gray-zone discretion of §1.2 and any builder-publisher collusion to suppress reveals. The spec must state: forfeited `tob_fee` and undelivered `key_publication_fee` are burned; unused-gas base fee on the non-reveal path is burned (as base fee always is).

4. **Whose gas limit and which EIP-1559 controller?** The Rationale says decrypted STs are “allotted at most 1/TOB_GAS_FRACTION_DENOMINATOR of the gas limit” and that unused allocation “does not encroach on the block gas limit” — implying that when used, ToB gas **does** consume block *n+1*'s limit. But then the builder of block *n*, by choosing ST-commitments, is selling block *n+1*'s blockspace — the exact incentive incompatibility EIP-8105 identifies and designs against (“The builder of one block should not be able to sell block space of the next,” 8105 Rationale/Transaction Execution). Either (a) count `gas_obligation` against block *n*'s gas limit and let block *n+1* execute the PTs limit-exempt (8105’s answer), or (b) keep it in *n+1* and add rationale for why the cross-block sale is acceptable given ToB gas is capped at 1/8. Relatedly: is `dual_gas_used` included in the `gas_used` that drives the base-fee update, and of which block? If it is invisible to the controller, up to 12.5% of sustained load never raises the base fee; if visible, say so and where. The authors acknowledge resource pricing is unsettled — these four questions are the ones I’d urge settling first, because three of them are incentive-critical rather than cosmetic.

### 1.4 Validity is not reconstructible from on-chain data: two verification paths that can diverge

`LucidKeyMessage` is explicitly “unsigned” and propagated on P2P; `LucidKeyTimelinessVote`s are P2P messages; neither is specified to be included in any beacon block or payload. Yet block validity depends on them: nodes “verify that `decrypted_transactions` contains the byte-identical transactions they derived via decryption,” and recovery outcomes are “fixed by the corresponding `LucidKeyTimelinessVote`.”

Two consequences:

* **Historical sync.** A node syncing later cannot re-derive decryption (it may never see the `k_dems` or the ciphertext bytes — the `ciphertext_envelope` is “not included on-chain”). It *can* check a PT against the on-chain `reveal_commitment` in the ticket. So live nodes verify via decryption; syncing nodes can only verify via commitment. These are not equivalent: a builder that knows a PT out-of-band (e.g., it is the key publisher) could include a `plaintext_tx` matching `reveal_commitment` while the released `k_dem` fails to decrypt the ciphertext (or no valid key message exists). Live nodes must reject; a commitment-checking syncing node would accept. That is a consensus-split shaped hole.

* **Accountability.** Without the key bitfield landing on-chain (8105 does this via the extended payload attestation, and notes “the protocol records which keys have been present”), LUCID’s stated reliance on reputation/out-of-protocol slashing for publishers has no canonical data to bite on.

**Fix:** include the PTC key-timeliness bitfield (or its aggregate) in the next beacon block, and define block validity for `decrypted_transactions` purely in terms of on-chain-recoverable facts: (attested key-present bit) ∧ (PT matches `reveal_commitment`) ∧ (`keccak256(k_dem) == key_commitment` for a `k_dem` recorded in the payload — 32 bytes per commitment is cheap and makes history self-verifying). Recording `k_dem` on-chain also collapses the live/sync divergence.

### 1.5 The protocol-defined DEM nonce turns a mundane wallet behavior into a broadcast-decryptable keystream reuse

The DEM nonce is `keccak256(chain_id || ticket_from || ticket_nonce)[:12]` (“Ciphertext envelope”), i.e., **fully determined by account and nonce**. The spec defines no replacement/fee-bump policy for STs in the mempool (itself a gap — what is the RBF rule for two STs with the same `(from, nonce)`?). A wallet that re-encrypts a modified plaintext (new slippage, bumped fees) for the same ticket nonce while reusing `k_dem` — the natural lazy implementation, since `key_commitment` can then stay stable — produces two ChaCha20-Poly1305 ciphertexts under the same (key, nonce). When `k_dem` is eventually released for the included one, **any mempool observer who archived the superseded ciphertext recovers keystream and hence the XOR of both plaintexts**, plus Poly1305 forgery capability — leaking the transaction the user thought they had replaced. The Security Considerations mention (key, nonce) reuse generically and delegate to “decryptors and self-decrypting senders,” but the protocol *invites* the collision by deriving the nonce from fields that are constant across replacements.

**Fix:** derive the nonce from `keccak256(chain_id || ticket_from || ticket_nonce || key_commitment)[:12]` (ciphertext-independent, so no circularity, and fresh whenever the key is fresh), and add normative language: a `k_dem` MUST NOT be used for more than one `ciphertext_envelope`. While here: note that `key_commitment = keccak256(k_dem)` publicly links any STs sharing a key — mempool observers can cluster them — so uniqueness also has a metadata-privacy rationale. And specify the ST replacement rule explicitly (e.g., replace-by-fee on `(from, nonce)` with ≥10% bumps on `max_fee` and `max_tob_fee`).

### 1.6 Smaller spec defects and ambiguities (each independently actionable)

* **`requires` is incomplete.** The preamble lists `2718, 7805`, but the spec extends `ExecutionPayloadBid` and the PTC — both EIP-7732 constructs — and normatively depends on EIP-7825, EIP-7843 (`SLOTNUM`), EIP-2935, EIP-4788 and EIP-7976 semantics. At minimum 7732 must be added; a Draft that hard-depends on ePBS should say so in metadata, not only in prose.

* **Processing order of `st_tickets` vs `decrypted_transactions` is unstated.** The `ExecutionPayload` container lists `st_tickets` first, but the timeline implies PTs (charged last slot) execute ToB before this slot’s tickets are charged. Whether tickets are charged before or after PT execution changes the state against which `executable` and the charging asserts are evaluated. Define the order normatively (I’d suggest: PTs first, then ticket charging, then regular transactions).

* **No expiry field.** ST tickets have no valid-until slot. A sealed intent can float indefinitely and execute against a market that has moved; the only cancellation is the nonce-burn that triggers §1.1. An explicit `expiry_slot` bounds staleness, gives mempools an eviction rule (currently unspecified), and — unlike 8105, where a key provider can enforce expiry by withholding policy — is the *only* possible lever for LUCID’s trustless self-decryption path, which has no policy-capable publisher.

* **Reorgs are unaddressed.** 8105 has a Reorgs section; 8184 has none. A key publisher releases `k_dem` once “confident that the beacon block will become canonical” — an advisory standard with no quantification. If the scheduling block reorgs after release, the plaintext is public, the ticket uncharged, and — unlike 8105, whose keys can be invalidated by the block-hash-scoped validation function — a bare symmetric key is irrevocable. The PT itself (`max_fee_per_gas = 0`) can’t be replayed as a regular tx, but its *information* is fully front-runnable when the user retries. Add a security section; recommend a concrete confidence rule (e.g., ≥40% of committee weight observed) and wallet guidance that a reorged ST must be treated as burned intent.

* **Execution-environment surprises for PTs.** PTs execute with `max_fee_per_gas = 0`, so `GASPRICE` returns 0 — contracts that branch on `tx.gasprice` (relayer reimbursement, gas-refund accounting) misbehave silently ToB. And with EIP-2935/EIP-4788 writes deferred until “after the last ST has been executed,” a PT querying the parent’s beacon root gets a revert from the 4788 contract (it reverts on unknown roots) and 2935 returns the grandparent era. Deterministic, but it should be documented as an error case; today it is discoverable only by close reading of “Block processing.”

* **Typo:** “or the same reason” → “For the same reason” (“Extended payload bid”).

### 1.7 The ordering rule’s tie-break is grindable — which breaks the authors’ own recipe for benign backrun monetization

The bounty asks about “allowing monetization of benign backrunning.” On the Hegota thread, the authors suggested execution coupling (e.g., MEV-Share-style user-trade + backrun + refund legs) “by setting identical or sequential (in wei) ToB fees.” The spec’s ordering rule undermines this: commitments are ordered by descending `commitment_tob_fee`, “tie-breaking by descending `gas_obligation` and ascending `commitment_root`.” `commitment_root` is `ticket_hash = keccak256(ticket_bytes)` — freely grindable *without* touching the sort keys, since any field outside `tob_fee`/`gas_obligation` (priority-fee dust, ciphertext bytes, key material) reshuffles the hash while leaving `commitment_tob_fee` and `gas_obligation` fixed. An adversary who wants to wedge between leg A (`fee = f`) and leg B (`fee = f − 1 wei`) cannot bid strictly between them, but can bid *exactly* `f`, match `gas_obligation`, and grind its ticket hash to sort immediately after A — landing between A and B. So wei-adjacency provides **no atomic adjacency at all**, and the only sound coupling primitive is the bundle, which requires a single `key_publisher` for all legs — i.e., the searcher must hand its backrun to the user’s publisher, reintroducing the trusted-intermediary concentration LUCID exists to reduce.

**Fix:** add an explicit coupling primitive: an optional `predecessor_root` field in the ticket (or an ordering-rule clause) such that a commitment declaring predecessor `r` is valid only in the position immediately after the commitment with root `r`, with both included or neither. This gives permissionless, multi-party backrun markets ToB adjacency without shared publishers, and turns “monetization of benign backrunning” from a fragile convention into a protocol feature. (A cheaper stopgap: tie-break by *descending* root after descending fee&gas is equally grindable — the grind is symmetric — so a field-based link is the only robust answer.)

## Part 2 — EIP-8105

### 2.1 Decryption/validation functions are mutable, state-dependent EVM code — pin them

The registry stores “a contract with a decryption and a key validation function” (Specification/Key Provider Registry), and block validity depends on “applying the decryption function given by `key_provider_id`” (Block Structure). Two under-specifications:

* **Mutability.** Nothing freezes the code. A provider can register benign logic, accumulate encrypted in-flight transactions, then upgrade a proxy so that pending envelopes decrypt to garbage (griefing: users still pay envelope fees, condition “decryption fails” drops the payload) — or worse, exploit the spec’s own admission that “maliciously chosen ‘decryption’ schemes… allow directly modifying specific parts of the decryption results.” Users’ trust decision is made at *encryption* time; the code that matters runs at *inclusion* time. Require the registry to pin a code hash at registration (registration is invalid for accounts with mutable dispatch), or evaluate the functions against the registration-block code.

* **State-dependence and gas.** If these functions SLOAD mutable storage, decryption results depend on *when/where* they are evaluated — the spec never pins the evaluation state (pre-state of the inclusion block? of the decryption block?), which is a determinism requirement, not a nicety. Mandate `STATICCALL` semantics with storage access restricted to the provider’s registration-time state, and give decryption an explicit gas cap analogous to the PTC validation’s “hardcoded small gas limit per key,” with out-of-gas defined as “decryption fails.”

### 2.2 Trust-graph reachability as a validity condition is a DoS surface, and edges mutate under in-flight transactions

Trust is defined as *path existence* in a directed graph (“A trusts B iff a directed path from A to B exists”), and block validity requires checking it for encrypted-transaction adjacency. Registration is permissionless, so an adversary can register thousand-node chains and force reachability queries into every block validation; nothing bounds graph size or path length. Store an explicit bounded transitive closure (or make trust direct-edge-only with a capped out-degree), updated at edge-mutation time, so validation is O(1) per pair.

Sharper: **trust edges can change while transactions are in flight.** A user encrypts to provider P given P’s current trust set; before inclusion, P adds an edge to Q (or Q∞ chain), and the user’s transaction may now validly be preceded by providers the user never accepted — with everything that implies given the 1-bit (or worse) pre-state influence the Security Considerations describe. Trust-set changes should be timelocked (edges effective after N slots) and the validity check evaluated against the trust graph at a pinned slot, so users can price what they’re consenting to.

### 2.3 No cap on the encrypted-gas share → sustained ~2x execution load

Decrypted transactions’ “consumed gas does not count towards the block gas limit and no fee is paid” (Transaction Execution), and unlike LUCID there is no `TOB_GAS_FRACTION`-style cap on how much of block *n* may be encrypted envelopes. A block full of encrypted transactions obligates block *n+1* to execute up to an extra full block of gas — and block *n+1* can itself be full of envelopes. All of it is *paid for*, so this isn’t free spam, but it converts the block gas limit from a per-slot execution bound into a soft target with sustained worst-case ~2x execution and state-growth throughput. That belongs in Security Considerations with a mitigation — most simply, adopt LUCID’s fractional cap; the convergence of the two teams makes this an easy harmonization.

### 2.4 The exclusion rule is self-referential — define it constructively

“A decrypted transaction must not be included if and only if any of the following… including the decrypted transaction would result in an invalid block” (Block Structure). Inclusion-set validity defined in terms of block validity, which depends on the inclusion set, can admit multiple fixed points (excluding X can change state so Y flips validity, and vice versa) — i.e., two conflicting blocks both satisfying the rule, or none. Replace with a constructive sequential filter: process candidates in envelope order against the running state; include each unless it individually fails (nonce/balance/etc.) at its position. Same intent, single fixed point.

## Part 3 — What both EIPs get right (worth keeping through any merge)

Executing decrypted transactions **against the committed block’s header/context** (8105’s insight, adopted by LUCID with the 7843/2935/4788 adjustments) is the single best idea in this design space — it closes the builder’s post-reveal information-injection channel structurally rather than economically. LUCID’s `tob_fee` construction (refund-on-reveal, burn-on-withhold) is the first serious attempt I’ve seen to *price* reveal optionality instead of assuming it away; §1.2–1.3 above are about protecting that mechanism from leaks around its edges, not replacing it. The exogenous/endogenous/autogenous MEV taxonomy in LUCID’s Rationale is genuinely clarifying and preempts the lazy “this becomes L2-style onchain spam-searching” objection with an argument, not a hope. The sponsor-ST trick for out-of-protocol publisher liability is elegant. And 8105’s scheme-agnostic registry plus the two teams’ public convergence is exactly how competing core EIPs should behave. Both proposals are, in my view, the right direction for the protocol; the issues above are the distance between a compelling Draft and a reviewable spec, and all of them have local fixes.

-–

*Word count: ~2,950. All section names quoted verbatim from the EIP files; line references available on request (reviewed at ethereum/EIPs master, 2026-07-20: eip-8105.md 153 lines, eip-8184.md 580 lines).*

Revision 2 (2026-07-21) — deepening pass

Revision 2 (2026-07-21). Deepening pass after a second line-by-line read of both specs. New findings:

  • §1.8 (new) — Censorship-resistance inversion. LUCID’s headline claim is that it “broadens the practical censorship resistance of inclusion lists.” I show the IL exclusion clause does the opposite for the transactions that need protection most: an attacker (or merely a fee-maximising builder) can validly exclude a FOCIL-timely, low-max_preceding_commitments ST for a cost of roughly $2 per slot.
  • §1.9 (new) — Recovery reordering breaks the max_preceding_commitments guarantee. A payload rejection — which a builder/proposer can induce — silently places an entire prior scheduling block’s decrypted transactions ahead of an ST that consented to zero preceding commitments, contradicting Rationale point 5’s 2^{m-1}-pattern bound.
  • §1.10 (new) — executable/gas_obligation/ordering is a global fixpoint, not a local check. The bit the builder is told to compute per-ticket is actually defined circularly; the spec gives no constructive algorithm, admitting both unsatisfiable configurations and builder-selectable ones.
  • §1.1 (extended) — same-sender nonce-vs-tob_fee conflict. The ordering rule sorts by fee, but charging asserts nonce sequentiality; two STs from one account with different tob_fees cannot both be ToB in one slot, and one extra 21k-gas ticket weaponises this into the block-validity dilemma without a second account.
  • §1.6 (extended) — the max_fee >= base_fee + key_publication_fee assert is not wired into the executable skip path.
  • Quantification added to §1.1, §1.2, and 8105 §2.2/§2.3 (attack costs, EV of the free option, sustained-load bounds).
  • Relationship to topic 897 (Bonus) stated explicitly: this review subsumes the withholding-economics and PTC-granularity angles as inputs and adds the spec-mechanics layer beneath them.

§1.8 — LUCID’s own CR claim is inverted by the IL exclusion clause: a ~$2 flood validly censors exactly the STs the design exists to protect

The Motivation sells LUCID as a censorship-resistance upgrade: it “broadens the practical censorship resistance of inclusion lists” for MEV-sensitive transactions that FOCIL alone cannot protect. The mechanism for that is the extension of the InclusionList container with sealed_transactions / sealed_bundles, so that an includer can force an ST into the block the way FOCIL forces a plaintext transaction. But the force-inclusion is subject to a carve-out that quietly removes the guarantee for the highest-protection STs.

“Block processing” fixes the order: commitments are “ordered by descending commitment_tob_fee, tie-breaking by descending gas_obligation and ascending commitment_root.” And “Inclusion lists with STs and bundles” says a timely IL entry may still be excluded if:

It cannot be inserted in st_commitments at the position determined by the ST-commitment ordering rule (see Block processing)—without breaking its max_preceding_commitments stipulation.

Compose the two. An ST with ToB fee f sorts after every commitment whose fee exceeds f. If the number of higher-fee commitments the builder includes is greater than the ST’s max_preceding_commitments = m, then at its fee-ordered position the ST is preceded by more than m commitments, so it “cannot be inserted … without breaking its max_preceding_commitments,” and is validly excluded — even though it is FOCIL-timely and an includer put it in an IL to guarantee inclusion.

Now note who wants a low m. Rationale point 5 is explicit that max_preceding_commitments is the front-running-protection dial: “If a sender allows at most $m$ preceding commitments, then an adversarial party can choose between at most $2^{m-1}$ distinct patterns.” The Security Considerations name low-m STs as the sensitive class, and “Competition for the ToB” concedes that “a sender that … requires absolutely ToB execution, and cannot leverage trust, indeed has a much lower value separation.” So the STs that most need CR are exactly the ones with small m — and they are precisely the ones this clause makes cheapest to exclude.

The attack (and the cost). To knock a FOCIL-timely victim ST with fee f and budget m out of the block, an attacker publishes m + 1 STs each with tob_fee just above f (one wei suffices; each also revealing normally). At the fee-ordered position, m + 1 > m higher-fee commitments now sit ahead of the victim, so the builder cannot place it without violating its own stipulation → validly excluded. For the maximally-protected case m = 0, this is one attacker ST. Because the attacker reveals, it only burns f / TOB_FEE_FRACTION = f / 128 of its own ToB fee plus base gas — it is a real, cheap transaction, not burned spam.

Worked cost, m = 0, victim f = 0.1 ETH, base fee 10 gwei, ETH ≈ $1,899.52:

attacker burn      = f / 128            = 0.00078 ETH  ≈ $1.48
attacker base gas  = 21,000 × 10 gwei   = 0.00021 ETH  ≈ $0.40
total              ≈ 0.00099 ETH        ≈ $1.88 to censor one maximally-protected ST for one slot

For general m the cost is (m + 1) · (f/128 + base_gas) — still a rounding error against the value of a ToB slot, and the attacker’s STs execute as ordinary cheap transfers, so this is not even a griefing burn. Worse, no adversary is required: a fee-maximising builder naturally fills the ToB lane with the highest-tob_fee commitments available, so ordinary block-building crowds low-fee, low-m, protection-seeking STs out of ToB as a matter of course. The IL path only makes it forceable — an attacker can seat its crowding STs via includers (MAX_ST_COMMITS_PER_IL = 2, so ⌈(m+1)/2⌉ includers), where FOCIL prevents the builder from dropping them.

Net: as specified, LUCID’s CR guarantee reduces to “your ST is force-includable at ToB only if you are willing to out-fee every other ToB bidder” — which is the private-order-flow, pay-for-priority regime LUCID exists to escape. The claim in Motivation should not stand un-caveated.

Fixes (any one closes it; I recommend the first):

  1. Make max_preceding_commitments a placement constraint, not an exclusion trigger. A FOCIL-timely ST that would violate its budget at its fee-ordered position is instead inserted at the earliest position that satisfies the budget (i.e., promoted past higher-fee commitments), with the existing tie-break for STs that collide. This preserves force-inclusion (CR) at the cost of strict fee-monotonicity in the ToB order — an acceptable trade, since max_preceding_commitments is a safety budget, not a price, and a benign high-fee arb that reveals does not threaten the promoted ST.
  2. Count only potentially-withholding preceding commitments. Adopt the trust-graph refinement the authors already sketch under “Alternative specifications” (max_preceding_untrusted_commitments): a preceding commitment from a trusted / reveal-committed publisher does not consume the victim’s budget, so honest high-fee flow cannot crowd out a protected ST.
  3. Reserve a FOCIL-forced sub-allotment. Carve out a minimum number of ToB positions (or a slice of tob_gas_limit) for FOCIL-included STs that is exempt from the max_preceding_commitments exclusion clause.

At minimum, if none of these is adopted, Security Considerations must state honestly that FOCIL force-inclusion does not hold for STs whose tob_fee is below the ToB clearing fee — otherwise the CR claim in Motivation over-promises.


§1.9 — Recovery reordering silently violates the max_preceding_commitments safety guarantee across scheduling blocks

max_preceding_commitments is described as an “upper bound” (ST-ticket field list) and is the sole in-band lever a self-decrypting sender has against probabilistic front-running (Rationale point 5: at most m preceding commitments ⇒ an adversary “can choose between at most 2^{m-1} distinct patterns”). “Recovery under rejected payload” quietly breaks that bound.

When payload n is rejected, the recovery payload carries decrypted transactions from two scheduling blocks:

Both sets of STs that shall be included must then be included in the next payload. … The decrypted_transactions must be ordered according to the ST-commitments: those derived from commitments in beacon block n-1 go before those from commitments in beacon block n.

and enforcement is scoped per scheduling block:

When a recovery payload includes commitments from multiple scheduling beacon blocks, they are processed in scheduling-block order, and max_preceding_commitments is enforced only with respect to commitments from the same scheduling beacon block.

Consequence: an ST committed in beacon block n with max_preceding_commitments = 0 — a sender who paid for and consented to zero preceding commitments, i.e., strictly ToB execution — is, under recovery, preceded by every decrypted transaction from beacon block n-1, none of which counts against its budget. A guarantee the sender purchased (“at most 0 patterns can precede me”) is silently converted into “an entire prior block’s worth of commitments precede me.” The 2^{m-1} bound the Rationale advertises is off by however many commitments the other scheduling block contributes.

Why this is more than a documentation gap. Payload rejection is not purely exogenous: a proposer/builder can induce it by releasing a late or invalid payload n (the whole “Recovery under rejected payload” machinery exists precisely because payloads get rejected). An adversary who wants to seat selectors ahead of a victim’s m = 0 ST scheduled in block n can pre-commit its selector STs in block n-1, then induce rejection of payload n; the recovery payload now executes the block-n-1 selectors ToB, ahead of the victim, for free — none charged against the victim’s zero budget, and the victim has no in-protocol signal that its protection has lapsed. Even absent a deliberate inducer, an honest reorg/rejection degrades the very users the design most wants to serve, with no warning.

Fixes:

  1. Enforce the budget across the recovery-payload prefix, not per scheduling block. For any ST scheduled in block n, count all decrypted transactions ordered before it in the recovery payload — including those from block n-1 — against its max_preceding_commitments. If the combined prefix would exceed the budget, the ST is deferred (recovered in a later payload) rather than executed under-protected. This keeps the sender’s purchased guarantee intact at the cost of one extra slot of latency in the rare rejection case.
  2. Alternatively, allow the sender to opt out of under-protected recovery. Add a ticket flag recover_only_if_budget_preserved; if set, an ST whose max_preceding_commitments cannot be honored against the full recovery prefix is treated as a burned intent (ticket already charged, PT not executed) rather than executed with broken protection — a wallet-level safety choice consistent with the reorg guidance requested in §1.6.
  3. At minimum, state the cross-block relaxation explicitly in the Rationale next to point 5, so senders can price the residual risk instead of discovering it after a rejection.

§1.10 — executable is specified as a per-ticket check but is actually a global fixpoint over ordering, gas_obligation, and bundle_tob_fee

“Extended payload bid with ST-commitments” defines the executable bit locally:

The executable[i] bit is set to 1 iff ticket i can pay ticket_fee … and passes the ticket.nonce check when ST tickets are processed in st_commitments order and bundle members are processed in bundle order.

But every input to that check is itself a function of the outputs:

  • gas_obligation is “the aggregate ticket.gas_limit for bundle members with executable[i] == 1”;
  • bundle_tob_fee “is the aggregate of the tob_fee of all STs in the bundle with executable = 1”;
  • a bundle’s collective max_preceding_commitments is “the minimum max_preceding_commitments among STs with executable = 1”;
  • the ST-commitment ordering rule sorts by commitment_tob_fee then gas_obligation — i.e., by quantities that depend on executable;
  • and executable is evaluated “when ST tickets are processed in st_commitments order” — i.e., against the position produced by that very sort.

So executable → gas_obligation, bundle_tob_fee → sort position → the running state a ticket is charged against → executable. This is a fixpoint, not a computation, and the spec supplies neither a constructive algorithm nor a uniqueness argument. Two concrete pathologies follow.

(a) Unsatisfiable configurations (griefing / accidental). Because ordering is by fee but the charging step asserts nonce sequentiality and standalone STs have no skip bit (see §1.1), a builder can be handed a set of FOCIL-forced commitments for which no assignment yields a valid block. Minimal instance: account P signs a standalone ST X (nonce k, tob_fee = 5) and also a ticket inside bundle Y (nonce k, contributing enough tob_fee that Y outsorts X when that member is executable = 1). If Y’s P-member is executable = 1, Y sorts first, consumes nonce k, and then standalone X fails assert ticket.signer.nonce == ticket.nonce → invalid payload (standalone STs cannot be skipped). If instead Y’s P-member is executable = 0, Y’s tob_fee drops, X may now outsort Y, X consumes nonce k, and Y’s member is correctly 0but whether X actually outsorts Y after the drop depends on the other commitments, and an adversary can tune fees so that neither branch is consistent. The builder is then forced to drop a FOCIL-forced entry (attester penalty) or ship an invalid block.

(b) Builder-selectable fixpoints (another free option). For pure-bundle configurations where more than one assignment is self-consistent, nothing in the spec says which the builder must pick. Dropping a marginal member (executable = 0) that lowers bundle_tob_fee just enough to move the bundle past a competing commitment can be self-consistent both ways, and the two choices decrypt/execute different transaction sets and pay out different refunds. That hands the builder discretion over which committed STs execute — the same per-commitment reveal optionality §1.2 flags in the key-vote path, arriving here through the ordering path instead.

Fix. Specify executable and the ordering as the output of a single deterministic constructive procedure, and make block validity “the builder’s assignment equals the output of that procedure” (a unique fixpoint by construction), rather than “the assignment satisfies the local predicate.” Concretely: seed the order using each commitment’s maximal tob_fee/gas_obligation (all members tentatively executable = 1); process commitments ToB against the running state, turning off any member that fails balance/nonce at its position; when a member flips off, re-derive that commitment’s bundle_tob_fee/gas_obligation and re-insert it at its new ordered position; iterate to a fixpoint with a tie-break (e.g., process the lowest-commitment_root unresolved commitment first) that guarantees termination and uniqueness. Then define standalone STs to carry a one-bit executable too (per §1.1) so the procedure never has to emit an invalid block. This is the LUCID-side analogue of the constructive-filter fix I propose for 8105 in §2.4.


§1.1 (extension) — the same-sender nonce-vs-tob_fee ordering conflict (sharper trigger, no second account needed)

There is a sharper, more general form of this dilemma that needs no cancellation and no separate attacker account. The commitment order is fixed by “descending commitment_tob_fee,” but ticket charging asserts ticket.signer.nonce == ticket.nonce in that same order. These two rules disagree whenever one account has two STs in a slot with different ToB fees. Take account P with STs at nonces k (tob_fee = 1) and k+1 (tob_fee = 2). The ordering rule sorts the k+1 ST first (higher fee); charging then asserts signer.nonce == k+1 while the account nonce is still k → the assert fails. For a standalone ST this invalidates the payload (no skip bit); inside a bundle it forces executable = 0 on a ticket the sender intended to execute. So a single honest user simply cannot place two ToB STs with non-equal fees in one slot — the protocol’s own ordering rule guarantees one of them fails its nonce check.

Adversarially, this collapses the cost of the §1.1 griefing attack and removes its only friction (needing a second transaction to burn the nonce): an attacker submits two STs from one fresh account with deliberately fee-inverted nonces (nonce k high fee, nonce k+1 low fee). Any builder that includes both is forced into the failing-nonce assert; if either is standalone, the block is invalid. The attacker never has to race a separate type-2 transaction to the builder — the two STs are self-contradicting by construction, and both can be seated via ILs to make them non-droppable. Cost is unchanged (~$1–2, see §1.1 quantification below); the trigger is now fully within the ST lane and deterministic.

…and specify that when one signer has multiple STs/bundle-members in a slot, they are ordered by ascending nonce before the tob_fee sort is applied across signers (or reject a bid containing fee-inverted same-signer nonces at validation), so the ordering rule can never contradict the nonce assert.


§1.6 (extension) — the max_fee >= base_fee + key_publication_fee assert is outside the executable skip path

  • The fee-adequacy assert is not integrated with executable. Charging begins with assert max_fee >= base_fee + ticket.key_publication_fee before any priority_fee/tob_fee is computed, but the executable definition only covers “can pay ticket_fee … and passes the ticket.nonce check” — it never mentions this earlier assert. A bundle member whose max_fee no longer covers base_fee + key_publication_fee (base fee is known at bid time for block n, but a mis-specified or stale ticket can still fail it) has an ill-defined executable — “can pay ticket_fee” is undefined when priority_fee = min(max_priority_fee, max_fee − (base_fee + key_publication_fee)) goes negative — yet the charging pseudocode hard-asserts. Fold this assert into the executable predicate (member is executable = 0 if max_fee < base_fee + key_publication_fee) so the skip mechanism, not a block-invalidating assert, handles it — the same defect class as §1.1.

Quantification addenda

§1.1 (extension) — attack cost / asymmetry

Concrete cost of forcing the block-validity dilemma, ETH ≈ $1,899.52:

Base fee Attacker cost (one 21k self-transfer, or one extra fee-inverted ST) Effect
10 gwei 0.00021 ETH ≈ $0.40 one contested / missed slot
30 gwei 0.00063 ETH ≈ $1.20 one contested / missed slot

Against the value of a slot (block reward + priority fees + MEV, routinely $10²–10⁴), the asymmetry is ~10³–10⁴×, and the attack has no per-account rate limit (a fresh account/nonce each slot), so it composes into a sustained sub-$2/slot liveness attack until the eviction condition in the §1.1 Fix is added.

§1.2 (extension) — EV of the sanctioned per-key free option, and how an adversary manufactures the tie deterministically

The overlap of conditions (M) and (O) is not a rare accident to be waited out — it can be constructed. Recall (M) ⟺ timely_1 <= timely_0 + late_0 + E and (O) ⟺ timely_1 + late_1 + E > timely_0. An adversary that controls its own key-release timing releases the k_dem right at lucid_key_vote_deadline, driving the honest committee toward a ~50/50 timely_1 ≈ timely_0 split (roughly half the PTC observes it before the deadline, half after). At that split, a single late “observed” vote (late_1 = 1) — which the adversary can supply from one colluding or merely slow PTC member at zero cost, since late votes are unpenalised and equivocation on LucidKeyTimelinessVote is explicitly unslashed — satisfies both inequalities simultaneously. The builder then holds a valid choice either way, and “If this test fails for any key…” fires for neither.

EV framing: the option the tob_fee burn is designed to price at ≥ tob_fee (Rationale point 4b: “it must pay the higher penalty … tob_fee”) is obtained here for 0. For a representative autogenous-MEV ST protecting a trade of value V with tob_fee = 0.1 ETH (≈ $190), the builder captures a per-key reveal option worth up to V at a marginal cost of one delayed vote — the entire pricing mechanism leaks at the gray-zone boundary, and the boundary is reachable on demand, per key, deniably. This is why the §1.2 fix (complementary predicates + counting absence as late_0) matters more than the vote-timing cosmetics: it removes the region in which the option exists at all.

§2.2 (EIP-8105) — trust-graph reachability DoS, bounded

Registration is permissionless and trust is path existence (“A trusts B iff a directed path from A to B exists”), evaluated during block validation for every encrypted-transaction adjacency. An adversary registers a chain of N providers P_1 → P_2 → … → P_N once; thereafter any block containing an adjacency that resolves along that chain forces an O(N)-hop reachability query on every validating node, every block, indefinitely. One-time attacker cost ≈ N × (registration gas); permanent cost imposed ≈ O(N) graph traversal per adjacency per block across the whole network. Even at a generous 100k gas/registration and 10 gwei, N = 1,000 costs the attacker ≈ 0.1 ETH (~$190) once, to lengthen the worst-case validation path for the network forever — the definition of an asymmetric DoS. This is why the §2.2 fix (bounded, pre-computed transitive closure or direct-edge-only trust with capped out-degree) is a validity-critical requirement, not an optimisation.

§2.3 (EIP-8105) — sustained ~2× execution load, quantified

Because decrypted transactions’ “consumed gas does not count towards the block gas limit and no fee is paid,” and 8105 (unlike LUCID) has no TOB_GAS_FRACTION-style cap, block n+1 can be obligated to execute up to a full extra block of gas on top of its own. At a 30M gas target that is a sustained worst case of ~60M gas of execution and state-growth per slot (≈ 4.5M slots/year → double the intended state-growth budget during encrypted-heavy periods), all of it paid for and therefore not spam, but converting the gas limit from a hard per-slot execution bound into a soft ~2× target. LUCID already solves this with tob_gas_limit = block.gas_limit // 8; the cheapest harmonisation, given the two teams’ convergence, is for 8105 to adopt the same fractional cap. Belongs in 8105 Security Considerations with that mitigation named.


RELATIONSHIP TO PRIOR SUBMISSIONS — explicit complement to topic 897 (Bonus)

The one other formal submission in this category, topic 897 (Bonus, “Key-Withholding Optionality as a Structural MEV Risk”), models the economics of the withholding option — the E[V_withhold] − E[V_reveal] > (127/128)·F decision, the risk-free-option framing, aggregate optionality accumulation, and the FRAME-as-primary-path and PTC-per-key-bribery observations (the latter two already anticipated in LUCID’s own Rationale under “Frame transaction integration” and “PTC influence and state observation”). I treat that economic model as an input, not a competitor: this review supplies the spec-mechanics layer beneath it. §1.2 shows the withholding option Bonus prices is, at the key-vote gray zone, available to the builder for zero rather than F — a strictly stronger version of the same concern, sourced to two non-mutually-exclusive predicates in the text. §1.8 shows the reveal/withhold option is not even the cheapest lever: tob_fee flooding excludes protected STs for ~$2 without any withholding at all. §1.3’s escrow/burn ambiguity determines whether the withholding penalty Bonus assumes is burned actually is burned, or silently rebated to a colluding builder. §1.10 exposes a second, ordering-path free option distinct from the key-vote path both posts discuss. In short: Bonus establishes that embedded optionality exists; this review locates where in the spec each option is minted and gives sentence-level fixes to close each one — which is the comprehensive, iteration-friendly artifact the contributor vote rewards.

Revision 3 (2026-07-22) — proposed normative fixes

Revision 3. Revisions 1–2 located, in the text of both specs, a set of block-validity ambiguities and free options that adversaries (or fee-maximising builders) can trigger cheaply. This revision converts the highest-impact of those findings into adoptable spec patches: each quotes the current normative text, gives a drop-in replacement, and a one-paragraph rationale, so an author can paste them into eip-8184.md without further design work. All quoted strings are verbatim from ethereum/EIPs master (reviewed 2026-07-20). One genuinely new finding (§1.11) closes.


Patch A — §1.2 key-timeliness: make the two treatments mutually exclusive

Current text (“Key timeliness”):

The builder can thus treat a key as missing under condition:

  • 2 * timely_1 <= (timely_1 + timely_0 + late_0 + equivocated_votes);

and treat a key as observed under condition:

  • 2 * (timely_1 + late_1 + equivocated_votes) > (timely_1 + timely_0 + late_1 + equivocated_votes).

If this test fails for any key, the PTC of the next slot votes against the payload timeliness.

Proposed replacement:

Let committee_size be the number of PTC members assigned to vote on the key. The builder MUST treat a key as observed iff

  • 2 * (timely_1 + late_1 + equivocated_votes) > committee_size

and MUST treat it as missing otherwise. The two treatments are exact negations of one another, so exactly one is valid in every view. A PTC member that has cast no non-equivocated key vote by attestation time is thereby counted on the missing side (it appears in none of timely_1, late_1, equivocated_votes). If the builder’s treatment of any key differs from the value this predicate selects, the PTC of the next slot votes against the payload timeliness.

Rationale. As written, the “missing” predicate omits late_1 and the “observed” predicate omits late_0, so both can hold at once (rev-1 §1.2 witness: timely_1=4, timely_0=4, late_1=1). In that overlap the builder holds a protocol-sanctioned free choice over whether a specific decrypted transaction executes — exactly the per-commitment reveal optionality the tob_fee burn exists to price, obtained here for zero (rev-2 §1.2). A single threshold against a fixed denominator (committee size, not votes received) removes the overlap by construction and makes validator silence count as missing-leaning rather than vanishing from both sides. Equivocated votes sit on the observed side only, so equivocation — an attributable, slashable fault — can push a key toward inclusion but never toward the builder’s discretionary drop; authors who prefer may instead exclude equivocated_votes entirely, the essential property being the single threshold.


Patch B — §1.8 CR inversion: make max_preceding_commitments a placement constraint, not an exclusion trigger

Current text (“Inclusion lists with STs and bundles”):

An entry of a timely IL may still be excluded from the block if:

  • The aggregate gas_limit of all STs (including bundled STs) of the IL exceeds tob_gas_limit_per_il.
  • It cannot be inserted in st_commitments at the position determined by the ST-commitment ordering rule (see Block processing)—without breaking its max_preceding_commitments stipulation.

Proposed replacement:

An entry of a timely IL may still be excluded from the block if:

  • The aggregate gas_limit of all STs (including bundled STs) of the IL exceeds tob_gas_limit_per_il.

A timely IL entry whose fee-ordered position would place more than max_preceding_commitments commitments ahead of it MUST NOT be excluded on that ground. Instead it is promoted to the earliest position in st_commitments at which at most max_preceding_commitments commitments precede it, ahead of higher-commitment_tob_fee commitments as required. Promotion overrides the descending-commitment_tob_fee ordering for that entry only; ties among promoted entries competing for the same position are broken by the existing rule (descending gas_obligation, ascending commitment_root). max_preceding_commitments is thus enforced as a placement constraint, never as an exclusion condition.

Rationale. Composing the fee-descending order with an exclusion trigger keyed on max_preceding_commitments lets an attacker publish m+1 STs with tob_fee one wei above a FOCIL-timely victim’s and validly evict it — ~$1.88 for the maximally-protected m=0 case (rev-2 §1.8), and a fee-maximising builder does it for free as a matter of course. That inverts the Motivation’s claim that LUCID “broadens the practical censorship resistance of inclusion lists” precisely for the low-m STs the design most exists to protect. Treating the budget as a placement constraint preserves force-inclusion at the cost of strict fee-monotonicity — an acceptable trade, since max_preceding_commitments is a safety budget, not a price, and a benign high-fee arb that reveals does not threaten the promoted ST. Update the following attester-audit paragraph in step: attesters vote against the block if a timely IL entry could have been promoted to a budget-satisfying position but was omitted.


Patch C — §1.9 recovery reordering: enforce the budget across the whole recovery prefix

Current text (“Recovery under rejected payload”):

When a recovery payload includes commitments from multiple scheduling beacon blocks, they are processed in scheduling-block order, and max_preceding_commitments is enforced only with respect to commitments from the same scheduling beacon block.

Proposed replacement:

When a recovery payload includes commitments from multiple scheduling beacon blocks, they are processed in scheduling-block order. For each ST-commitment, max_preceding_commitments is enforced against all decrypted transactions ordered before it in the recovery payload, including those derived from a different scheduling beacon block. If honoring an ST-commitment’s max_preceding_commitments against this combined prefix is impossible, its transactions MUST NOT be executed under recovery and are instead deferred to a later payload (its ticket having already been charged), rather than executed with more preceding commitments than the sender consented to.

Rationale. max_preceding_commitments is the sole in-band lever a self-decrypting sender has against probabilistic front-running: Rationale point 5 advertises that at most m preceding commitments bound the adversary to 2^{m-1} patterns. Enforcing it “only with respect to commitments from the same scheduling beacon block” silently places an entire prior block’s decrypted transactions ahead of an m=0 ST — converting “zero preceding commitments” into “a whole block’s worth,” none charged against the budget. Because payload rejection is inducible by a proposer/builder (the recovery machinery exists because payloads do get rejected), an adversary can pre-seat selector STs in block n-1 and induce rejection of payload n to run them ToB ahead of a victim in block n for free (rev-2 §1.9). Enforcing the budget across the full recovery prefix keeps the purchased guarantee intact at the cost of one extra slot of latency in the rare rejection case. Authors who prefer to preserve liveness over protection can instead gate this on a per-ticket opt-in flag (recover_only_if_budget_preserved), but the default should not silently under-protect.


Patch D — §1.10 fixpoint: define executable/ordering by a single constructive procedure

Current text (“Extended payload bid with ST-commitments”):

The executable[i] bit is set to 1 iff ticket i can pay ticket_fee (defined below) and passes the ticket.nonce check when ST tickets are processed in st_commitments order and bundle members are processed in bundle order.

Proposed replacement (and add, in “Block processing”, “the builder’s st_commitments ordering and every executable bitfield MUST equal the output of the following procedure”):

st_commitments, every executable bit, each gas_obligation, and each bundle_tob_fee are the unique output of the following deterministic procedure. Seed: set executable[i] = 1 for every ticket of every commitment; compute each commitment’s gas_obligation, bundle_tob_fee/tob_fee, and collective max_preceding_commitments from these tentative bits, and order commitments by the ST-commitment ordering rule. Iterate: process commitments top-of-block against the running post-state; within a commitment, process members in bundle order and set executable[i] = 0 for any member that fails its balance or nonce check, or for which max_fee_per_gas * gas_limit < base_fee + key_publication_fee, at its position. Re-derive: whenever a bit flips to 0, recompute that commitment’s gas_obligation/bundle_tob_fee/max_preceding_commitments and re-insert it at its new ordered position; restart processing from the earliest commitment whose ordering key changed. The procedure terminates because bits are monotone (only 1→0); ties in “which unresolved commitment to resolve first” are broken by ascending commitment_root, giving a unique fixpoint. A standalone ST carries a one-bit executable under this procedure exactly as a singleton bundle does; a standalone ST whose bit resolves to 0 is omitted from the payload rather than invalidating it.

Rationale. Every input to the current per-ticket executable predicate is a function of its own output: gas_obligation and bundle_tob_fee aggregate over executable, the ordering sorts by those aggregates, and executable is evaluated at the position that ordering produces. This is a fixpoint, not a computation; the spec gives neither a constructive algorithm nor a uniqueness argument, admitting both unsatisfiable configurations (rev-2 §1.10a — FOCIL-forced commitments with no valid assignment, forcing a dropped entry or an invalid block) and builder-selectable ones (rev-2 §1.10b — a second free option distinct from the §1.2 key-vote path). Defining validity as “the assignment equals the procedure’s output” replaces a satisfaction predicate with a computation that has exactly one answer, and the standalone skip bit closes the block-validity dilemma of rev-1 §1.1. This is the LUCID-side analogue of the constructive-filter fix proposed for EIP-8105 §2.4.


Patch E — §1.7 tie-break: add an explicit ToB-adjacency primitive for benign backrunning

Current text (“Block processing”):

verify that the commitments are ordered by descending commitment_tob_fee, tie-breaking by descending gas_obligation and ascending commitment_root respectively; referred to as the “ST-commitment ordering rule”.

Proposed replacement / addition (add predecessor_root as a ticket field, default 0x0…0):

verify that the commitments are ordered by descending commitment_tob_fee, tie-breaking by descending gas_obligation and ascending commitment_root respectively; referred to as the “ST-commitment ordering rule”. A commitment whose ticket sets a non-zero predecessor_root = r is valid only if it is placed in the position immediately following the commitment whose commitment_root == r, and is included if and only if that predecessor commitment is included; this linkage overrides the ordering rule for the linked pair. A predecessor_root referencing an absent commitment makes its own commitment ineligible for inclusion (it is treated as an unmatched IL entry, not a block-validity failure).

Rationale. The authors recommend (Hegota thread) monetizing benign backruns by setting “identical or sequential (in wei) ToB fees,” but the tie-break sorts on commitment_root = keccak256(ticket_bytes), grindable without touching tob_fee or gas_obligation — any priority-fee dust or ciphertext byte reshuffles the hash. An adversary bids exactly the leg’s fee, matches gas_obligation, and grinds its root to seat itself between a user’s trade and its backrun (rev-1 §1.7); wei-adjacency yields no atomic adjacency, leaving the single-key_publisher bundle as the only sound coupling primitive and reintroducing the trusted intermediary LUCID exists to reduce. An explicit predecessor_root link gives permissionless, multi-party backrun markets true ToB adjacency without a shared publisher.


Patch F — §1.5 DEM nonce: derive from the key commitment (one-line diff)

Current text (“Ciphertext envelope”):

  • Nonce – keccak256(chain_id || ticket_from || ticket_nonce)[:12] for STs.

Proposed replacement:

  • Nonce – keccak256(chain_id || ticket_from || ticket_nonce || key_commitment)[:12] for STs. A given k_dem MUST NOT be used to encrypt more than one ciphertext_envelope.

Rationale. The nonce is currently fully determined by (account, nonce), both constant across a replacement/fee-bump. A wallet that re-encrypts a modified plaintext for the same ticket nonce while reusing k_dem — the natural lazy implementation, since key_commitment then stays stable — produces two ChaCha20-Poly1305 ciphertexts under the same (key, nonce); when k_dem is released, any observer who archived the superseded ciphertext recovers keystream, the XOR of both plaintexts, and Poly1305 forgery capability (rev-1 §1.5). Binding the nonce to key_commitment makes it fresh whenever the key is fresh, is circularity-free (key_commitment is fixed before encryption), and turns the generic reuse warning in Security Considerations into a protocol-enforced property.


§1.11 (new finding) — key-timeliness equivocation is detected per-message but consumed per-key

New in rev-3; HIGH confidence it is an underspecification with consensus impact.

A LucidKeyTimelinessVote carries a single keys_observed: Bitlist[MAX_ST_COMMITS] covering every commitment in the slot, signed once. But the equivocation rule is written per-key:

Nodes treat equivocated LucidKeyTimelinessVote messages the same way they do for equivocated ILs in FOCIL: they propagate the first two distinct signed votes they have observed for the same key to signal equivocation.

and equivocated_votes is then consumed per key inside the Patch-A threshold conditions (“The number of committee members who equivocated is tallied in equivocated_votes. These votes are not included in the key votes bitfields.”). The two granularities are incompatible: because one signature spans all keys, any two distinct signed votes from a validator differ as messages, not “for a key.” If a member signs two messages that disagree on a single bit, the spec does not say whether it is counted in equivocated_votes for that one key or for all keys in its bitfield — and, per Patch A, that choice moves the numerator of every affected key’s timeliness threshold by one, i.e. it can flip a key between observed and missing. Different clients resolving this differently split consensus on payload timeliness.

Fix. Define equivocation at the message level, matching the vote structure: a validator that publishes two distinct signed LucidKeyTimelinessVote messages for the same (scheduling_beacon_block_root, scheduling_slot) has its entire bitfield discarded and is counted once in equivocated_votes for every key in the slot. Concretely, replace “for the same key” with “for the same (scheduling_beacon_block_root, scheduling_slot),” and state that an equivocating validator contributes to equivocated_votes uniformly across all commitments.


Six patches (§1.2, §1.8, §1.9, §1.10, §1.7, §1.5) drafted as quoted-current → proposed-replacement + rationale, adoptable as-is; §1.11 is a new consensus-relevant underspecification. Prior findings (rev-1 §§1.1–2.4, rev-2 §§1.8–1.10 + quantification) stand as the analysis these patches close. Reviewed at ethereum/EIPs master, 2026-07-20.

Revision 4 (2026-07-28) — the decrypted-transaction execution path

Revision 4. Revisions 1–3 concentrated on the commitment side of LUCID: ticket charging, the executable/ordering fixpoint, the key-timeliness algebra, IL force-inclusion, and recovery ordering. This revision audits the other half of the lifecycle — what happens in slot n+1 when the committed decrypted_transactions actually execute — and finds two spec-level holes that none of the prior revisions, nor the other reviews in this venue, have traced. Both are grounded in verbatim block-processing text; one is a cheap unpreventable block-validity dilemma, the other a consensus-relevant underspecification with a concrete economic failure. A third, lower-severity completeness finding closes.

All quoted strings are verbatim from eip-8184.md (LUCID) and eip-8105.md, ethereum/EIPs master as reviewed 2026-07-28.

Diligence note (not a finding). While auditing the execution path I also checked whether the refund line parent_ticket.signer.balance += parent.base_fee_per_gas * (parent_ticket.gas_limit - dual_gas_used) + parent_tob_fee * (TOB_FEE_FRACTION - 1) // TOB_FEE_FRACTION can underflow or misprice under base-fee movement. It cannot. dual_gas_used is a real EVM gas_used metered against gas_limit = ticket.gas_limit, so parent_ticket.gas_limit - dual_gas_used >= 0 by EVM invariant (inflated ciphertext calldata only makes an under-provisioned PT OOG at dual_gas_used = gas_limit, giving a zero — never negative — term). And the base-fee-movement worry is unfounded: the ticket is charged in block n at block.base_fee_per_gas, and the refund runs in block n+1 against parent.base_fee_per_gas, i.e. block n’s base fee — the same figure — so charge and refund use an identical rate regardless of how the base fee moves between the two slots. The 127/128 split only loses sub-wei dust for parent_tob_fee < 128. I flag this explicitly so the sound part is not mistaken for unreviewed.


§1.12 (new) — LUCID has no execution-validity skip for a decrypted transaction; a PT that is invalid at slot-n+1 execution forces an invalid-block-vs-omit dilemma the builder cannot avoid

New in rev-4; HIGH confidence — this is a dropped validity condition that EIP-8105 has and LUCID does not.

The plaintext transaction (PT) is not merely decrypted and checked against a commitment — it is executed in the following block. “Block processing”:

The PT is then executed as a regular transaction, except that it bypasses the ordinary EIP-1559 fee-cap validation and no gas fee is charged, since the fee was already prepaid by the ST ticket.

A “regular transaction” is only includable in a valid block if it passes transaction-level validity at its execution position: the signer’s nonce must equal tx.nonce, and the signer must hold the up-front cost (here, with gas prepaid, the transferred value). Crucially, the PT’s signer need not be the ticket signer — the Overview states the envelope “decrypts to a signed plaintext transaction (PT) that can have a different from field than the ST” — and nothing constrains that account’s nonce or balance relative to the ticket. The reveal check binds only shape, not executability:

Decryption of a scheduled ST is defined as successful only if decrypting ciphertext_envelope yields a plaintext_tx whose:

  • hash_tree_root(RevealCommitmentPreimage(...)) == ticket.reveal_commitment, and
  • plaintext_tx decodes to a type-2 transaction with:
    • max_fee_per_gas = 0,
    • max_priority_fee_per_gas = 0,
    • gas_limit = ticket.gas_limit.

So a PT can decrypt successfully (commitment matches, type-2, fees zeroed) and still be invalid at execution because its own from account has the wrong nonce or too little balance by the time it runs top-of-block in slot n+1. And the only omission clause LUCID gives covers non-decryption, not execution-invalidity:

They verify that decrypted_transactions contains the byte-identical transactions they derived via decryption, positioned in the same order as the corresponding charged ST tickets, with STs not decrypted left out from the decrypted_transactions list.

EIP-8105, the proposal LUCID consolidated over, had exactly this condition and LUCID dropped it. 8105 “Block Structure”:

A decrypted transaction must not be included if and only if any of the following conditions is met: […]

  • including the decrypted transaction would result in an invalid block (e.g., because of a nonce mismatch)

Why the builder cannot escape it. At commitment time (slot n) the PT is encrypted — the builder commits to ciphertext_hash without the key. It therefore cannot simulate the PT and cannot know whether the PT’s nonce/balance will be valid when the key arrives and the PT executes in slot n+1. This is precisely why 8105 needed an explicit skip. Note this finding survives the rev-1 §1.1 and rev-2 §1.10 fixes: those give the ticket a skip bit and a constructive executable at charging time in the commitment block. §1.12 is about a different transaction (the PT, possibly a different signer), a different block (the execution slot n+1), and a different failure (the PT’s own execution validity, unknowable when the block that commits to it is built).

Concrete failure / attack. Two variants, both cheap:

  • Organic. A self-decrypting user commits an ST at ticket_nonce = k. Between commitment and execution the user (or their wallet’s retry logic) sends an ordinary transaction from the same account, advancing the nonce. The PT’s nonce is now stale → invalid at execution.
  • Griefing. An attacker crafts a PT whose from is a fresh zero-balance account sending value = 1 wei, or whose nonce is deliberately wrong. The ST reveals normally (key released, reveal_commitment matches), so the builder is obligated to place the byte-identical PT in decrypted_transactions — but the PT is invalid at execution. The builder’s only moves are: include it → invalid block; omit it → violates “contains the byte-identical transactions they derived via decryption.” Cost to the attacker is one ST ticket fee; cost to the network is a contested/missed slot, and unlike rev-1 §1.1 no diligent builder can pre-empt it, because the poison is inside the ciphertext.

Proposed replacement. Extend the omission clause and pin the fee treatment. Replace:

[…] with STs not decrypted left out from the decrypted_transactions list.

with:

[…] with two classes of ST left out of the decrypted_transactions list: (a) STs whose key was not timely or whose decryption was unsuccessful, and (b) STs whose successfully-decrypted plaintext_tx is invalid at its execution position — i.e. it would not be includable as a regular transaction (nonce mismatch against the PT signer’s account, or insufficient balance for its value), evaluated sequentially against the running top-of-block state. Omission of a class-(b) ST MUST NOT invalidate the block. For a class-(b) ST the ticket sender is refunded as on the successful-reveal path for zero executed gas (dual_gas_used set to the PT’s intrinsic/calldata cost only), and the key_publication_fee is credited as usual, since the key was revealed; the sender is thus charged only the reveal-path residual (tob_fee / TOB_FEE_FRACTION plus consumed calldata), never the full non-reveal penalty.

Rationale. Restoring the 8105 condition is the minimal fix and keeps LUCID’s block-validity closed under the one input a committing builder provably cannot inspect. The fee clause matters: a naïve “just skip it” would let an attacker mint STs that reveal-then-fail for the price of the reveal residual while the sender keeps the full base-fee-on-gas refund — a griefing subsidy. Pinning class-(b) to the reveal-path refund (they did reveal) with dual_gas_used = intrinsic-only both prevents that subsidy and avoids over-punishing the organic self-decrypt case with the non-reveal penalty. Validity stays deterministic: the PTs execute sequentially against slot n+1’s fixed top-of-block pre-state, so class-(b) membership is a function of committed data, introducing no builder option (contrast §1.13).


§1.13 (new) — the execution context of decrypted transactions is defined only for three opcodes; every other header field (COINBASE, NUMBER, TIMESTAMP, PREVRANDAO, BASEFEE) is left ambiguous between two readings, splitting consensus and mis-paying backruns

New in rev-4; HIGH confidence it is a consensus-relevant underspecification.

LUCID makes a strong blanket statement about where decrypted transactions execute, then enumerates adjustments for exactly three things. “Block processing”:

The decrypted_transactions are executed against the header of the block in which their ST ticket was committed to, in order to prevent builders from being able to inject information into the block after having witnessed decrypted transactions. During processing, the SLOTNUM opcode introduced in EIP-7843 must always return the slot number which included the corresponding ST ticket. The addition of the parent block hash to the historical block registry defined in EIP-2935 is no longer done at the top of the block; it is deferred till after the last ST has been executed. Likewise, the addition of the parent block’s beacon root to the historical root registry defined in EIP-4788 is no longer done at the top of the block; it is also deferred till after the last ST has been executed.

The clause is self-undermining. If “executed against the header of the block in which their ST ticket was committed to” already substituted the whole header, then SLOTNUM would return the committed block’s slot without a special rule — yet the text singles SLOTNUM out. That singling-out is evidence the blanket phrase is not meant to swap every header field, which leaves the resolution of NUMBER, TIMESTAMP, COINBASE, PREVRANDAO, BASEFEE, GASLIMIT, and BLOBBASEFEE undetermined. Two defensible readings:

  • Reading A (full header substitution): every block-context opcode returns the committed block n’s value while the PT runs inside block n+1.
  • Reading B (only the three enumerated fields change): SLOTNUM/2935/4788 behave as stated; all other opcodes return the execution block n+1’s values.

EIP-8105 chose Reading B explicitly and said so — “Transaction Execution”:

Decrypted transactions are executed as a regular transaction under the execution context of the inclusion block (e.g., for block.slot and block.coinbase).

LUCID inverted 8105’s rule for SLOTNUM (commitment-slot, per the special case) but never restated the choice for coinbase or the rest, so it is strictly less precise than the spec it is meant to consolidate.

Concrete failure #1COINBASE mis-pays the backrun that LUCID’s own Rationale promotes. The Rationale actively markets on-chain backrunning of decrypted flow — “The key publisher may also place backrunning transactions there, or at the end of a bundle” — and the standard searcher pattern pays the block producer via block.coinbase. Under Reading A, COINBASE inside a slot-n+1 decrypted transaction resolves to block n’s proposer/builder — the previous slot’s producer, who neither ordered nor included the PT — so a coinbase-paying backrun pays the wrong party, while block n+1’s builder, who did the work, receives nothing and has no incentive to include the backrun at all. Under Reading B it pays n+1’s builder, correctly. The design’s flagship endogenous-MEV feature therefore behaves oppositely under the two readings.

Concrete failure #2 — consensus split. NUMBER, TIMESTAMP, and PREVRANDAO differ between block n and n+1. A contract that branches on block.number, a deadline check on block.timestamp, or any PREVRANDAO-seeded logic in a decrypted transaction produces a different state transition under Reading A vs Reading B. Two honest clients that read the ambiguous clause differently compute different post-states for the identical block and permanently disagree — a chain split, not a mere quirk.

Proposed replacement. Replace the blanket sentence + three special cases with an exhaustive table:

Each decrypted_transaction executes in the current block (slot n+1) but resolves block-context queries as follows. Returning the committed block’s values (slot n, where the ST ticket was included): SLOTNUM; the EIP-2935 BLOCKHASH history and the EIP-4788 beacon-root history, whose top-of-block writes for slot n+1 are deferred until after the last ST executes (so a decrypted transaction observes the pre-n+1 history it was committed against). Returning the execution block’s values (slot n+1): COINBASE, NUMBER, TIMESTAMP, PREVRANDAO, BASEFEE, GASLIMIT, BLOBBASEFEE, and GASPRICE (which is 0, per the zeroed PT fee fields). Any header field not listed resolves to the execution block. Clients MUST implement this table exactly; divergence is consensus-breaking.

(The split above is one defensible resolution — commitment-slot only for the anti-information-injection primitives that the design’s security argument actually needs, execution-slot for producer identity and fee/number/time so backruns pay the real builder and contract logic sees the block it runs in. The authors may pick the other allocation for any given field; the finding is that the allocation must be total and stated, not that this particular split is uniquely correct.)

Rationale. The stated purpose — “prevent builders from being able to inject information into the block after having witnessed decrypted transactions” — is served by fixing the pre-state and the history-registry views (2935/4788) and the slot number, none of which a slot-n+1 builder should be able to bend. It is not served by aliasing COINBASE to the previous producer or NUMBER/TIMESTAMP to the previous slot; those choices instead break producer payment and split consensus for zero security benefit. An exhaustive table removes the ambiguity that the enumerate-three-fields phrasing creates and restores the precision 8105 had.


§1.14 (new) — the PT is constrained to a type-2 transaction, silently excluding EIP-7702 and blob flows from the encrypted mempool

New in rev-4; MEDIUM confidence — a completeness/limitation finding, not an attack.

Decryption success requires the PT to be a type-2 transaction. “Ciphertext envelope”:

The ciphertext_envelope decrypts to plaintext_tx, the raw bytes of an EIP-1559 transaction to be executed onchain.

reinforced in “Key message validity and decryption”:

  • plaintext_tx decodes to a type-2 transaction with:
    • max_fee_per_gas = 0,
    • max_priority_fee_per_gas = 0,
    • gas_limit = ticket.gas_limit.

This hard-codes the payload to EIP-1559 form, which excludes EIP-7702 (type-4) set-code transactions and EIP-4844 (type-3) blob transactions (and legacy/type-1) from LUCID entirely. The consequence is not cosmetic: the transactions LUCID most wants to protect — MEV-sensitive DeFi order flow — are migrating to 7702 smart-account UX, where the user-facing operation is a type-4 authorization-bearing transaction. As written, none of that flow can enter the encrypted mempool; a 7702 user must either forgo the delegation-setting operation or forgo MEV protection. The spec never states this restriction in Backwards Compatibility, so integrators discover it only by reading the decryption predicate.

Proposed replacement. Either broaden the payload or document the exclusion. Minimal documentation patch (add to Backwards Compatibility):

The sealed payload is restricted to EIP-1559 (type-2) transactions. EIP-7702 (type-4), EIP-4844 (type-3), and legacy transaction types cannot presently be carried as sealed transactions; users of those types must use the plaintext mempool. Extending the sealed payload to additional envelope types is deferred to a future EIP.

Preferred normative patch: allow plaintext_tx to decode to a type-2 or type-4 transaction (7702 authorization lists execute deterministically top-of-block and carry no extra builder option), keeping max_fee_per_gas = max_priority_fee_per_gas = 0 and gas_limit = ticket.gas_limit as invariants across whichever types are admitted; leave type-3 out (blob data cannot be prepaid through the ticket model without a separate blob-fee accounting, which is its own EIP).

Rationale. LUCID is silent here in the strongest sense: a one-line decryption predicate quietly amputates the fastest-growing slice of exactly the order flow the Motivation targets, with no note in the compatibility section. Admitting type-4 is low-risk (delegated accounts execute normally as senders; the delegation code runs only when the account is called), so the addressable-set cost is avoidable; at minimum the exclusion must be stated so it is a decision, not an accident.


Three new sections. §1.12 (HIGH) — no execution-validity skip for decrypted transactions, a builder-unpreventable invalid-block dilemma that 8105 explicitly guarded and LUCID dropped; survives the rev-1 §1.1 and rev-2 §1.10 fixes because it lives on the PT-execution side, not the ticket-charging side. §1.13 (HIGH) — the decrypted-transaction execution context is specified for only three opcodes, leaving COINBASE/NUMBER/TIMESTAMP/PREVRANDAO/BASEFEE ambiguous between two readings that mis-pay backruns and split consensus. §1.14 (MEDIUM) — the type-2-only payload silently excludes 7702/blob flows. The refund-arithmetic and base-fee-movement suspicions were checked and found sound (see diligence note). Reviewed at ethereum/EIPs master, 2026-07-28; all quotes verbatim from the local spec files.