diff --git a/EIPS/eip-8141.md b/EIPS/eip-8141.md index 9a26f8a343febd..e1523fe8149c06 100644 --- a/EIPS/eip-8141.md +++ b/EIPS/eip-8141.md @@ -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: @@ -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` @@ -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: @@ -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: @@ -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 ``` @@ -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) @@ -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. @@ -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. @@ -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.