Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 95 additions & 8 deletions EIPS/eip-8141.md
Original file line number Diff line number Diff line change
Expand Up @@ -793,11 +793,11 @@ A node MUST drop a frame transaction from the public mempool if it contains an `

#### Canonical Paymaster Exception

The generic validation trace and opcode rules below apply to all frames in the validation prefix except a `pay` frame whose target runtime code exactly matches the canonical paymaster implementation. The canonical paymaster implementation is explicitly designed to be safe for public mempool use and is therefore admitted by code match, successful `APPROVE(APPROVE_PAYMENT)`, and the paymaster accounting rules in this section, rather than by requiring it to satisfy each generic validation rule individually.
The generic validation trace and opcode rules below apply to all frames in the validation prefix except a `pay` frame whose target's runtime code hash equals the canonical paymaster code hash of the active fork. The canonical paymaster implementation is explicitly designed to be safe for public mempool use and is therefore admitted by that code-hash match, successful `APPROVE(APPROVE_PAYMENT)`, and the paymaster accounting rules in this section, rather than by requiring it to satisfy each generic validation rule individually.

#### Direct Evaluation of Protocol-Defined Frames

Three frame species in the validation prefix have fully protocol-defined semantics, leaving no deployed code whose behavior a node would need to discover by execution: a frame whose resolved target has the empty code hash (default code), an expiry verifier frame whose runtime code at `EXPIRY_VERIFIER` matches the canonical expiry verifier code, and a `pay` frame admitted by canonical paymaster code match per the previous section.
Three frame species in the validation prefix have fully protocol-defined semantics, leaving no deployed code whose behavior a node would need to discover by execution: a frame whose resolved target has the empty code hash (default code), an expiry verifier frame whose runtime code at `EXPIRY_VERIFIER` matches the canonical expiry verifier code, and a `pay` frame whose target's runtime code hash equals the canonical paymaster code hash of the active fork, per the previous section.

When every frame in the validation prefix is one of these, direct evaluation of the protocol-defined semantics is equivalent to simulation, and a node MAY use it to satisfy the validation requirements below. Direct evaluation MUST apply the same limits as simulation: signature validation and the evaluated frames' work count against `MAX_VERIFY_GAS`, and the paymaster accounting rules in this section apply unchanged.

Expand Down Expand Up @@ -865,11 +865,94 @@ We address this conflict in two ways:

##### Canonical paymaster

The canonical paymaster is not a singleton deployment. Many instances may be deployed. For public mempool purposes, a paymaster instance is considered canonical if and only if the runtime code at the `pay` frame target exactly matches the canonical paymaster implementation.
The canonical paymaster is not a singleton: many instances may be deployed, one per sponsor. An instance is canonical for public mempool purposes if and only if its runtime code hash equals the active fork's pinned canonical paymaster code hash (listed in this section). A new version is introduced by pinning a new hash; versions unpinned at a fork boundary demote to non-canonical.

The canonical paymaster in this draft authorizes with a single secp256k1 signer via `ecrecover`, does not support contract-signature schemes, and may change in later specifications, in which case a new canonical implementation version would be required.
**Storage layout.** The layout is normative per canonical version. Nodes read these slots directly for admission and revalidation; no EVM execution is required to compute solvency.

Because the canonical paymaster implementation is explicitly standardized to be safe for public mempool use, nodes do not need to apply the generic validation trace and opcode rules to that `pay` frame. Instead, they identify it by runtime code match and apply the paymaster-specific accounting and revalidation rules in this section.
| Slot | Content |
|------|---------|
| `0` | `signer` - the address whose signature authorizes payments |
| `1` | `pending_withdrawal_amount` - `0` when no withdrawal is pending |
| `2` | `pending_unlock_time` - timestamp at which the pending action matures; `0` when none |
| `3` | `pending_new_signer` - `0` when no rotation is pending |

At most one pending action exists at a time: a withdrawal (slot 1 non-zero) or a signer rotation (slot 3 non-zero); slot 2 is the shared maturity clock. `pending_withdrawal_amount(paymaster)` in the reservation formula below is slot 1.

**Deployment.** Recognition is on the *runtime* code hash, so every instance shares identical runtime bytes; the per-instance `signer` is written to slot `0` by initialization code that then returns the canonical runtime (the standard constructor-writes-storage pattern) and is not itself part of recognition. An instance is fully described by its runtime code hash and slot `0`. An instance whose slot `0` is zero authorizes nothing - no protocol-verified signature resolves to the zero address - so a mis-deployment is inert, not open.

**Payment authorization.** The sponsor's authorization is a protocol-verified signature entry (any scheme except `ARBITRARY`) at index 1, naming the stored `signer` with empty `msg` - a signature over `compute_sig_hash(tx)`. That hash covers the chain id, the sender and nonce, and every frame including the `pay` target, so one authorization is valid for exactly one transaction, instance, and chain, and a fee change requires a fresh one. Since `compute_sig_hash` elides only empty-`msg` signature bytes, the authorization commits to the index-1 signer without circularity.

`ARBITRARY` entries are excluded - structurally checked but not cryptographically verified; every other scheme is accepted the moment the protocol verifies it, so the paymaster names no curve and inherits signature-scheme agility, including post-quantum schemes, from the signature list with no new canonical version. Because verification happens before any frame executes, the validation path performs no recovery: it checks that index 1 names the stored signer, then calls `APPROVE(APPROVE_PAYMENT)`, which itself enforces balance sufficiency, the `sender_approved` precondition, nonce increment, and the maximum-cost charge - so a canonical `pay` frame can only follow an approving frame.

**Treasury rules.** Deposits are unrestricted: any plain value transfer is accepted. Ether leaves an instance in exactly two ways: the protocol's `APPROVE` charge, and a matured withdrawal. Signer rotation shares the withdrawal timelock, because payments are an exit the timelock cannot guard: an instantly rotated key could drain the balance through valid payments. `DELAY` is `86400` seconds in this version.

**Calldata interface.** The `pay` frame passes empty `data`. Administrative operations are invoked with `op (1 byte) || argument (32 bytes)`, mirroring the expiry verifier's raw-calldata convention:

| `op` | Operation | Argument | Authorization |
|------|-----------|----------|---------------|
| `0x01` | initiate withdrawal | amount (non-zero) | authorized by `signer`; no action pending |
| `0x02` | initiate rotation | new signer (non-zero) | authorized by `signer`; no action pending |
| `0x03` | cancel pending action | - | authorized by `signer` |
| `0x04` | finalize matured action | - | anyone, once `block.timestamp >= slot 2` |

`authorized()` accepts the direct caller or a signer-signed index-1 entry (the same check as the payment path): the caller route serves a plain secp256k1 signer, the signature route serves any protocol-verified scheme whose derived address cannot originate calls, and because that signature covers the canonical transaction hash it binds the whole transaction and cannot be replayed. Finalizing a withdrawal sends the amount to `signer`, reverting if the balance is insufficient (the sponsor cancels and re-initiates a smaller amount) or `signer` cannot receive it; finalizing a rotation writes slot 3 into slot 0. The withdrawal path clears its pending slots (1 and 2) before the external transfer; rotation has no external call, so its slot ordering is unobservable.

**Runtime behavior** (normative):

```python
if CALLVALUE > 0:
if CALLDATASIZE == 0: STOP # deposit
REVERT # admin ops are non-payable

if CALLDATASIZE == 0: # validation path (pay frame)
if SIGPARAM(scheme, 1) == ARBITRARY: REVERT
if SIGPARAM(resolved_signer, 1) != SLOAD(0): REVERT
if SIGPARAM(msg, 1) != 0: REVERT # must sign the canonical sig hash
APPROVE(APPROVE_PAYMENT)

def authorized(): # caller, or signer-signed frame-tx entry
if CALLER == SLOAD(0): return true
return SIGPARAM(scheme, 1) != ARBITRARY \
and SIGPARAM(resolved_signer, 1) == SLOAD(0) \
and SIGPARAM(msg, 1) == 0 # halts in a non-frame context: correct rejection

op = CALLDATA[0]
if op == 0x01: # initiate withdrawal
require authorized() and SLOAD(2) == 0
amount = CALLDATA[1:33]; require amount != 0
SSTORE(1, amount); SSTORE(2, TIMESTAMP + 86400); STOP
if op == 0x02: # initiate rotation
require authorized() and SLOAD(2) == 0
new_signer = CALLDATA[1:33]; require new_signer != 0
SSTORE(3, new_signer); SSTORE(2, TIMESTAMP + 86400); STOP
if op == 0x03: # cancel
require authorized()
SSTORE(1, 0); SSTORE(2, 0); SSTORE(3, 0); STOP
if op == 0x04: # finalize
require SLOAD(2) != 0 and TIMESTAMP >= SLOAD(2)
amount = SLOAD(1)
if amount != 0:
SSTORE(1, 0); SSTORE(2, 0)
require CALL(signer, amount) # state cleared before the send
else:
SSTORE(0, SLOAD(3)); SSTORE(3, 0); SSTORE(2, 0)
STOP
REVERT
```

The assembled runtime bytecode (355 bytes) is:

```text
0x3461002e57366100355760016001b41561005a575f6001b45f54141561005a5760026001b461005a5760015f5faa5b3661005a57005b5f3560f81c8060011461005e57806002146100a557806003146100ec57600414610123575b5f5ffd5b50335f54146100875760016001b41561005a575f6001b45f54141561005a5760026001b461005a575b60025461005a57600135801561005a57600155426201518001600255005b50335f54146100ce5760016001b41561005a575f6001b45f54141561005a5760026001b461005a575b60025461005a57600135801561005a57600355426201518001600255005b50335f54146101155760016001b41561005a575f6001b45f54141561005a5760026001b461005a575b5f6001555f6002555f600355005b600254801561005a57421061005a576001548015610153575f6001555f6002555f5f5f5f845f545af11561005a57005b506003545f555f6003555f60025500
```

Its per-fork `keccak256` code hash is `0xda42f0d11838c4c0c3129b8b8e93e9718127ad6b315e517e1088125707c4d45c`. An annotated assembly listing is given in the [canonical paymaster assets file](../assets/eip-8141/canonical-paymaster.md).

**Gas.** The validation path is dominated by a single cold `SLOAD` of slot 0, plus a small fixed overhead, so a canonical `pay` frame needs a `gas_limit` of at most 15,000 on any current schedule; nodes MAY reject canonical `pay` frames with a higher `gas_limit` as non-canonical usage.

**Tracked dependencies.** For revalidation purposes, the canonical paymaster's tracked state is its balance, its code, and storage slots 0 through 3. A finalized rotation changes slot 0 and therefore invalidates pending transactions authorized by the previous signer.

Because the canonical paymaster implementation is explicitly standardized to be safe for public mempool use, nodes do not need to apply the generic validation trace and opcode rules to that `pay` frame. Instead, they identify it by its code hash and apply the paymaster-specific accounting and revalidation rules in this section.

A transaction using a paymaster is eligible for public mempool propagation only if the `pay` frame targets a canonical paymaster instance and the node can reserve the maximum transaction cost against that paymaster.

Expand Down Expand Up @@ -906,7 +989,7 @@ available_paymaster_balance = state.balance(paymaster) - reserved_pending_cost(p
1. A transaction is received over the wire and the node decides whether to accept or reject it.
2. The node validates all protocol-validated signatures and structurally checks all `ARBITRARY` signatures. If any signature is malformed or invalid, reject.
3. The node analyzes the frame structure and determines the validation prefix. If the prefix is not one of the recognized prefixes, reject.
4. The node simulates the validation prefix and enforces the structural and trace rules above, except that a `pay` frame whose target runtime code exactly matches the canonical paymaster implementation is handled via the canonical paymaster exception and the paymaster-specific rules below.
4. The node simulates the validation prefix and enforces the structural and trace rules above, except that a `pay` frame whose target's runtime code hash equals the canonical paymaster code hash of the active fork is handled via the canonical paymaster exception and the paymaster-specific rules below.
5. The node records the sender storage slots read during validation. Calls into helper contracts do not create additional mutable-state dependencies unless they cause disallowed storage access under the trace rules above.
6. If a canonical paymaster instance is used, the node verifies paymaster solvency using the reservation rule above.
7. A node should keep at most one pending frame transaction per sender in the public mempool. A new transaction from the same sender MAY replace the existing one only if it uses the same nonce and satisfies the replacement rules below.
Expand All @@ -918,13 +1001,13 @@ Pending frame transactions are identified by `(sender, nonce)`. The sender's non

A replacement must be valid under all rules in this section and should only be accepted and propagated if it increases both `max_fee_per_gas` and `max_priority_fee_per_gas` by at least a node-configured minimum increment (10% is the conventional default). A blob-carrying frame transaction additionally follows the blob pool's replacement conventions for `max_fee_per_blob_gas`. A replacement may name a different payer than the transaction it replaces. The node should release the reservation or exposure held for the old payer and shift it to the new payer atomically with the replacement.

Reservation accounting applies to every payer, not only canonical paymasters. A node must not hold pending frame transactions whose summed maximum costs exceed the payer's balance, tracked per payer exactly as `reserved_pending_cost` is tracked for canonical paymaster instances. For canonical paymaster instances this is the reservation rule above. For any other payer it bounds aggregate exposure so that a single balance change cannot invalidate an unbounded set of pending transactions. The `MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER` cap continues to apply to `pay` frames whose target carries code that is not the canonical paymaster implementation; a `pay` frame whose target has the empty code hash (a default-code sponsor) is not a paymaster and is governed by the per-payer exposure rule alone.
Reservation accounting applies to every payer, not only canonical paymasters. A node must not hold pending frame transactions whose summed maximum costs exceed the payer's balance, tracked per payer exactly as `reserved_pending_cost` is tracked for canonical paymaster instances. For canonical paymaster instances this is the reservation rule above. For any other payer it bounds aggregate exposure so that a single balance change cannot invalidate an unbounded set of pending transactions. The `MAX_PENDING_TXS_USING_NON_CANONICAL_PAYMASTER` cap continues to apply to `pay` frames whose target's runtime code hash does not equal the canonical paymaster code hash of the active fork; a `pay` frame whose target has the empty code hash (a default-code sponsor) is not a paymaster and is governed by the per-payer exposure rule alone.

When local resource limits are reached, a node should evict in this order: first transactions already invalid against the current head, then transactions with the nearest expiry deadline, then transactions with the lowest effective priority fee. Evicted and replaced transactions must not be re-propagated. Re-admission requires receiving the transaction again. On any eviction or replacement the corresponding payer reservation or exposure is released.

#### Revalidation

When a new canonical block is accepted, the node removes any included frame transactions from the public mempool, updates paymaster reservations accordingly, and identifies the remaining pending transactions whose tracked dependencies were touched by the block. This includes at least transactions for the same sender, transactions whose recorded sender storage slots changed, transactions that reference a canonical paymaster instance whose balance, code, or delayed-withdrawal state changed, and transactions whose payer's balance or code changed. The node then re-simulates the validation prefix of only those affected transactions against the new head and evicts any transaction that no longer satisfies the public mempool rules.
When a new canonical block is accepted, the node removes any included frame transactions from the public mempool, updates paymaster reservations accordingly, and identifies the remaining pending transactions whose tracked dependencies were touched by the block. This includes at least transactions for the same sender, transactions whose recorded sender storage slots changed, transactions that reference a canonical paymaster instance whose balance, code, signer, or delayed-withdrawal state changed - equivalently, any of its tracked storage slots (0 through 3) or its balance or code - and transactions whose payer's balance or code changed. A finalized signer rotation changes slot 0 and so invalidates transactions authorized by the previous signer; this trigger is what evicts them. The node then re-simulates the validation prefix of only those affected transactions against the new head and evicts any transaction that no longer satisfies the public mempool rules.

#### Transaction origination

Expand Down Expand Up @@ -1228,6 +1311,10 @@ For deployment of the sender account in the first frame, the mempool enforces de

In general, it can be assumed that handling of frame transactions imposes similar restrictions as EIP-7702 on mempool relay, i.e. only a single transaction can be pending for an account that uses frame transactions.

### Canonical Paymaster Key Compromise

The canonical paymaster protects the mempool from the sponsor; it does not protect the sponsor from signer-key compromise. Payments are not timelocked, so an attacker holding the signer key can drain the balance immediately through self-sponsored transactions - the timelock guards only withdrawals and rotations, not the payment exit. Rotation is no remedy under compromise: the replacement is authorized by the same key, matures only after `DELAY`, and the attacker can `cancel` any competing rotation the owner initiates, so a single compromised key means total loss with no in-contract recovery. This is the deliberate cost of a minimal, single-signer canonical implementation; multi-key control, spend limits, and social recovery are the province of non-canonical wallet code layered above, not of the canonical mempool-safe tier. A separate, bounded effect: anyone may finalize a matured action, so a sponsor's own pending rotation, once mature, may be finalized by a third party, invalidating pending transactions authorized by the outgoing key.

### Execution Approval Authorizes All Subsequent Sender Frames

`sender_approved` is a single transaction-scoped flag. Once a frame grants `APPROVE_EXECUTION` (or `APPROVE_EXECUTION_AND_PAYMENT`), every subsequent `SENDER` frame executes with `caller` set to `tx.sender`, not only the frame the approving code inspected. The approval is not scoped to a particular frame or call target.
Expand Down
Loading
Loading