Skip to content
Draft
Changes from all commits
Commits
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
63 changes: 44 additions & 19 deletions EIPS/eip-8141.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ assert len(tx.sender) == 20
for sig in tx.signatures:
if sig.scheme in [SECP256K1, P256]:
assert len(sig.signer) == 20
elif sig.scheme == ARBITRARY:
assert len(sig.signer) == 0
else:
invalid_transaction()
if len(sig.msg) == 0:
Expand Down Expand Up @@ -147,23 +149,25 @@ for frame in tx.frames:

#### Transaction Signatures

The `signatures` list contains signatures that may be referenced by `VERIFY` frames and by ordinary EVM execution. Every signature in this list must validate successfully before any frame is executed. If any signature is malformed or invalid, the whole transaction is invalid.
The `signatures` list contains signatures that may be referenced by `VERIFY` frames and by ordinary EVM execution. Every protocol-validated signature in this list must validate successfully before any frame is executed. If any signature is malformed, or any protocol-validated signature is invalid, the whole transaction is invalid.

The `signatures` list is optional. Contracts may still ignore it entirely and perform bespoke signature verification inside frame execution.
The `signatures` list is optional. Contracts may still ignore it entirely and perform bespoke signature verification inside frame execution. Bespoke signature schemes must place their witness bytes in an `ARBITRARY` signature entry rather than in frame data when the witness signs the canonical transaction signature hash.

The raw `signature` byte strings are intentionally not introspectable by EVM code to allow future aggregation schemes. Contracts can inspect only the metadata of the signature list through transaction introspection.
The raw `signature` byte strings of protocol-validated schemes are intentionally not introspectable by EVM code to allow future aggregation schemes. Contracts can inspect only the metadata of protocol-validated signature entries through transaction introspection. The raw `signature` byte strings of `ARBITRARY` entries are introspectable by EVM code through `SIGPARAM`'s copy operation. This means that they cannot be aggregated in the future.

The `scheme` identifies how the raw `signature` bytes are interpreted.

| `scheme` | Name | `signature` encoding | Gas cost |
|------------|--------------|------------------------------------------------|----------|
| 0x0 | `SECP256K1` | `v (1 byte) || r (32 bytes) || s (32 bytes)` | 2800 |
| 0x1 | `P256` | `r || s || qx || qy` (all 32 bytes) | 6700 |
| 0x2..255 | reserved | reserved | reserved |
| 0x0 | `ARBITRARY` | arbitrary bytes | 0 |
| 0x1 | `SECP256K1` | `v (1 byte) || r (32 bytes) || s (32 bytes)` | 2800 |
| 0x2 | `P256` | `r || s || qx || qy` (all 32 bytes) | 6700 |
| 0x3..255 | reserved | reserved | reserved |

For `SECP256K1` and `P256`, the `signer` is a 20-byte Ethereum address.
For `ARBITRARY`, `signer` MUST be empty. The protocol does not assign an effective signer address to `ARBITRARY` entries.

The `msg` which message the signature authorizes:
The `msg` field defines which message the signature authorizes:

- if `len(msg) == 0`, the signature is signed over `compute_sig_hash(tx)`
- if `len(msg) == 32`, the signature is signed the explicit 32-byte digest `msg`
Expand All @@ -186,7 +190,7 @@ frame_receipt = [status, gas_used, logs]

#### Signature Hash

The canonical signature hash is defined such that any signature with empty `msg` will have its raw `signature` bytes elided:
The canonical signature hash is defined such that any signature with empty `msg` will have its raw `signature` bytes elided.

```python
def compute_sig_hash(tx: FrameTx) -> Hash:
Expand All @@ -198,13 +202,16 @@ def compute_sig_hash(tx: FrameTx) -> Hash:

#### Signature Validation

Every signature in the outer transaction object is validated before frame execution. The validation rules are:
Every signature in the outer transaction object is validated or structurally checked before frame execution. The validation rules are:

```python
SECP256K1 = 0x0
P256 = 0x1
ARBITRARY = 0x0
SECP256K1 = 0x1
P256 = 0x2

def effective_signer(sig) -> Address:
if sig.scheme == ARBITRARY:
return Address(0)
return sig.signer

def validate_signature(sig, tx, sig_hash) -> bool:
Expand Down Expand Up @@ -236,6 +243,9 @@ def validate_signature(sig, tx, sig_hash) -> bool:
return False
return P256VERIFY(msg, r, s, qx, qy)

elif sig.scheme == ARBITRARY:
return len(sig.signer) == 0

else:
return False
```
Expand Down Expand Up @@ -363,6 +373,8 @@ def signature_gas(sig):
return 2800
if sig.scheme == P256:
return 6700
if sig.scheme == ARBITRARY:
return 0
invalid_transaction()

signature_verification_cost = sum(signature_gas(sig) for sig in tx.signatures)
Expand Down Expand Up @@ -515,20 +527,29 @@ Notes:

#### `SIGPARAM` Instruction (`0xb4`)

This instruction gives access to signature-scoped metadata. The raw `signature` bytes are intentionally not accessible from the EVM. The gas cost of this operation is `2`. It takes two values from the stack, `signatureIndex` on top and `param` second from top.
This instruction gives access to signature-scoped metadata. The raw `signature` bytes of protocol-validated schemes are intentionally not accessible from the EVM. `ARBITRARY` signature bytes are validated during EVM execution and therefore can be accessed with the copy operation.

For `param` values `0x00` through `0x03`, the gas cost of this operation is `2`. It takes two values from the stack, `signatureIndex` on top and `param` second from top, and returns the associated value to the stack.

| `param` | `signatureIndex` | Return value |
|---------|------------------|--------------------|
| 0x00 | signatureIndex | effective signer address |
| 0x01 | signatureIndex | `scheme` |
| 0x02 | signatureIndex | `msg` |
| 0x03 | signatureIndex | `len(signature)` |
For `param == 0x04`, this instruction copies bytes from an `ARBITRARY` signature's raw `signature` bytes into memory. Its gas cost is calculated exactly as for `CALLDATACOPY`, including the fixed cost of 3, the per-word copy cost, and the standard EVM memory expansion cost. It takes five values from the stack: `signatureIndex` on top, `param` second from top, followed by `length`, `dataOffset`, and `memOffset`. No stack output value is produced.

| `param` | `signatureIndex` | Behavior |
|---------|------------------|--------------------------------------|
| 0x00 | signatureIndex | returns effective signer address |
| 0x01 | signatureIndex | returns `scheme` |
| 0x02 | signatureIndex | returns `msg` |
| 0x03 | signatureIndex | returns `len(signature)` |
| 0x04 | signatureIndex | copies `ARBITRARY` signature bytes to memory |

Notes:

- Undefined `param` values result in an exceptional halt.
- Out-of-bounds access for `signatureIndex` results in an exceptional halt.

For `ARBITRARY` signature entries, requesting effective signer address results in exceptional halt

For `param == 0x04`, if the referenced signature entry's `scheme` is not `ARBITRARY`, an exceptional halt occurs. The operation semantics match `CALLDATACOPY`, copying `length` bytes from the chosen signature's raw `signature` bytes, starting at the given byte `dataOffset`, into a memory region starting at `memOffset`. Bytes beyond the end of the signature are copied as zeroes.

### Mempool

The transaction mempool must carefully handle frame transactions, as a naive implementation could introduce denial-of-service vulnerabilities. The fundamental goal of the public mempool rules is to avoid allowing an arbitrary number of transactions to be invalidated by a single environmental change or state modification. Beyond this, the rules also aim to minimize the amount of work needed to complete the initial validation phase of a transaction before an acceptance decision can be made.
Expand Down Expand Up @@ -747,7 +768,7 @@ available_paymaster_balance = state.balance(paymaster) - reserved_pending_cost(p
#### Acceptance Algorithm

1. A transaction is received over the wire and the node decides whether to accept or reject it.
2. The node validates all signatures. If any signature is malformed or invalid, reject.
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.
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.
Expand Down Expand Up @@ -1023,6 +1044,10 @@ Because `tx.sender` is explicit in the transaction envelope, an attacker can sub

`FRAMEPARAM`, `FRAMEDATALOAD`, and `FRAMEDATACOPY` allow validation code to inspect other frames, including later `SENDER` frames and their `value`s. As a result, paymasters and other `VERIFY` frames can observe user operation parameters before approval and may condition their behavior on that information. Users should therefore treat non-`VERIFY` frame parameters and data as visible to validation logic and should not rely on untrusted paymasters or verifiers to keep such information private.

#### Arbitrary Signature Malleability

The protocol performs no cryptographic validation for `ARBITRARY` signatures. When an `ARBITRARY` signature has empty `msg`, its raw bytes are elided from the canonical signature hash and can therefore be changed without changing `TXPARAM(0x08)`. If the custom verifier accepts multiple encodings for the same witness the transaction hash may be malleable even though the canonical signature hash is not. Custom verifiers should enforce canonical encodings and reject unused bytes. When an `ARBITRARY` signature uses an explicit 32-byte `msg`, its raw bytes are committed to by the canonical signature hash and this particular source of transaction-hash malleability does not apply.

##### Mitigations

Node implementations should consider restricting which opcodes and storage slots validation frames can access, similar to ERC-7562. This isolates transactions from each other and limits mass invalidation vectors.
Expand Down
Loading