From 157372295bb5384bc1bdcdc16d01bb9afa8b64a2 Mon Sep 17 00:00:00 2001 From: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:12:16 +0300 Subject: [PATCH 01/15] Specify the canonical paymaster implementation --- EIPS/eip-8141.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-8141.md b/EIPS/eip-8141.md index e1b8afc28dd11f..6332e9bc64f5e6 100644 --- a/EIPS/eip-8141.md +++ b/EIPS/eip-8141.md @@ -865,9 +865,76 @@ 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 deployment. Many instances may be deployed - the expected model is one instance per sponsor. For public mempool purposes, a paymaster instance is considered canonical if and only if its runtime code hash equals the canonical paymaster code hash of the active fork. The runtime code and its hash are listed at the end of this section; a new canonical version is introduced by pinning a new code hash, and 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. + +| 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. + +**Payment authorization.** The sponsor's authorization is a protocol-validated `SECP256K1` signature entry at signature index 1 with `signer` equal to the instance's stored signer and empty `msg` - that is, a signature over `compute_sig_hash(tx)`. Because the canonical signature hash covers the chain id, the sender and its nonce, and every frame including the `pay` frame's own target, one authorization is valid for exactly one transaction against exactly one instance on exactly one chain, and a fee change requires a fresh authorization. Because `compute_sig_hash` elides only the signature bytes of empty-`msg` entries, the authorization commits to the signer address at index 1 without circularity. + +The protocol has already verified this signature before any frame executes, so the validation path performs no signature recovery: it checks that the entry at index 1 names the stored signer, then calls `APPROVE(APPROVE_PAYMENT)`. Balance sufficiency, the sender-approval precondition, nonce increment, and the maximum-cost charge are all enforced by `APPROVE` itself. Since `APPROVE_PAYMENT` requires `sender_approved == true`, a canonical paymaster `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) | caller is `signer`; no action pending | +| `0x02` | initiate rotation | new signer (non-zero) | caller is `signer`; no action pending | +| `0x03` | cancel pending action | - | caller is `signer` | +| `0x04` | finalize matured action | - | anyone, once `block.timestamp >= slot 2` | + +Finalizing a withdrawal sends the amount to `signer` and reverts if the balance is insufficient (the sponsor cancels and re-initiates a smaller amount). Finalizing a rotation writes slot 3 into slot 0. Both clear the pending state before acting. + +**Runtime behavior** (normative; the assembled bytecode and its code hash will be added to this section): + +```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) != SECP256K1: REVERT + if SIGPARAM(resolved_signer, 1) != SLOAD(0): REVERT + if SIGPARAM(msg, 1) != 0: REVERT # must sign the canonical sig hash + APPROVE(APPROVE_PAYMENT) + +op = CALLDATA[0] +if op == 0x01: # initiate withdrawal + require CALLER == SLOAD(0) 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 CALLER == SLOAD(0) 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 CALLER == SLOAD(0) + 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 +``` + +**Gas.** The validation path costs approximately 2,150 gas (three `SIGPARAM` reads, one cold `SLOAD`, dispatch, and `APPROVE`). A canonical `pay` frame `gas_limit` of 15,000 is sufficient with wide margin. + +**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 runtime code match and apply the paymaster-specific accounting and revalidation rules in this section. @@ -1228,6 +1295,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 key compromise beyond the delay window. A compromised signer key can authorize payments - the exit the timelock cannot guard - until a rotation initiated with the same key matures. 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; this is bounded and self-inflicted. + ### 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. From 0b89763cfa855d8cc011051931fd87044ccfa897 Mon Sep 17 00:00:00 2001 From: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:18:29 +0300 Subject: [PATCH 02/15] Harmonize canonical recognition to the pinned code hash; fix tense --- EIPS/eip-8141.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-8141.md b/EIPS/eip-8141.md index 6332e9bc64f5e6..2d7f4b303bc70f 100644 --- a/EIPS/eip-8141.md +++ b/EIPS/eip-8141.md @@ -793,7 +793,7 @@ 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 @@ -865,7 +865,7 @@ We address this conflict in two ways: ##### Canonical paymaster -The canonical paymaster is not a singleton deployment. Many instances may be deployed - the expected model is one instance per sponsor. For public mempool purposes, a paymaster instance is considered canonical if and only if its runtime code hash equals the canonical paymaster code hash of the active fork. The runtime code and its hash are listed at the end of this section; a new canonical version is introduced by pinning a new code hash, and versions unpinned at a fork boundary demote to non-canonical. +The canonical paymaster is not a singleton deployment. Many instances may be deployed - the expected model is one instance per sponsor. For public mempool purposes, a paymaster instance is considered canonical if and only if its runtime code hash equals the canonical paymaster code hash of the active fork. The runtime code and its hash will be listed in this section; a new canonical version is introduced by pinning a new code hash, and versions unpinned at a fork boundary demote to non-canonical. **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. @@ -936,7 +936,7 @@ REVERT **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 runtime code match and apply the paymaster-specific accounting and revalidation rules in this section. +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. @@ -973,7 +973,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. From 567df4bb5a43cebb622ae8cab72bcf17c60452de Mon Sep 17 00:00:00 2001 From: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:49:55 +0300 Subject: [PATCH 03/15] Accept any protocol-verified signature scheme for payment authorization --- EIPS/eip-8141.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-8141.md b/EIPS/eip-8141.md index 2d7f4b303bc70f..532ad72a89b61f 100644 --- a/EIPS/eip-8141.md +++ b/EIPS/eip-8141.md @@ -878,9 +878,9 @@ The canonical paymaster is not a singleton deployment. Many instances may be dep 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. -**Payment authorization.** The sponsor's authorization is a protocol-validated `SECP256K1` signature entry at signature index 1 with `signer` equal to the instance's stored signer and empty `msg` - that is, a signature over `compute_sig_hash(tx)`. Because the canonical signature hash covers the chain id, the sender and its nonce, and every frame including the `pay` frame's own target, one authorization is valid for exactly one transaction against exactly one instance on exactly one chain, and a fee change requires a fresh authorization. Because `compute_sig_hash` elides only the signature bytes of empty-`msg` entries, the authorization commits to the signer address at index 1 without circularity. +**Payment authorization.** The sponsor's authorization is a protocol-verified signature entry (`SECP256K1` or `P256`) at signature index 1 with `signer` equal to the instance's stored signer and empty `msg` - that is, a signature over `compute_sig_hash(tx)`. Because the canonical signature hash covers the chain id, the sender and its nonce, and every frame including the `pay` frame's own target, one authorization is valid for exactly one transaction against exactly one instance on exactly one chain, and a fee change requires a fresh authorization. Because `compute_sig_hash` elides only the signature bytes of empty-`msg` entries, the authorization commits to the signer address at index 1 without circularity. -The protocol has already verified this signature before any frame executes, so the validation path performs no signature recovery: it checks that the entry at index 1 names the stored signer, then calls `APPROVE(APPROVE_PAYMENT)`. Balance sufficiency, the sender-approval precondition, nonce increment, and the maximum-cost charge are all enforced by `APPROVE` itself. Since `APPROVE_PAYMENT` requires `sender_approved == true`, a canonical paymaster `pay` frame can only follow an approving frame. +`ARBITRARY` entries are excluded: they are structurally checked but not cryptographically verified by the protocol. A future protocol-verified scheme is adopted by pinning a new canonical version at the fork that introduces the scheme - the same cadence for both changes. The protocol has already verified this signature before any frame executes, so the validation path performs no signature recovery: it checks that the entry at index 1 names the stored signer, then calls `APPROVE(APPROVE_PAYMENT)`. Balance sufficiency, the sender-approval precondition, nonce increment, and the maximum-cost charge are all enforced by `APPROVE` itself. Since `APPROVE_PAYMENT` requires `sender_approved == true`, a canonical paymaster `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. @@ -903,7 +903,7 @@ if CALLVALUE > 0: REVERT # admin ops are non-payable if CALLDATASIZE == 0: # validation path (pay frame) - if SIGPARAM(scheme, 1) != SECP256K1: REVERT + if SIGPARAM(scheme, 1) not in (SECP256K1, P256): REVERT if SIGPARAM(resolved_signer, 1) != SLOAD(0): REVERT if SIGPARAM(msg, 1) != 0: REVERT # must sign the canonical sig hash APPROVE(APPROVE_PAYMENT) From d88ca424063d781af7c7d9a40e177025c77ff2e7 Mon Sep 17 00:00:00 2001 From: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:51:10 +0300 Subject: [PATCH 04/15] Make payment authorization scheme-agnostic: exclude only ARBITRARY --- EIPS/eip-8141.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-8141.md b/EIPS/eip-8141.md index 532ad72a89b61f..e0bc4ab2fa0d83 100644 --- a/EIPS/eip-8141.md +++ b/EIPS/eip-8141.md @@ -878,9 +878,9 @@ The canonical paymaster is not a singleton deployment. Many instances may be dep 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. -**Payment authorization.** The sponsor's authorization is a protocol-verified signature entry (`SECP256K1` or `P256`) at signature index 1 with `signer` equal to the instance's stored signer and empty `msg` - that is, a signature over `compute_sig_hash(tx)`. Because the canonical signature hash covers the chain id, the sender and its nonce, and every frame including the `pay` frame's own target, one authorization is valid for exactly one transaction against exactly one instance on exactly one chain, and a fee change requires a fresh authorization. Because `compute_sig_hash` elides only the signature bytes of empty-`msg` entries, the authorization commits to the signer address at index 1 without circularity. +**Payment authorization.** The sponsor's authorization is a protocol-verified signature entry - any scheme except `ARBITRARY` - at signature index 1 with `signer` equal to the instance's stored signer and empty `msg` - that is, a signature over `compute_sig_hash(tx)`. Because the canonical signature hash covers the chain id, the sender and its nonce, and every frame including the `pay` frame's own target, one authorization is valid for exactly one transaction against exactly one instance on exactly one chain, and a fee change requires a fresh authorization. Because `compute_sig_hash` elides only the signature bytes of empty-`msg` entries, the authorization commits to the signer address at index 1 without circularity. -`ARBITRARY` entries are excluded: they are structurally checked but not cryptographically verified by the protocol. A future protocol-verified scheme is adopted by pinning a new canonical version at the fork that introduces the scheme - the same cadence for both changes. The protocol has already verified this signature before any frame executes, so the validation path performs no signature recovery: it checks that the entry at index 1 names the stored signer, then calls `APPROVE(APPROVE_PAYMENT)`. Balance sufficiency, the sender-approval precondition, nonce increment, and the maximum-cost charge are all enforced by `APPROVE` itself. Since `APPROVE_PAYMENT` requires `sender_approved == true`, a canonical paymaster `pay` frame can only follow an approving frame. +`ARBITRARY` entries are excluded because they are structurally checked but not cryptographically verified by the protocol; every other scheme, present or future, is accepted the moment the protocol verifies it - the paymaster names no curve, so signature-scheme agility (including post-quantum schemes) is inherited from the signature list with no new canonical version. The protocol has already verified this signature before any frame executes, so the validation path performs no signature recovery: it checks that the entry at index 1 names the stored signer, then calls `APPROVE(APPROVE_PAYMENT)`. Balance sufficiency, the sender-approval precondition, nonce increment, and the maximum-cost charge are all enforced by `APPROVE` itself. Since `APPROVE_PAYMENT` requires `sender_approved == true`, a canonical paymaster `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. @@ -903,7 +903,7 @@ if CALLVALUE > 0: REVERT # admin ops are non-payable if CALLDATASIZE == 0: # validation path (pay frame) - if SIGPARAM(scheme, 1) not in (SECP256K1, P256): REVERT + 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) From 855a8da1245a82d0451145f23dee9b6bdff6c4d2 Mon Sep 17 00:00:00 2001 From: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:00:09 +0300 Subject: [PATCH 05/15] Scheme-agnostic administration: authorize by caller or signer-signed entry --- EIPS/eip-8141.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/EIPS/eip-8141.md b/EIPS/eip-8141.md index e0bc4ab2fa0d83..f7531098571c83 100644 --- a/EIPS/eip-8141.md +++ b/EIPS/eip-8141.md @@ -888,12 +888,12 @@ At most one pending action exists at a time: a withdrawal (slot 1 non-zero) or a | `op` | Operation | Argument | Authorization | |------|-----------|----------|---------------| -| `0x01` | initiate withdrawal | amount (non-zero) | caller is `signer`; no action pending | -| `0x02` | initiate rotation | new signer (non-zero) | caller is `signer`; no action pending | -| `0x03` | cancel pending action | - | caller is `signer` | +| `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` | -Finalizing a withdrawal sends the amount to `signer` and reverts if the balance is insufficient (the sponsor cancels and re-initiates a smaller amount). Finalizing a rotation writes slot 3 into slot 0. Both clear the pending state before acting. +An operation is authorized by `signer` when the caller is `signer`, or - inside a frame transaction - when the entry at signature index 1 is protocol-verified, names `signer`, and has empty `msg`, exactly as in the payment path. The caller check keeps the plain-transaction path for secp256k1 signers; the signature check makes administration available to signers of every protocol-verified scheme, whose derived addresses cannot originate calls. The signature covers the canonical transaction hash, so an administrative authorization binds the entire transaction and cannot be replayed. Finalizing a withdrawal sends the amount to `signer` and reverts if the balance is insufficient (the sponsor cancels and re-initiates a smaller amount). Finalizing a rotation writes slot 3 into slot 0. Both clear the pending state before acting. **Runtime behavior** (normative; the assembled bytecode and its code hash will be added to this section): @@ -908,17 +908,23 @@ if CALLDATASIZE == 0: # validation path (pay frame) 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 CALLER == SLOAD(0) and SLOAD(2) == 0 + 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 CALLER == SLOAD(0) and SLOAD(2) == 0 + 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 CALLER == SLOAD(0) + require authorized() SSTORE(1, 0); SSTORE(2, 0); SSTORE(3, 0); STOP if op == 0x04: # finalize require SLOAD(2) != 0 and TIMESTAMP >= SLOAD(2) From 9544155c4fd5d88a00f999567430d75bf11ab193 Mon Sep 17 00:00:00 2001 From: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:10:49 +0300 Subject: [PATCH 06/15] Specify instance initialization; sharpen key-compromise security considerations --- EIPS/eip-8141.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-8141.md b/EIPS/eip-8141.md index f7531098571c83..06a82bf9585430 100644 --- a/EIPS/eip-8141.md +++ b/EIPS/eip-8141.md @@ -878,6 +878,8 @@ The canonical paymaster is not a singleton deployment. Many instances may be dep 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` cannot be embedded in code and is instead written to slot `0` by the deployment initialization code, which then returns the canonical runtime (the standard constructor-writes-storage, returns-runtime pattern). An instance is therefore fully described by its runtime code hash and its slot `0`; the initialization code is not part of recognition. An instance whose slot `0` is zero authorizes nothing (no protocol-verified signature resolves to the zero address), so a mis-deployed instance is inert rather than open. + **Payment authorization.** The sponsor's authorization is a protocol-verified signature entry - any scheme except `ARBITRARY` - at signature index 1 with `signer` equal to the instance's stored signer and empty `msg` - that is, a signature over `compute_sig_hash(tx)`. Because the canonical signature hash covers the chain id, the sender and its nonce, and every frame including the `pay` frame's own target, one authorization is valid for exactly one transaction against exactly one instance on exactly one chain, and a fee change requires a fresh authorization. Because `compute_sig_hash` elides only the signature bytes of empty-`msg` entries, the authorization commits to the signer address at index 1 without circularity. `ARBITRARY` entries are excluded because they are structurally checked but not cryptographically verified by the protocol; every other scheme, present or future, is accepted the moment the protocol verifies it - the paymaster names no curve, so signature-scheme agility (including post-quantum schemes) is inherited from the signature list with no new canonical version. The protocol has already verified this signature before any frame executes, so the validation path performs no signature recovery: it checks that the entry at index 1 names the stored signer, then calls `APPROVE(APPROVE_PAYMENT)`. Balance sufficiency, the sender-approval precondition, nonce increment, and the maximum-cost charge are all enforced by `APPROVE` itself. Since `APPROVE_PAYMENT` requires `sender_approved == true`, a canonical paymaster `pay` frame can only follow an approving frame. @@ -893,7 +895,7 @@ At most one pending action exists at a time: a withdrawal (slot 1 non-zero) or a | `0x03` | cancel pending action | - | authorized by `signer` | | `0x04` | finalize matured action | - | anyone, once `block.timestamp >= slot 2` | -An operation is authorized by `signer` when the caller is `signer`, or - inside a frame transaction - when the entry at signature index 1 is protocol-verified, names `signer`, and has empty `msg`, exactly as in the payment path. The caller check keeps the plain-transaction path for secp256k1 signers; the signature check makes administration available to signers of every protocol-verified scheme, whose derived addresses cannot originate calls. The signature covers the canonical transaction hash, so an administrative authorization binds the entire transaction and cannot be replayed. Finalizing a withdrawal sends the amount to `signer` and reverts if the balance is insufficient (the sponsor cancels and re-initiates a smaller amount). Finalizing a rotation writes slot 3 into slot 0. Both clear the pending state before acting. +An operation is authorized by `signer` when the caller is `signer`, or - inside a frame transaction - when the entry at signature index 1 is protocol-verified, names `signer`, and has empty `msg`, exactly as in the payment path. The caller check keeps the plain-transaction path for secp256k1 signers; the signature check makes administration available to signers of every protocol-verified scheme, whose derived addresses cannot originate calls. The signature covers the canonical transaction hash, so an administrative authorization binds the entire transaction and cannot be replayed. Finalizing a withdrawal sends the amount to `signer` and reverts if the balance is insufficient (the sponsor cancels and re-initiates a smaller amount) or if `signer` cannot receive the transfer; a sponsor using a contract signer must ensure it accepts the withdrawal. Finalizing a rotation writes slot 3 into slot 0. Both clear the pending state before acting. **Runtime behavior** (normative; the assembled bytecode and its code hash will be added to this section): @@ -1303,7 +1305,7 @@ In general, it can be assumed that handling of frame transactions imposes simila ### Canonical Paymaster Key Compromise -The canonical paymaster protects the mempool from the sponsor; it does not protect the sponsor from key compromise beyond the delay window. A compromised signer key can authorize payments - the exit the timelock cannot guard - until a rotation initiated with the same key matures. 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; this is bounded and self-inflicted. +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 From 7a9e0badcfb640dc2d97217eced3ee93d1698981 Mon Sep 17 00:00:00 2001 From: Stavros Vlachakis <89769224+svlachakis@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:14:21 +0300 Subject: [PATCH 07/15] Add signer to the revalidation trigger set (rotation invalidates old-signer txs) --- EIPS/eip-8141.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-8141.md b/EIPS/eip-8141.md index 06a82bf9585430..b757ace12214e0 100644 --- a/EIPS/eip-8141.md +++ b/EIPS/eip-8141.md @@ -999,7 +999,7 @@ When local resource limits are reached, a node should evict in this order: first #### 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 From 7a5ad30753a7aeb6a4f5361efc61853518a510c6 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:25:00 +0100 Subject: [PATCH 08/15] EIP-8141: add canonical paymaster reference bytecode, code hash, and measured gas Insert the assembled 355-byte runtime bytecode and its per-fork keccak256 code hash into the canonical paymaster section, replace the ~2,150 gas estimate with the measured validation-path cost, and add the reference artifacts asset file. --- EIPS/eip-8141.md | 14 +++++++++++--- assets/eip-8141/canonical-paymaster.md | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 assets/eip-8141/canonical-paymaster.md diff --git a/EIPS/eip-8141.md b/EIPS/eip-8141.md index b757ace12214e0..81d74fdd5ea996 100644 --- a/EIPS/eip-8141.md +++ b/EIPS/eip-8141.md @@ -865,7 +865,7 @@ We address this conflict in two ways: ##### Canonical paymaster -The canonical paymaster is not a singleton deployment. Many instances may be deployed - the expected model is one instance per sponsor. For public mempool purposes, a paymaster instance is considered canonical if and only if its runtime code hash equals the canonical paymaster code hash of the active fork. The runtime code and its hash will be listed in this section; a new canonical version is introduced by pinning a new code hash, and versions unpinned at a fork boundary demote to non-canonical. +The canonical paymaster is not a singleton deployment. Many instances may be deployed - the expected model is one instance per sponsor. For public mempool purposes, a paymaster instance is considered canonical if and only if its runtime code hash equals the canonical paymaster code hash of the active fork. The runtime code and its hash are listed in this section; a new canonical version is introduced by pinning a new code hash, and versions unpinned at a fork boundary demote to non-canonical. **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. @@ -897,7 +897,7 @@ At most one pending action exists at a time: a withdrawal (slot 1 non-zero) or a An operation is authorized by `signer` when the caller is `signer`, or - inside a frame transaction - when the entry at signature index 1 is protocol-verified, names `signer`, and has empty `msg`, exactly as in the payment path. The caller check keeps the plain-transaction path for secp256k1 signers; the signature check makes administration available to signers of every protocol-verified scheme, whose derived addresses cannot originate calls. The signature covers the canonical transaction hash, so an administrative authorization binds the entire transaction and cannot be replayed. Finalizing a withdrawal sends the amount to `signer` and reverts if the balance is insufficient (the sponsor cancels and re-initiates a smaller amount) or if `signer` cannot receive the transfer; a sponsor using a contract signer must ensure it accepts the withdrawal. Finalizing a rotation writes slot 3 into slot 0. Both clear the pending state before acting. -**Runtime behavior** (normative; the assembled bytecode and its code hash will be added to this section): +**Runtime behavior** (normative): ```python if CALLVALUE > 0: @@ -940,7 +940,15 @@ if op == 0x04: # finalize REVERT ``` -**Gas.** The validation path costs approximately 2,150 gas (three `SIGPARAM` reads, one cold `SLOAD`, dispatch, and `APPROVE`). A canonical `pay` frame `gas_limit` of 15,000 is sufficient with wide margin. +The assembled runtime bytecode (355 bytes) is: + +```text +0x3461002e57366100355760016001b41561005a575f6001b45f54141561005a5760026001b461005a5760015f5faa5b3661005a57005b5f3560f81c8060011461005e57806002146100a557806003146100ec57600414610123575b5f5ffd5b50335f54146100875760016001b41561005a575f6001b45f54141561005a5760026001b461005a575b60025461005a57600135801561005a57600155426201518001600255005b50335f54146100ce5760016001b41561005a575f6001b45f54141561005a5760026001b461005a575b60025461005a57600135801561005a57600355426201518001600255005b50335f54146101155760016001b41561005a575f6001b45f54141561005a5760026001b461005a575b5f6001555f6002555f600355005b600254801561005a57421061005a576001548015610153575f6001555f6002555f5f5f5f845f545af11561005a57005b506003545f555f6003555f60025500 +``` + +Its per-fork `keccak256` code hash is `0xda42f0d11838c4c0c3129b8b8e93e9718127ad6b315e517e1088125707c4d45c`. The bytecode and hash are reproducible from the reference assembler and are also listed in `assets/eip-8141/canonical-paymaster.md`. + +**Gas.** The validation path costs a single cold `SLOAD` of slot 0 plus dispatch, three `SIGPARAM` reads, and `APPROVE` — roughly 3,100 gas at the state-access prices of this fork (dominated by the cold `SLOAD`; the reference implementation measures 3,110). A canonical `pay` frame's `gas_limit` of 15,000 is sufficient with wide margin, and nodes MAY reject canonical `pay` frames with a `gas_limit` above that bound 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. diff --git a/assets/eip-8141/canonical-paymaster.md b/assets/eip-8141/canonical-paymaster.md new file mode 100644 index 00000000000000..c892c42f3426b6 --- /dev/null +++ b/assets/eip-8141/canonical-paymaster.md @@ -0,0 +1,15 @@ +# Canonical paymaster reference artifacts + +Assembled runtime bytecode (355 bytes): + +```text +0x3461002e57366100355760016001b41561005a575f6001b45f54141561005a5760026001b461005a5760015f5faa5b3661005a57005b5f3560f81c8060011461005e57806002146100a557806003146100ec57600414610123575b5f5ffd5b50335f54146100875760016001b41561005a575f6001b45f54141561005a5760026001b461005a575b60025461005a57600135801561005a57600155426201518001600255005b50335f54146100ce5760016001b41561005a575f6001b45f54141561005a5760026001b461005a575b60025461005a57600135801561005a57600355426201518001600255005b50335f54146101155760016001b41561005a575f6001b45f54141561005a5760026001b461005a575b5f6001555f6002555f600355005b600254801561005a57421061005a576001548015610153575f6001555f6002555f5f5f5f845f545af11561005a57005b506003545f555f6003555f60025500 +``` + +Per-fork `keccak256` code hash: + +```text +0xda42f0d11838c4c0c3129b8b8e93e9718127ad6b315e517e1088125707c4d45c +``` + +Both values are reproducible byte-for-byte from the reference assembler (two-pass label resolution, self-contained keccak-256). From 40f9b5128d9cdf92d6f2e14554e8c9bfb8b7ba3a Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:33:47 +0100 Subject: [PATCH 09/15] EIP-8141: add annotated assembly for the canonical paymaster reference Present the reference contract as annotated EVM assembly (mnemonics, labels, per-block comments) alongside the compiled bytecode and code hash, matching the house style of the system-contract EIPs. The listing assembles byte-for-byte to the pinned runtime bytecode and hash. --- EIPS/eip-8141.md | 2 +- assets/eip-8141/canonical-paymaster.md | 297 ++++++++++++++++++++++++- 2 files changed, 295 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-8141.md b/EIPS/eip-8141.md index 81d74fdd5ea996..14f30fe2256b41 100644 --- a/EIPS/eip-8141.md +++ b/EIPS/eip-8141.md @@ -946,7 +946,7 @@ The assembled runtime bytecode (355 bytes) is: 0x3461002e57366100355760016001b41561005a575f6001b45f54141561005a5760026001b461005a5760015f5faa5b3661005a57005b5f3560f81c8060011461005e57806002146100a557806003146100ec57600414610123575b5f5ffd5b50335f54146100875760016001b41561005a575f6001b45f54141561005a5760026001b461005a575b60025461005a57600135801561005a57600155426201518001600255005b50335f54146100ce5760016001b41561005a575f6001b45f54141561005a5760026001b461005a575b60025461005a57600135801561005a57600355426201518001600255005b50335f54146101155760016001b41561005a575f6001b45f54141561005a5760026001b461005a575b5f6001555f6002555f600355005b600254801561005a57421061005a576001548015610153575f6001555f6002555f5f5f5f845f545af11561005a57005b506003545f555f6003555f60025500 ``` -Its per-fork `keccak256` code hash is `0xda42f0d11838c4c0c3129b8b8e93e9718127ad6b315e517e1088125707c4d45c`. The bytecode and hash are reproducible from the reference assembler and are also listed in `assets/eip-8141/canonical-paymaster.md`. +Its per-fork `keccak256` code hash is `0xda42f0d11838c4c0c3129b8b8e93e9718127ad6b315e517e1088125707c4d45c`. An annotated assembly listing that assembles byte-for-byte to this bytecode, together with the bytecode and hash, is given in [`assets/eip-8141/canonical-paymaster.md`](../assets/eip-8141/canonical-paymaster.md); all three are reproducible from the reference assembler. **Gas.** The validation path costs a single cold `SLOAD` of slot 0 plus dispatch, three `SIGPARAM` reads, and `APPROVE` — roughly 3,100 gas at the state-access prices of this fork (dominated by the cold `SLOAD`; the reference implementation measures 3,110). A canonical `pay` frame's `gas_limit` of 15,000 is sufficient with wide margin, and nodes MAY reject canonical `pay` frames with a `gas_limit` above that bound as non-canonical usage. diff --git a/assets/eip-8141/canonical-paymaster.md b/assets/eip-8141/canonical-paymaster.md index c892c42f3426b6..85f325dd00ac36 100644 --- a/assets/eip-8141/canonical-paymaster.md +++ b/assets/eip-8141/canonical-paymaster.md @@ -1,15 +1,306 @@ # Canonical paymaster reference artifacts -Assembled runtime bytecode (355 bytes): +## Annotated assembly + +Storage layout: `slot0 = signer`, `slot1 = pending_withdrawal_amount`, +`slot2 = pending_unlock_time`, `slot3 = pending_new_signer`. `DELAY = 86400`. +Jump targets are assembled as fixed-width `push2