> 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).*