EIP-7851: Deactivate/Reactivate a Delegated EOA's Key

I apologize, I misjudged the concept of outdated when I assumed that once most wallets had completed the ECDSA conversion, they would no longer write to them, the reality is that they will still use this contract to read the account status.
Using system contracts creates at least 8 extra bytes of state (uint64), whereas setting a prohibition indicator would be less than. The network encourages reducing state creation, and we can achieve that.
We could implement a similar contract, but not a system contract, and allow users to delegate authority to it. Then we initiate the cancellation process and then completely cancel it after a certain period of time. This process is done manually and requires a opcode as mentioned above to complete, however, the current infrastructure is capable of absorbing this limitation.

A system contract is fundamentally the same as a normal contract in terms of state storage. In EVM, whether a contract is designated as a “system contract” (like those in EIP-4788 or EIP-7002) or a user-deployed “normal contract”, they are both bytecode and utilize standard 32-byte storage slots. Storing an 8-byte uint64 (such as the finalizationTimestamp for the 7-day delay) consumes the same amount of state regardless of which contract holds it.

Additionally, alternative methods and the exact justification for choosing a system contract are thoroughly illustrated in the Rationale section of the update PR: Update EIP-7851: Move to Draft by colinlyguo ¡ Pull Request #11316 ¡ ethereum/EIPs ¡ GitHub . Trade-offs are explained there: adding a new account-state field would require RLP encoding and p2p protocol changes, and a precompile lacks a natural storage mechanism. A system contract is the cleanest fit to consolidate deactivation, cancellation, and status queries. There are also other hybrid methods that I thought of, and they will eventually encounter similar issues regarding implementing complexity, overhead introduced, etc.

The analysis in the last paragraph of your last reply seems mostly high-level and directional, and it’s already aligned with the current design. Opcode can be a design choice, as is system contract, precompile, and there are plenty of details about pros and cons, if it’s feasible and doable, etc. So to make the discussion go deeper and align with the understanding of both parties, could you please provide specific technical details? And also it’s nice to check the update PR.

2 Likes

Thank you, your solution is not contradictory, but what is a problem with using an authorization indicator like 0xef0101 to identify a cancelled private key eoa. Identifying it doesn’t significantly increase client workload, while reducing one state access step instead of checking the system contract and further reducing 32 bytes of state needed for timestamp writing, thus reducing user costs. No new state fields or precompiled contracts were introduced; it was all just a one-byte change to the authorization indicator.
If this update is intended to allow a delay period, we could also have an EOA use 7702 and set parameters on it. Once the valid period has elapsed, we can call the update function via code and remove the status used for marking to receive a refund.
I also acknowledge that the solution uses more complex in client implementation, but it covers more use cases for both EOA private key cancellation and contracts that want minimal code deployment. These two could be separated into two different solutions, but I found a unified solution for them and suggest that it should be done this way for overall optimization, and it also reduces some cost for end users during use.
The ideas regarding delegation cancellation and minimum code for contracts have already been proposed, therefore, I will not open any new PR, but will only provide minor technical details to improve them.
The execution flow of the code I have presented here, I also want to know the potential limitations of this design

First a small clarification: reading the account status does not need to “interact with” (i.e., execute) the system contract, because the per-account status storage location is predictable. Concretely, if the status is stored in a mapping(address => ...) at slot 0, then the storage key for a given account is:

keccak256(abi.encode(account, uint256(0)))

So for client-side read queries in various validation paths, it only needs to perform a storage read at that deterministic slot (no contract execution).

With that out of the way, I provide a code-based design summary (brainstormed before and thanks for the chance to post during the discussion) below to make it easier for the community to review and discuss. Also, proposing and iterating on an update PR is not a blocker for me. I can adjust the spec/implementation quickly once there’s alignment.

Design Prototype

Without Cancellation (simplest model)

If we decide that a cancellation/finalization waiting period is not needed, we can encode an immediately-finalized deactivation directly in code (i.e., no timestamp at all). Two simple encodings:

  • Prefix-based (dedicated “finalized deactivated” prefix):
    e.g., 0xef0101 || address (or 0xef01fe || address if we want to treat it as a “ban-style” prefix and keep smaller prefixes available for other extensions).
    Semantics: the account is deactivated/frozen immediately and the delegated code becomes non-updatable as soon as this encoding is set.
  • Suffix-flag-based (keep 0xef0100, append a single byte):
    0xef0100 || address || 0x00
    Semantics: the trailing 0x00 indicates an immediately-finalized deactivation (again: no waiting period; code becomes non-updatable immediately).

This method goes back to the original draft of this EIP by removing reactivation.

With Cancellation

Code Encoding

A straightforward semantics:

  • Keep the existing prefix (no new encoding required):
    0xef0100 || address.
  • When a user initiates key deactivation, append a uint64 finalization_timestamp to the end of the code:
    0xef0100 || address || finalization_timestamp
    The presence of finalization_timestamp simply means “a deactivation has been scheduled (with an activation time)”.

Note that choices about “keeping 0xef0100” or “using a new prefix” also have trade-offs related to implementation and integration validation logic (e.g., backward compatibility). I would like to hear voices and opinions from devs.

Cancel Period / Precompile Interaction

For interaction, both a new precompile and an opcode can do the job. I’d lean toward a precompile because both an EOA and a smart contract wallet can directly interact with it; with a new opcode, an EOA would need a “proxy” contract to access it, adding another risk layer. To keep the authorization boundary hard and the implementation simple, the precompile acts only on msg.sender (no target parameter).

The precompile can use the first byte/bit of calldata as a function selector, e.g.:

  • deactivate(): set msg.sender code to 0xef0100 || address || finalization_timestamp, finalization_timestamp is defined as block.timestamp + finalization_waiting_period at the time deactivate() is called.
  • cancel_deactivate(): allowed only while block.timestamp < finalization_timestamp (returns to 0xef0100 || address).

Rule: cancellation is allowed at any time before finalization_timestamp; once block.timestamp >= finalization_timestamp, the account is considered finalized (no further changes via this path). Reauthorization during the cancellation period is equivalent to canceling the pending finalization and overwriting the code with the newly authorized delegated code; after block.timestamp >= finalization_timestamp, the code becomes non-updatable (i.e., reauthorization is no longer possible).

Design Choices about State Pruning

Choice 1: No Consensus-Level Automatic Cleanup

The simplest approach would be: do not add any consensus-level “automatic cleanup”. Even after the timestamp has passed, we don’t rewrite the code into 0xef0101 ... or remove fields; we just specify in the rules that once block.timestamp >= timestamp, the account is in its final deactivated/frozen semantics. This keeps client implementation much simpler and avoids introducing any global expiry data structure.

Choice 2: Eagerly Clean-Up Timestamps

Clients proactively track expirations and rewrite/remove timestamps when block.timestamp >= timestamp. This requires maintaining extra data structures and can introduce DoS/griefing surfaces (e.g., repeatedly resetting timestamps to create unavoidable maintenance/scan costs).

Choice 3: Lazily Clean-Up Timestamps

Perform expiry checks and potential pruning only on the next interaction (a deterministic hook on tx processing). This raises the attacker cost (they must pay to trigger checks), but accounts that deactivate once and never touch the chain again might never be “cleaned up”. In practice, such state growth is in the same category as general state bloat where someone pays to write persistent bytes.

For the prune strategy, I lean toward choice 3, the reasons are provided above.

Finally, I’m going to collect feedback from the community and client developers on these design preferences (system-contract-based or code-based, whether we want a cancel period, whether we want any expiry cleanup). Since it currently seems that common designs converge to a certain degree of completion, and adding an update PR won’t be the bottleneck, I can iterate quickly once there’s alignment.

2 Likes

I recall that precompiled contracts have had consensus errors in the past, so I lean towards using opcode instead. Proxy isn’t a major issue with the current design a delay like that is long enough to allow users to take action in case of unforeseen problems.

Cleaning up the old state can be done manually in the future, so I still lean towards option 3.
Additionally, I like this suggestion and hope it will be implemented in future network upgrades.

Why not use a different authorization type instead of a system contract or an opcode? An EIP-7702 transaction with one of these authorizations would permanently delegate (and deactivate) the account, using the 0xef0101 (or whatever) prefix.

The new authorization type doesn’t even need to be fundamentally different. We could use a nonce of 2**64-1 to signal a permanent delegation.

With respect to the deactivation window: I don’t think the complexity is worth it. Users can simply delegate with the current mechanism, use it for 7 days or 7 months or however long they want, and then permanently delegate when they feel comfortable.

2 Likes