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
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.
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.
| 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.
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[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.
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.
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.
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 extensionThe 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.
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:
BALANCE, SELFBALANCE, SLOAD, SSTORE, TLOAD, TSTORE, EXTCODESIZE, EXTCODECOPY, EXTCODEHASH, and SELFDESTRUCT;BLOCKHASH, COINBASE, TIMESTAMP, NUMBER, PREVRANDAO, GASLIMIT, BASEFEE, BLOBHASH, and BLOBBASEFEE;GASPRICE and ORIGIN;CREATE, CREATE2, CALL, CALLCODE, and DELEGATECALL;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.
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.
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.
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.
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 interactionUnder 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.
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.
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.
The scheme separates two operations with different cost and state profiles:
AUTHENTICATOR_GAS_LIMIT. Resolves (authenticator, key_id) cryptographically.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.
signer is the authenticatorEIP-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.
key_idkey_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.
AUTHENTICATOR is opaque to the EVMEIP-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.
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.
compute_sig_hash is unchangedCommitting 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.
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.
AUTHENTICATOR_BASE_COST is symbolicThe 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.
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.
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.
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.
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.
Full executable state tests remain to be added. Implementations must cover at least the cases below.
signer to the authenticator and supply key_id and a proof whose authenticator result equals key_id.SIGPARAM(0x00) returns the authenticator.VERIFY frame rather than the sender's and assert that validation succeeds.AUTHENTICATOR_COST execution gas is charged regardless of gas left unused by the call.Each condition makes the signature invalid:
signer is absent or not exactly 20 bytes;signer is the zero address;AUTHENTICATOR_HEADER_LENGTH;key_id is zero;key_id different from the claimed one;AUTHENTICATOR_GAS_LIMIT;VERIFY reads SIGPARAM(0x00), isAuthorized[authenticator] is true, and APPROVE succeeds.VERIFY rejects because the authenticator is not authorized.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.SIGPARAM(0x04), assert it equals the authenticated key_id, and accept or reject according to account-defined key-level policy.key_id routingkey_id and assert that they produce the same canonical transaction signature hash and, at equal length, the same fee.key_id and assert that validation fails.key_id from the transaction before authenticator execution, and that the signature becomes valid only when the authenticator's result matches it.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.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.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.
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 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.
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.
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.