EIP-8151 - Private Key Deactivation Aware ecRecover

Created 2026-02-09
Status Draft
Category Core
Type Standards Track
Authors
Requires

Abstract

This EIP modifies the ecRecover precompile at address 0x0000000000000000000000000000000000000001 to respect EIP-7851 key deactivation. After performing ECDSA public key recovery, the precompile checks whether the recovered address has a deactivated private key. If so, it returns 32 zero bytes (the existing failure sentinel of ecRecover) instead of the recovered address.

Motivation

EIP-7851 enables delegated EOAs (per EIP-7702) to deactivate their private keys, preventing those keys from authorizing transactions or new delegation authorizations. However, the ecRecover precompile is currently a pure cryptographic function with no awareness of account state. Even after an EOA's key is deactivated, on-chain signature verification through ecRecover continues to succeed for that key. This affects contracts that rely on ecRecover for signature-based authorization, such as ERC-20 contracts implementing permit (ERC-2612). Since many such contracts are immutable and cannot be updated to add deactivation checks, modifying ecRecover at the protocol level is a practical path.

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.

Constant Value
ECRECOVER_ADDRESS 0x0000000000000000000000000000000000000001
COLD_ACCOUNT_ACCESS_COST 2600
WARM_ACCOUNT_ACCESS_COST 100

Modified ecRecover Behavior

Starting at the activation of this EIP, the ecRecover precompile at address ECRECOVER_ADDRESS MUST perform the following steps:

  1. Perform ECDSA public key recovery from the input (hash, v, r, s) as currently specified, yielding a recovered_address.
  2. If recovery fails, return 32 zero bytes and consume 3000 gas.
  3. If recovery succeeds, read the account code of recovered_address and determine whether the key is deactivated per EIP-7851.
  4. If recovered_address has a deactivated key, return 32 zero bytes.
  5. Otherwise, return recovered_address left-padded to 32 bytes.

Deactivation Check

An address is considered to have a deactivated key if and only if its account code has a prefix of 0xef0100 and a length of exactly 24 bytes, consistent with the deactivated state 0xef0100 || delegate_address || 0x00 defined in EIP-7851:

code = state.get_code(recovered_address)
is_deactivated = len(code) == 24 and code[:3] == bytes.fromhex("ef0100") and code[-1] == 0x00

Gas Cost

When ECDSA recovery fails, no state access is performed and the gas cost remains 3000.

When ECDSA recovery succeeds, the precompile MUST access the account of recovered_address to read its code. This access MUST follow the EIP-2929 warm/cold rules:

Rationale

Protocol-Level Modification

Modifying ecRecover at the protocol level is chosen because many deployed contracts that rely on ecRecover for signature-based authorization (e.g., ERC-20 permit implementations) are immutable and cannot be upgraded to incorporate deactivation checks. A protocol-level change ensures these existing contracts automatically benefit from key deactivation without requiring redeployment.

Returning 32 Zero Bytes

When a deactivated key is detected, the precompile returns 32 zero bytes rather than triggering an execution failure (success = 0). Currently, malformed v, out of range r/s, and failed recovery all return 32 zero bytes with success = 1, and ecRecover has never used execution failure to signal invalid input. Treating a deactivated key as another form of "recovery failed" keeps this convention intact.

Furthermore, introducing an execution failure path would break deployed contracts that wrap ecRecover with a low level staticcall and require(success), turning a "signature not valid" result into an unexpected revert. Contracts that already check for a zero return will naturally reject deactivated keys without any code changes.

EIP-2929 Gas Accounting

Since the precompile now reads account state, the additional gas cost follows the existing EIP-2929 warm/cold access pattern for consistency with the rest of the protocol. This avoids introducing a new gas model and ensures that the cost of state access is fairly accounted for.

Backwards Compatibility

For addresses whose private key has been deactivated under EIP-7851, ecRecover now returns 32 zero bytes where it previously returned the recovered address.

On every successful ECDSA recovery, the precompile now performs an additional account access, adding either WARM_ACCOUNT_ACCESS_COST (100) or COLD_ACCOUNT_ACCESS_COST (2600) to the base cost of 3000. Transactions that invoke ecRecover near their gas limit MAY fail with an out-of-gas error after activation.

Test Cases

Reference Implementation

COLD_ACCOUNT_ACCESS_COST = 2600
WARM_ACCOUNT_ACCESS_COST = 100
BASE_GAS = 3000

def ecrecover_gas(state, recovered_address):
    if recovered_address is None:
        return BASE_GAS
    if recovered_address in state.accessed_addresses:
        return BASE_GAS + WARM_ACCOUNT_ACCESS_COST
    state.accessed_addresses.add(recovered_address)
    return BASE_GAS + COLD_ACCOUNT_ACCESS_COST

ZERO_BYTES32 = b'\x00' * 32
DELEGATED_CODE_PREFIX = b'\xef\x01\x00'
DEACTIVATED_CODE_LEN = 24  # len(0xef0100 || address || 0x00)

def ecrecover(state, hash: bytes, v: int, r: int, s: int) -> bytes:
    # None means recovery failure
    recovered_address = ecdsa_recover(hash, v, r, s)

    if recovered_address is None:
        return ZERO_BYTES32

    # Check EIP-7851 deactivation
    code = state.get_code(recovered_address)
    if len(code) == DEACTIVATED_CODE_LEN and code[:3] == DELEGATED_CODE_PREFIX and code[-1] == 0x00:
        return ZERO_BYTES32

    return recovered_address.rjust(32, b'\x00')

Security Considerations

Contracts Not Checking for Zero Return

Contracts that use ecrecover but do not verify the result is non-zero are already vulnerable to accepting invalid signatures. This EIP maps deactivated keys to the same failure sentinel (32 zero bytes) and therefore does not introduce a new class of vulnerability.

Application-Level ECDSA Verification

This EIP only modifies the ecrecover precompile. Contracts that perform ECDSA recovery in application-level code (e.g., pure Solidity implementations) bypass the precompile and will not observe deactivation.

Cross-Domain / L2 Fault Proof Implications

Some systems re-execute ecrecover in a different context (different chain, different layer, or asynchronously at a later time) and assume it is a pure function of (hash, v, r, s). Because this EIP introduces a state read (the recovered address's account code), that assumption no longer holds.

In particular, L2 fault proof systems that accelerate ecrecover by executing the L1 precompile and caching the result may become incorrect. Mitigations include removing such acceleration, or migrating to a pure recovery primitive (e.g., implementing secp256k1 recovery inside the fault-proof VM or via a new dedicated pure-recovery precompile in L1), so that the accelerated operation remains a pure function of its inputs.

Multi-Chain Considerations

Key deactivation under EIP-7851 is per-chain state. A key deactivated on one chain remains active on other chains. Users SHOULD NOT assume that deactivating their key on one chain protects them across all EVM-compatible chains.

Copyright

Copyright and related rights waived via CC0.