Continuation of the post above, moved here to stay within Discourse’s length limit. Sections follow the order in which the post refers to them.
Route B details
Continued from “Route B” above.
begin. It requires EXTCODEHASH(sender) == keccak256(""). Frame 0’s APPROVE has already bumped the nonce, so the result is never 0. An EOA with an active 7702 delegation must clear it first.
It then reads the next three frames with FRAMEPARAM and FRAMEDATACOPY. Together with begin itself, they must match rows 1–4 of the table below in mode, target, data and flags (‖ is byte concatenation), so the four frames form exactly one atomic batch. It also requires finish to be the last frame of the transaction, so frame 0’s approval cannot act on the adopted code after Migrated.
Why forward. A revert unrolls only its own batch. If the set-code frame were outside finish’s batch, a revert in finish would leave the code adopted. When begin carries the flag, it shares a batch with the set-code frame, so a wrong structure reverts before the adoption and skips it. (Thanks to Daniil Ankushin for pointing this out.) Order matters too: initialize before the set-code frame would hit EIP-8141’s default code and still succeed.
finish. Transient storage doesn’t survive across frames, so finish checks that the frame three back is begin(p, ownerSig) on this instance, that it and the next two carry the flag, and that all three have status 1. Reading ownerSig from that frame is what makes P4 use the signature P1 accepted. Because finish locates begin by position, only frame 4 can pass, so Migrated is emitted at most once and initialize (frame 3) cannot call it.
What remains. If begin lacks the flag and so does the set-code or initialize frame, no guard frame shares a batch with the adoption. The same holds for a set-code frame anywhere outside this batch: begin rejects a sender that already has code, but cannot unroll a frame it shares no batch with. Wallets must build the frames as in the table; simulation catches a mistake.
ownerSig. It is still needed, because the ECDSA bytes verified by the default code are not readable from the EVM.
| # | mode | target | data | flags |
|---|---|---|---|---|
| 0 | VERIFY | sender | – | 0x3 (APPROVE_EXECUTION_AND_PAYMENT) |
| 1 | SENDER | MigrationCheck | begin(p, ownerSig) |
atomic |
| 2 | SENDER | sender | 0x01 ‖ p.source ‖ p.codeHash |
atomic |
| 3 | SENDER | sender | initialize(initData) |
atomic |
| 4 | SENDER | MigrationCheck | finish(p, ownerSig) |
– (closes the batch) |
More on the guard
- Missing EIP-8151 is caught. On a chain without EIP-8151, P4 fails, so the migration is refused. A missing EIP-8141 (FM7), by contrast, cannot be detected.
- The reference template is inert. The canonical reference kernel is deployed uninitialized. It has no authority and accepts initialization only from itself, so nobody can initialize or replace it, and its hash is stable in practice. FM5 matters mainly when the source is someone else’s live account.
Verifier rules in full
- Stateless (MUST). ERC-7913 only says SHOULD.
- Immutable (MUST). No
SETCODEFROM,SELFDESTRUCT,DELEGATECALLorCALLCODE, and not 7702-delegated. This covers helpers and key blobs too. - Canonical encodings (MUST). Reject trailing bytes, since
ARBITRARYsignature bytes are outside the signature hash. Reject empty keys. - Bytes at opcode positions (SHOULD). No
0xA7,0xF4or0xF2at an opcode position, including in compiler metadata.- A contract containing
0xA7is treated by EIP-8298 like a 7702-delegated account, which EIP-8141’s trace rules reject. - Contracts containing
0xF4/0xF2are tracked by code hash, with a cap on the pending transactions that depend on them. - The reference kernel’s verifiers avoid all three.
- A contract containing
- Gas (SHOULD). Publish worst-case gas.
Reference kernel: sketch, storage, PQ budget
Continued from “Reference kernel” above.
// frame data: verify(uint8 authorityId, uint8 sigIndex), covered by the signature hash
assert FRAMEPARAM(cur, MODE) == VERIFY
assert SIGPARAM(sigIndex, SCHEME) == ARBITRARY
assert SIGPARAM(sigIndex, MSG) == 0 // canonical tx signature hash
sig = SIGDATACOPY(sigIndex)
allowed = FRAMEPARAM(cur, ALLOWED_SCOPE)
scope = _authorize(authorityId, TXPARAM(SIG_HASH), sig) // shared verification (R3)
assert allowed != 0 && (scope & allowed) == allowed
APPROVE(0, 0, allowed)
- Storage. ERC-7201 namespaced. An active-id bitmap and the code hash recorded at initialization implement R4.
- PQ budget. The 100k budget covers the whole validation prefix, not just the verifier. Two candidates to measure against it:
- JARDIN’s compact path, at about 48K per the JARDIN note, or about 60K (an estimate) per its repository README;
- SLH-DSA-Keccak-128-24, at about 94K for verification alone per the SPHINCs- repository README, which leaves little room.
- Large PQ keys. They can live in a key-blob contract encoded as
0x00 ‖ (PUSH32 ‖ 32 bytes)*, so every byte is PUSH data. A raw 1,312-byte ML-DSA-44 key contains0xA7at an opcode position in about 80% of cases.
More related work
- ERC-8403 (ethereum/ERCs#1979) defines an account-authority lifecycle. It notes that no revoke removes a 7702-delegated EOA’s native key, and names two remedies: a new account, or adopting regular code at the same address with EIP-8298 and EIP-8151. This proposal covers the second.
- JARDIN is a hash-based PQ account (a research prototype per its README). Its verifiers could be wrapped as ERC-7913 verifiers.
- EIP-8425 (ethereum/EIPs#12383) installs a PQ wallet at the same address during recovery. The kernel could serve as that wallet. That path removes
ecrecover, though, so P1 and P4 don’t apply. - ERC-8421 (ethereum/ERCs#2028) defines alternative mempools, the natural home for configurations over
MAX_VERIFY_GAS.
More residual risks
- Q-day timing. The migration is signed with the old key, so this is a pre-Q-day procedure. Submit privately.
- Admin rights. Any execution-capable authority can replace the code; v1 has no separate admin role.
- Validation calling into the kernel. Another account’s validation may call into the kernel, for example to check its ERC-1271 signature. EIP-8141’s trace rules already reject that, because it reads storage outside
tx.sender. EIP-8298’s rule would reject even a storage-free call, because the kernel’s code containsSETCODEFROM. - Sponsoring. When the kernel approves payment for another sender, it acts as a non-canonical paymaster, limited to one pending transaction in the public mempool. Its
payframe reads the kernel’s own storage. I read EIP-8141’s “gas account” rationale as allowing that, but the trace rules don’t say so explicitly.
Nested-replacement test (from @Revinand)
Expected results:
- Replacement from inside
execute,executeBatch, an executor, or executor →handleOpsreverts. - Direct dispatch succeeds, with no
SSTORE,CALL*orSETCODEFROMby the account afterinitializereturns. - With old code that ignores R6, both
replace(oldSource)and a second op of the same bundle revert on R4’s lock.
More questions (Q4–Q13)
Continued from the open questions above.
For EIP-8141 authors
- Q4.
MAX_VERIFY_GAS. EIP-8141’s mempool rule 6 counts “the intrinsic cost of validatingtx.signatures”. I assume this means onlysignature_verification_cost(100 perARBITRARYentry), not signature calldata. Can you confirm? - Q5. P256 entries. Should protocol-validated P256 signature entries be an authority kind? They cost 6,700 gas and need no verifier call, so they leave the most room under 100k. But a contract sees only their resolved signer, and only inside the frame transaction that carries them, so ERC-1271 would need a separate path.
- Q6. Stateless verifiers. Under EIP-8151,
ecrecoverreads the recovered account’s code.ecrecover-based verifiers are therefore no longer stateless, and EIP-8141’s trace and revalidation rules don’t track that read; the canonical paymaster’s signer check is one such case. Should EIP-8141’s mempool section say so? (ERC-7913 itself is Final.) Now also raised in the EIP-8141 thread. - Q7. Large PQ keys. PUSH32 key blobs now, or wait for Public Key Aliases?
For wallet and account authors
- Q8. ERC-1271 wrapping. Should v1 use a minimal
AccountMessage(bytes32 hash)under the account’s domain, as Coinbase Smart Wallet does? Or ERC-7739, which is readable but heavier and still Draft? - Q9. Revocation. Are
executor, per-accountrevoke(D)on the namedMigrationCheck, and a short deadline enough? - Q10. Registration digests. Should they also exclude the chain ID, so that one slow PQ signature serves every chain?
- Q11. The event. Is an on-chain completion event useful to anyone besides the migrating wallet?
- Q12. Canonical addresses. Should
MigratorandMigrationCheckstay specified by behavior, with a canonical deployment recommended, or be fixed as exact bytecode at fixed addresses? - Q13. What signers can see.
initDatais opaque to signers. Should the ERC fix an authority-list prefix forinitData, or add anauthoritiesHashtoMigration, so that wallets can display what they install?
EIP-8298 Security Considerations
The draft text that was here is merged, with review edits, in ethereum/EIPs#12564.