---
eip: 8397
title: Frame Authenticator Signatures
description: Adds a bounded, state-independent custom-authentication signature scheme to frame transactions.
author: Taek (@leekt)
discussions-to: https://ethereum-magicians.org/t/eip-8397-frame-authenticator-signatures/29517
status: Draft
type: Standards Track
category: Core
created: 2026-08-26
requires: 2929, 7702, 7928, 8141
---

## Abstract

This proposal extends [EIP-8141](./eip-8141.md) with an `AUTHENTICATOR` signature scheme: a bounded, state-independent custom-authentication path for frame transactions.

`AUTHENTICATOR` allows a frame transaction to carry a custom authentication proof whose expensive cryptographic verification is executed by protocol in a bounded state-independent context. The signature entry's `signer` is an authenticator contract, and its `signature` carries a claimed `key_id` followed by the `proof`. The authenticator returns the credential identifier proven by the witness; validation succeeds only when that identifier equals the claimed `key_id`. Account authorization remains ordinary stateful `VERIFY`-frame logic and authorizes the authenticator address through EIP-8141's existing resolved-signer path.

```text
signature = key_id || proof
        |
        | authenticate(digest, proof)
        v
  authenticated key_id
        |
        | require authenticated key_id == claimed key_id
        v
  resolved_signer = authenticator, key_id retained
        |
        v
account VERIFY frame: require isAuthorized[authenticator]
        |
        v
     APPROVE
```

## Motivation

EIP-8141 validates secp256k1 and P-256 signatures in protocol. Any other scheme must be carried as `ARBITRARY` and verified by account code inside a `VERIFY` frame. This mixes expensive state-independent cryptography with cheap stateful account authorization, and makes the expensive work hard for a sequencer to bound or cache independently of account state.

`AUTHENTICATOR` moves only the expensive, state-independent part into protocol validation, with a protocol-fixed gas budget so that every such signature has a statically known cost. Which authenticator an account trusts remains account policy in the `VERIFY` frame, where EIP-8141 already bounds and tracks it.

## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174).

This specification is a delta against EIP-8141.

### Constants

| Name | Value |
|---|---:|
| `AUTHENTICATOR` | `0x03` |
| `AUTHENTICATOR_HEADER_LENGTH` | `32` |
| `AUTHENTICATOR_GAS_LIMIT` | `50000` |
| `AUTHENTICATOR_BASE_COST` | `COLD_ACCOUNT_ACCESS_COST` |
| `AUTHENTICATOR_COST` | `AUTHENTICATOR_BASE_COST + AUTHENTICATOR_GAS_LIMIT` |

`AUTHENTICATOR_HEADER_LENGTH` is the length of `key_id`. `AUTHENTICATOR_BASE_COST` is the [EIP-2929](./eip-2929.md) cold account access cost in the active gas schedule, charged for reading the authenticator's code; it tracks any future repricing of that constant rather than fixing a number here. `AUTHENTICATOR_COST` is the fixed, protocol-defined cost of one `AUTHENTICATOR` signature (`52600` under the current schedule); neither the transaction nor the authenticator chooses the authentication gas budget.

### Transaction structural changes

The EIP-8141 signature-scheme table is extended with:

| `scheme` | Name | `signer` encoding | `signature` encoding | Gas cost |
|---|---|---|---|---|
| `0x03` | `AUTHENTICATOR` | 20-byte authenticator address | `key_id (32 bytes) || proof` | `AUTHENTICATOR_COST` |

Values `0x04` through `0xff` remain reserved.

EIP-8141's static constraints are extended equivalently to:

```python
for sig in tx.signatures:
    if sig.scheme == AUTHENTICATOR:
        assert len(sig.signer) == 20
        assert len(sig.signature) >= AUTHENTICATOR_HEADER_LENGTH
    # Existing scheme-specific constraints remain unchanged.
```

### `AUTHENTICATOR` signature scheme

#### Wire format

```text
[scheme, signer, msg, signature]

scheme    = AUTHENTICATOR
signer    = authenticator address     (20 bytes)
signature = key_id                    (32 bytes)
         || proof                     (variable bytes)
```

```python
authenticator = address(sig.signer)
claimed_key_id = sig.signature[0:32]
proof = sig.signature[32:]
```

The `signer` MUST be present and MUST be the authenticator; unlike `SECP256K1` and `P256`, an absent `signer` does not default to `tx.sender`. The `msg` field retains the EIP-8141 meaning: an empty value selects the canonical frame-transaction signature hash and a 32-byte value selects that explicit digest.

The `key_id` is protocol-visible authentication metadata. It identifies the credential claimed by the witness and allows builders and sequencers to route, cache, batch, or otherwise classify authentication work before frame execution. The value is not trusted merely because it appears in the transaction: the authenticator MUST derive the authenticated key identifier from the proof, and protocol validation succeeds only when the returned identifier equals the claimed `key_id`.

An `AUTHENTICATOR` signature is structurally valid only if:

```python
assert len(sig.signer) == 20
assert authenticator != address(0)
assert len(sig.signature) >= AUTHENTICATOR_HEADER_LENGTH
assert claimed_key_id != bytes32(0)
```

The authenticator MUST contain regular deployed code, MUST NOT be a precompile, and MUST NOT contain an [EIP-7702](./eip-7702.md) delegation indicator. The code currently deployed at the address is what runs.

#### Authenticator execution

```solidity
pragma solidity ^0.8.0;

interface IFrameAuthenticator {
    function authenticate(bytes32 digest, bytes calldata proof)
        external
        view
        returns (bytes32 keyId);
}
```

The protocol calls `authenticate(digest, proof)` in the [pure authentication context](#pure-authentication-context) with `AUTHENTICATOR_GAS_LIMIT` gas. The authenticator is responsible for parsing the proof, selecting or deriving the underlying credential, performing cryptographic verification against `digest`, and deriving the authenticated key identifier. The call MUST return exactly 32 bytes equal to the claimed `key_id`. A zero or different result, malformed return data, revert, exceptional halt, out-of-gas condition, or forbidden operation makes the signature invalid.

#### Signature validation

```python
def validate_authenticator(sig, sig_hash, state):
    authenticator, claimed_key_id, proof = parse_authenticator(sig)

    if len(sig.msg) == 0:
        digest = sig_hash
    elif len(sig.msg) == 32 and sig.msg != bytes32(0):
        digest = sig.msg
    else:
        return INVALID

    require_regular_code(state, authenticator)

    authenticated_key_id = pure_authenticator_call(
        target=authenticator,
        calldata=abi_encode_authenticate(digest, proof),
        gas=AUTHENTICATOR_GAS_LIMIT,
    )
    if authenticated_key_id != claimed_key_id:
        return INVALID

    return VALID(resolved_signer=authenticator, key_id=authenticated_key_id)
```

`SIGPARAM(0x00)` returns the authenticator address because the authenticator is the entry's resolved `signer`. The validated entry's `key_id` attribute (see [Signature entry attributes](#signature-entry-attributes)) is what `SIGPARAM(0x04)` returns. `SIGPARAM(0x01)` and `SIGPARAM(0x02)` retain their EIP-8141 meaning.

EIP-8141's `compute_sig_hash` is not modified. For a canonical-hash `AUTHENTICATOR` entry the entire `signature` field is elided exactly like every other EIP-8141 witness, so `key_id` and `proof` are replaceable witness data. The integrity of `key_id` comes from the authenticator result plus the protocol equality check, not from inclusion in the canonical hash. Once validation succeeds, `key_id` may be treated as authenticated metadata.

`AUTHENTICATOR` is an opaque protocol-validated scheme like `SECP256K1` and `P256`: `SIGDATACOPY` and `SIGPARAM(0x03)` are not defined for it and result in an exceptional halt. The authenticated `key_id` is exposed through a new `SIGPARAM` value instead, so that the raw proof never becomes EVM-visible.

#### Signature entry attributes

EIP-8141 associates one derived attribute with each protocol-validated signature entry, `resolved_signer`, which `SIGPARAM(0x00)` returns. This proposal extends the validated signature entry with a second derived attribute:

| Attribute | Type | Set for | Value |
|---|---|---|---|
| `resolved_signer` | 20-byte address | `SECP256K1`, `P256`, `AUTHENTICATOR` | EIP-8141 semantics; the authenticator address for `AUTHENTICATOR` |
| `key_id` | 32 bytes | `AUTHENTICATOR` only | the identifier returned by the authenticator and matched against the claimed `key_id` |

Both attributes are populated during signature validation, before any frame executes, and are immutable for the rest of the transaction. `key_id` is undefined for entries of any other `scheme`.

#### `SIGPARAM` extension

The EIP-8141 `SIGPARAM` table is extended with:

| `param` | `signatureIndex` | Return value |
|---|---|---|
| `0x04` | signatureIndex | `key_id` (`AUTHENTICATOR` only) |

`SIGPARAM(0x04)` returns the entry's `key_id` attribute. Requesting `key_id` for a signature entry of any other `scheme` results in an exceptional halt. Account code MAY read it when it needs per-key policy; doing so is optional and does not change the default authenticator-level authorization model.

There is no fallback from `AUTHENTICATOR` to `ARBITRARY`, account-code signature verification, or another signature scheme.

### Pure authentication context

Authenticator execution is expensive, state-independent, and bounded. Its top-level environment is:

| Property | Value |
|---|---|
| `ADDRESS` | authenticator address |
| `CALLER` | EIP-8141 `ENTRY_POINT` |
| `CALLVALUE` | `0` |
| calldata | ABI-encoded `authenticate(digest, proof)` |
| static mode | enabled |
| gas limit | `AUTHENTICATOR_GAS_LIMIT` |

Every opcode not listed as forbidden below is allowed, including `GAS`, `CHAINID`, `ADDRESS`, `CALLER`, `CALLVALUE`, and code-local `CODESIZE` and `CODECOPY`.

The following operations are forbidden and make the signature invalid:

- world-state reads or writes: `BALANCE`, `SELFBALANCE`, `SLOAD`, `SSTORE`, `TLOAD`, `TSTORE`, `EXTCODESIZE`, `EXTCODECOPY`, `EXTCODEHASH`, and `SELFDESTRUCT`;
- block-dependent reads: `BLOCKHASH`, `COINBASE`, `TIMESTAMP`, `NUMBER`, `PREVRANDAO`, `GASLIMIT`, `BASEFEE`, `BLOBHASH`, and `BLOBBASEFEE`;
- transaction-price or origin reads: `GASPRICE` and `ORIGIN`;
- contract creation or non-precompile calls: `CREATE`, `CREATE2`, `CALL`, `CALLCODE`, and `DELEGATECALL`;
- logs: `LOG0` through `LOG4`.

`STATICCALL` is allowed only when its target is an active protocol precompile. A `STATICCALL` to any other address makes the signature invalid. Precompile gas charging and return behavior are unchanged.

An opcode introduced after this proposal is forbidden in the pure authentication context unless a later EIP explicitly permits it.

### Access accounting

Signature validation runs before any frame. Reading the authenticator's code is a world-state access and is accounted as follows.

Authenticator code access performed during protocol signature validation MUST be recorded as a read access for the transaction, at the transaction's block access index, under [EIP-7928](./eip-7928.md). Precompiles reached via `STATICCALL` from the pure authentication context follow EIP-8141's existing rule and are not recorded.

Signature validation MUST NOT add the authenticator to `accessed_addresses` or otherwise modify the EIP-2929 warm set. Warm/cold status during subsequent frame execution is determined solely by EIP-8141's ordinary initial warm set and the accesses performed during frame execution. Block access list accounting and EIP-2929 warmness are therefore independent: the authenticator appears in the block access list for this transaction whether or not any frame later touches it.

### Signature gas and data accounting

Existing EIP-8141 signature schemes retain their existing costs. For `AUTHENTICATOR`, `signature_gas(sig) = AUTHENTICATOR_COST`. The complete fixed amount is execution gas and is charged if the signature validates, even when the call returns with unused gas. It is also used in maximum transaction cost, payer reservation, and public-mempool verification-gas accounting.

Because the `signature` field is not committed by the canonical hash, its bytes are charged at the worst-case nonzero-byte rate:

```python
def authenticator_signature_tokens(sig):
    return 4 * len(sig.signature)
```

When EIP-8141 computes `signature_data_cost` and `calldata_tokens`, it MUST use `authenticator_signature_tokens(sig)` in place of `tokens_in(sig.signature)` for `AUTHENTICATOR`. The `signer` and `msg` fields retain ordinary transaction-data pricing.

This rule removes fee variation caused by witness byte *content*. It does not remove fee variation caused by witness *length*: a relayer replacing a canonical-hash witness with a longer one raises the payer's data cost. This is an inherited property of EIP-8141's witness semantics and applies equally to canonical-hash `ARBITRARY` entries; it is not addressed uniquely here. Authenticators SHOULD use canonical proof encodings and reject proofs with unexpected length or meaningless trailing bytes, which bounds the achievable inflation to the difference between accepted encodings.

### Account authorization

The account authorization unit is the authenticator. Account code reads the authenticator with `SIGPARAM(0x00)` and decides whether it trusts that authenticator. A minimal shape is:

```solidity
pragma solidity ^0.8.0;

interface IFrameAccount {
    function validateFrame(uint256 signatureIndex) external;
}
```

conceptually doing:

```text
require(SIGPARAM(0x01, signatureIndex) == AUTHENTICATOR)
require(SIGPARAM(0x02, signatureIndex) == 0)   // canonical transaction hash
authenticator = SIGPARAM(0x00, signatureIndex)
require(isAuthorized[authenticator])
APPROVE(scope)
```

The account does not re-verify `key_id` or parse the proof; those have already been authenticated by the protocol equality check. An account MAY additionally read the authenticated `key_id` with `SIGPARAM(0x04)` and apply per-key policy, but this EIP does not require per-key authorization. Authorizing an authenticator without an additional key-level check delegates credential selection and proof semantics to it (see [Security Considerations](#authenticator-trust-model)). Key rotation policy, recovery, guardians, sessions, locks, storage layout, and wallet presentation are not standardized here; they belong to account implementations or a companion ERC.

### Public mempool

Consensus validity and public-mempool eligibility are separate: every rule in this section is mempool policy and never affects block validity.

`AUTHENTICATOR` validation counts `AUTHENTICATOR_COST` toward EIP-8141's `MAX_VERIFY_GAS`, not observed execution gas. The only validation dependency of a pending `AUTHENTICATOR` transaction is the authenticator address and its current code; authenticator execution creates no mutable state dependency. Nodes MUST revalidate pending `AUTHENTICATOR` transactions when the authenticator's code changes.

#### `MAX_VERIFY_GAS` interaction

Under EIP-8141's current `MAX_VERIFY_GAS = 100_000`, two `AUTHENTICATOR` signatures cost `2 * AUTHENTICATOR_COST = 105_200` under the current gas schedule, before any `VERIFY`-frame execution is counted. A transaction containing multiple `AUTHENTICATOR` signatures — for example a sender credential and a sponsor credential — may therefore remain consensus-valid while being ineligible for public-mempool propagation. This EIP does not modify `MAX_VERIFY_GAS`; if sender and sponsor should both be able to use `AUTHENTICATOR` through the public mempool, an explicit `MAX_VERIFY_GAS` adjustment should be proposed separately.

#### Authenticator admission

Consensus does not restrict authenticator addresses beyond the structural and pure-context rules defined here. Nodes MAY apply ordinary local transaction-admission policy, but no authenticator registry or protocol allowlist is defined by this EIP. Because authenticator execution is state-independent and gas-bounded, a node that admits every authenticator is exposed to at most `AUTHENTICATOR_GAS_LIMIT` of wasted work per invalid signature, comparable to an `ARBITRARY` signature verified in a `VERIFY` frame.

#### Authentication caching

Nodes MAY cache authenticator results across pending transactions and blocks. Because authenticator code is deliberately not committed by the transaction and can change at the same address, a cache identity MUST include the code actually executed. A safe cache key includes at least:

```text
(chain_id, fork_id, authenticator, authenticator_code_hash, digest, key_id, keccak256(proof))
```

A cache entry keyed without `authenticator_code_hash` may return a result produced by different code and MUST NOT be used.

## Rationale

### Two layers

The scheme separates two operations with different cost and state profiles:

1. Protocol authentication: expensive, state-independent, bounded by `AUTHENTICATOR_GAS_LIMIT`. Resolves `(authenticator, key_id)` cryptographically.
2. Account authorization: cheap, stateful, account-specific, inside the ordinary `VERIFY` frame. Authorizes the authenticator address, with optional additional key-level policy chosen by the account.

Responsibilities divide as follows. The protocol executes the bounded authenticator, enforces that the authenticated `key_id` equals the claimed one, and exposes the authenticator as `resolved_signer`. The authenticator owns credential selection, proof semantics, cryptographic verification, and derivation of `key_id`. Builders and sequencers see `(authenticator, key_id)` and may use them for routing, caching, batching, aggregation, or DoS accounting. Account code owns authorization policy: it may trust or reject the authenticator alone, or additionally consume authenticated `key_id` through `SIGPARAM(0x04)` for finer-grained policy.

### Why `signer` is the authenticator

EIP-8141's `signer` identifies the verifying identity of a protocol-validated entry: the recovered address for `SECP256K1`, the public key for `P256`. For `AUTHENTICATOR` the verifying identity is the authenticator contract, so `signer = authenticator` and account code can authorize it through the existing resolved-signer path with no new instruction.

### Why the witness carries `key_id`

`key_id` is claimed in the witness so that builders and sequencers can classify authentication work before executing the authenticator: two transactions naming the same `(authenticator, key_id)` are candidates for the same cache line or batch, and a node can account for or rate-limit per credential. Because the value is unsigned witness data, it is only a claim until the authenticator derives the real identifier from the proof and the protocol checks equality. After that check the claim is authenticated, and nothing downstream needs to re-derive it. Account policy need not consume `key_id`, but can read it through `SIGPARAM(0x04)` when per-key policy is useful.

### Why `AUTHENTICATOR` is opaque to the EVM

EIP-8141 hides the raw bytes of protocol-validated schemes so that future aggregation schemes remain possible. Exposing the `AUTHENTICATOR` proof through `SIGDATACOPY` would foreclose aggregating authenticator proofs for no benefit to accounts, which need only `key_id`. A dedicated `SIGPARAM` value delivers `key_id` while keeping the proof opaque, matching how `SECP256K1` and `P256` expose only metadata.

### Why account authorization is authenticator-level by default

The minimal account path needs only to authorize the authenticator through `resolved_signer`; this keeps the common authorization path aligned with EIP-8141's native signature schemes. Accounts that want per-credential restrictions MAY additionally read the authenticated `key_id` through `SIGPARAM(0x04)` and apply their own key-level policy. This EIP deliberately does not require either storage layout or policy model.

### Why `compute_sig_hash` is unchanged

Committing `key_id` or a proof length in the canonical hash would introduce an `AUTHENTICATOR`-specific hashing rule. The integrity of `key_id` is already established by the authenticator result plus the equality check, and witness-length fee malleability is an inherited property of EIP-8141's canonical-hash witnesses generally. If that semantics should change, it should change consistently in EIP-8141 rather than uniquely here.

### Why authenticator gas is protocol-fixed

A per-signature or per-key gas budget would make `AUTHENTICATOR` cost depend on witness contents, complicating intrinsic-gas computation and public-mempool bounding. A single `AUTHENTICATOR_GAS_LIMIT` gives every `AUTHENTICATOR` signature the same statically known maximum cost, in the same way native schemes have fixed costs. That static bound is the point of the scheme.

### Why `AUTHENTICATOR_BASE_COST` is symbolic

The base cost prices a cold code read. Fixing a numeral here would silently diverge from the EVM's account access price if another EIP reprices it. Tying it to the active `COLD_ACCOUNT_ACCESS_COST` keeps signature pricing consistent with frame-entry pricing under any schedule.

### Why a signature scheme instead of a pure frame

A separate pure frame would require wallet and account code to coordinate a signature index, pure-frame index, result format, and subsequent `VERIFY` frame. Keeping the witness in the signature list preserves one authentication namespace.

### Why no authenticator registry

`AUTHENTICATOR` has no state reads, no mutable dependencies, fixed verification gas, and deterministic validation constraints. That is exactly the kind of path that should be permissionless. Nodes retain ordinary transaction-admission policy; nothing here needs a registry or allowlist.

### Why the authenticator code read is in the block access list

Unlike `SECP256K1` and `P256`, whose verification is protocol logic with no contract-code state access, `AUTHENTICATOR` validation reads arbitrary deployed code from world state. EIP-7928 exists to record actual state and code reads, so excluding this one would be inconsistent with its purpose. Warmness is a separate concept: recording the read does not warm the address, and the signature validator does not pay or grant EIP-2929 warmth on behalf of later frames.

## Backwards Compatibility

This proposal assigns signature scheme `0x03`, which EIP-8141 currently reserves, and defines `SIGPARAM(0x04)`. Activation requires a coordinated network upgrade. Pre-upgrade nodes reject transactions using these values; existing EIP-8141 transactions retain their prior behavior.

## Test Cases

Full executable state tests remain to be added. Implementations must cover at least the cases below.

### Authentication success

1. Deploy a regular non-delegated authenticator.
2. Set `signer` to the authenticator and supply `key_id` and a proof whose authenticator result equals `key_id`.
3. Assert that validation succeeds and `SIGPARAM(0x00)` returns the authenticator.
4. Repeat with the signature consumed by a sponsor's `VERIFY` frame rather than the sender's and assert that validation succeeds.
5. Assert that exactly `AUTHENTICATOR_COST` execution gas is charged regardless of gas left unused by the call.

### Authenticator rejection

Each condition makes the signature invalid:

- `signer` is absent or not exactly 20 bytes;
- `signer` is the zero address;
- signature is shorter than `AUTHENTICATOR_HEADER_LENGTH`;
- `key_id` is zero;
- authenticator is a precompile or delegated account;
- authenticator returns a `key_id` different from the claimed one;
- authenticator returns malformed data;
- authenticator reverts;
- authenticator exceeds `AUTHENTICATOR_GAS_LIMIT`;
- authenticator reads state;
- authenticator calls a non-precompile.

### Account authorization

1. Account authorizes the authenticator: validation succeeds, `VERIFY` reads `SIGPARAM(0x00)`, `isAuthorized[authenticator]` is true, and `APPROVE` succeeds.
2. Account does not authorize the authenticator: cryptographic authentication succeeds, but `VERIFY` rejects because the authenticator is not authorized.
3. Same authenticator, different authenticated `key_id`: supply a proof for a second credential with its matching `key_id`. Protocol authentication succeeds, and an account that authorizes only the authenticator behaves unchanged.
4. Account applies optional per-key policy: read `SIGPARAM(0x04)`, assert it equals the authenticated `key_id`, and accept or reject according to account-defined key-level policy.

### Witness replacement and `key_id` routing

1. Produce two valid proofs for the same `key_id` and assert that they produce the same canonical transaction signature hash and, at equal length, the same fee.
2. Replace the proof with one authenticating a different key while keeping the claimed `key_id` and assert that validation fails.
3. Assert that a client can parse `key_id` from the transaction before authenticator execution, and that the signature becomes valid only when the authenticator's result matches it.
4. Assert that `SIGDATACOPY` and `SIGPARAM(0x03)` on an `AUTHENTICATOR` entry result in an exceptional halt, and that `SIGPARAM(0x04)` on a `SECP256K1`, `P256`, or `ARBITRARY` entry results in an exceptional halt.

### Caching

1. Cache an authentication result, replace the runtime code at the same authenticator address, and assert that the old result is not reused.

### Access accounting

1. Assert that the authenticator code access appears in the transaction's EIP-7928 block access list.
2. Assert that validation does not modify EIP-2929 warmness: an authenticator that is neither `tx.sender` nor otherwise in the initial warm set is cold at first frame access, and an authenticator equal to `tx.sender` is warm because EIP-8141 initializes `tx.sender` warm.

## Security Considerations

### Authenticator trust model

The account authorizes an authenticator. The authenticator determines which underlying credentials are valid. `require(isAuthorized[authenticator])` therefore means the account delegates credential-selection and proof-validation semantics to that authenticator unless the account additionally constrains the authenticated `key_id`.

An authenticator that accepts arbitrary credentials effectively authorizes every credential it accepts when the account performs only authenticator-level authorization. Accounts that need per-credential restrictions MAY read the authenticated `key_id` with `SIGPARAM(0x04)` and enforce additional account-defined policy. Alternatively, the authenticator itself may bind successful proofs to the intended credential set.

### Unsigned witness

`key_id` and `proof` are not committed by the canonical hash and may be replaced by anyone. A replacement cannot forge authentication: the authenticator MUST derive `key_id` from the proof rather than echo the transaction-provided value, and the protocol accepts the signature only when the derived value equals the claim. An authenticator that returns a `key_id` not cryptographically bound to the proof allows anyone to satisfy validation for any claimed `key_id` and MUST NOT be authorized.

Witness-length fee malleability is inherited from EIP-8141 canonical-hash witnesses, as described under [Signature gas and data accounting](#signature-gas-and-data-accounting).

### Code upgrade and address reuse

Code is not pinned. An authenticator whose address can be recreated with different runtime code can change behavior under a previously reviewed address. Accounts should only authorize authenticators deployed without that pattern, and node caches MUST key on the executed code hash as described under [Authentication caching](#authentication-caching).

### Context consistency

Clients must identically enforce pure-authentication opcodes, returndata validation, gas boundaries, block access list recording, and warm-set non-modification. Differences may cause consensus failures.

### Explicit-message signatures

A nonempty `msg` does not necessarily commit to the frame transaction. Accounts approving execution or payment must reject such signatures unless they independently prove an exact commitment to every security-relevant transaction field.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
