Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-8397: Frame Authenticator Signatures

Adds a bounded, state-independent custom-authentication signature scheme to frame transactions.

Authors Taek (@leekt)
Created 2026-08-26
Discussion Link https://ethereum-magicians.org/t/eip-8397-frame-authenticator-signatures/29517
Requires EIP-2929, EIP-7702, EIP-7928, EIP-8141

Abstract

This proposal extends EIP-8141 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.

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 and RFC 8174.

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 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:

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

[scheme, signer, msg, signature]

scheme    = AUTHENTICATOR
signer    = authenticator address     (20 bytes)
signature = key_id                    (32 bytes)
         || proof                     (variable bytes)
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:

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 delegation indicator. The code currently deployed at the address is what runs.

Authenticator execution

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 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

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) 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. 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:

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:

pragma solidity ^0.8.0;

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

conceptually doing:

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). 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:

(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.

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.

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 and related rights waived via CC0.

Citation

Please cite this document as:

Taek (@leekt), "EIP-8397: Frame Authenticator Signatures [DRAFT]," Ethereum Improvement Proposals, no. 8397, August 2026. Available: https://eips.ethereum.org/EIPS/eip-8397.