Flexible and Upgradeable Account Authentication

We are excited to share the following proposal for introducing the next generation of authentication features to the Monad protocol: accounts that can add, rotate and retire authenticators in place, without changing their address. This enables post-quantum accounts, social recovery, and user-friendly schemes such as passkeys natively.
We welcome discussion and feedback from the community.

Flexible and Upgradeable Account Authentication

Status: Draft (v1, design and architecture, concrete implementation spec deferred)

Authors: Kushal Babel, Jan Camenisch

Contents

  1. Abstract
  2. Motivation
  3. Goals and Scope
  4. Design Constraints
  5. Account Model
  6. Verification Model
  7. Address Derivation
  8. Configuration Supply and Transaction Shape
  9. Reconfiguration, Rotation and Recovery
  10. Account Lookup
  11. Components Touched
  12. Backward Compatibility

1. Abstract

Separate an account’s authentication from how its address is derived. An account holds an authentication configuration (a set of authenticators and a policy over them) stored in account state and mutable over the account’s lifetime. The address is fixed at creation and does not change when authenticators are added, removed, rotated, or upgraded.

This gives recovery from a lost or compromised key without moving assets, in-place scheme upgrade (including post-quantum), and protocol-enforced multi-factor and scoped-access policies. Existing accounts and transactions are unaffected; addresses stay 20 bytes.

2. Motivation

Today an account identifier is the last 20 bytes of the hash of a secp256k1 public key. Binding the identifier to one key and one scheme means:

  • compromise of the key compromises every asset under the account;
  • a broken scheme exposes every account using it;
  • a lost key makes the account unrecoverable; and
  • security cannot be upgraded in place: it is weakest at creation, with no path to strengthen it without abandoning the address.

Multi-factor, staged, and scoped authentication is therefore pushed off-chain or into smart-contract accounts (bundlers, paymasters, per-wallet code).

3. Goals and Scope

3.1 In scope

  • Separate the authentication mechanism from address derivation.
  • Support multiple authenticators per account, with add and remove.
  • Support in-place upgrade of authenticators, including post-quantum schemes.
  • Support a minimal declarative policy over authenticators (including threshold).
  • Preserve existing types for accounts, addresses and transactions.

3.2 Out of scope

  • Stateful policies such as per-period spending limits applicable to specific authenticators.
  • User-supplied custom authentication mechanisms (except for the policy they express).
  • Automatic expiry or time-to-live for keys or policies.

4. Design Constraints

These are why the design does not simply adopt an existing proposal from EIPs like frame transactions (EIP-8141) or EIP-8130.

  • Want simplicity, frame transactions are not simple.
  • Authentication must be decidable cheaply at inclusion time (before EVM execution) and must not read or write shared state which would serialise parallel transactions. Reading only the sender’s own record is fine.
  • EVM backward compatibility. Addresses stay 20 bytes; CALL/CREATE/CREATE2 and tooling unchanged. A no-config account behaves as a legacy EOA. The configuration adds one field to the account record (§5.2): a state-trie change. Since the config has public keys in it, and the address is a hash of it, nobody can squat on your account address.

4.1 Relation to Account Abstraction

This proposal deliberately abstracts only the authentication layer, and unlike account abstraction, is not concerned with bringing programmability to EOAs. In that sense, this proposal is complementary to account abstraction mechanisms like EIP-7702, which still cannot retire the underlying ECDSA key.

This allows the design to be relatively simple, and efficient, and provide a uniform, efficient authentication layer rather than relying on fragmented auth implementations inside individual smart contracts.

5. Account Model

5.1 Authentication configuration

An account’s authentication is a record in account state:

AuthConfig {
  version,
  next_id,                          // monotonic; authenticator ids are assigned from here
  config_version,                   // count of applied reconfigurations
  authenticators: [ { id, scheme_id, pubkey_or_params }, … ],
  signing_policy: <policy>,         // may sign transactions
  reconfiguration_policy: <policy>, // may change the configuration
}

Each authenticator names a scheme_id (initially: ECDSA on secp256k1, ECDSA on P-256, webauthn on P-256 (passkeys), EDDSA on Ed25519, ML-DSA (a post-quantum scheme), and a ZK-OAuth verifier) and carries that scheme’s public key or parameters; for post-quantum security of a non-PQ scheme, an authenticator may instead store a hash of the public key. A policy is either a single authenticator or a threshold over (recursive) sub-policies: policy := ID(i) | THRESHOLD(k, [p1, ..., pn]); a leaf ID(i) holds when authenticator i signs, and THRESHOLD(k, […]) holds when at least k of the listed sub-policies are satisfied. OR is THRESHOLD(1, …), AND is THRESHOLD(n, …), and k-of-n is the general form. The policy expresses how many and which authenticators must sign. An account with no explicit AuthConfig (a legacy EOA account) is treated as a single implicit secp256k1 authenticator under a default signing and reconfiguration policy that permits the public key to sign and reconfigure for the account.

For Monad’s deferred execution, the account record also holds at most one pending configuration together with its activation block.

5.2 Storage

The account record gains a field, auth_config_root, alongside nonce, balance, storage_root, code_hash. An empty value denotes a legacy account. The configuration is per-account state: reads and writes during verification touch only the sender’s subtree and do not contend with other accounts.

6. Verification Model

As signature schemes are few and slow-moving, the protocol ships a roster of verification algorithms, each having a fixed-gas cost. An authenticator refers to one of the verification algorithms. Adding a scheme requires a fork / governance.

Policy is declarative data stored in the configuration; the protocol reads the declared rule and evaluates it. Additionally, the result of evaluation (which authenticators signed, i.e. the authentication level) could be exposed to execution via a new opcode (an execution-environment value), so a called contract can read it and branch on how the transaction was authenticated.

A transaction carries one or more (authenticator_id, signature / proofs) pairs; each signature is verified against its authenticator, and the valid ids form the satisfied set. The policy is evaluated over that set: THRESHOLD(k, S) holds when the satisfied set intersects S in at least k elements. Each scheme has a fixed gas cost, and a transaction pays the sum over the authenticators it supplies. The policy evaluated for a transaction is always the signing policy; the reconfiguration policy is evaluated only inside the AuthConfigManager precompile (§9.1).

7. Address Derivation

7.1 Existing accounts

An account with no configuration authenticates as today: recover the secp256k1 public key from the signature and require the recovered address to equal the account address. To upgrade, the AuthConfigManager precompile is called with the new configuration (§9.1). The address does not change.

7.2 New accounts

The address is a hash of the genesis configuration: address = keccak256(domain_sep ‖ version ‖ genesis_config)[-20:]. Similar to the CREATE2 pattern. The address is fixed at creation and is recomputable by anyone who knows the genesis configuration, so an account can be funded before it is first used. The live configuration may then diverge from the genesis configuration through rotation (§9) without changing the address. New accounts can be activated either by sending the genesis config through the new transaction type (see §8.3), or by calling the AuthConfigManager precompile with the genesis config (possibly via a relayer’s transaction).

8. Configuration Supply and Transaction Shape

8.1 Configuration storage

The config is stored in the account record, and is bound to the address only at genesis. The config is mutable thereafter.

8.2 Supplying the configuration

The chain cannot know the preimage of the address until it is revealed, so the genesis configuration is supplied exactly once and then stored. The only exception is a legacy account, which has no stored configuration and is treated as the implicit default (§5.1). Every new account supplies its genesis configuration once, in an optional init_config field, the hash of which is checked against the address, and it is then stored. Later transactions omit it.

8.3 New transaction type

A new transaction type that includes from address (not present currently in EVM transactions), auth_data (may include signatures and public keys / proofs) instead of the “ECDSA v,r,s values”, and optional init_config for the first transaction that can install config and perform a call atomically. Config need not be supplied again per transaction.

The envelope carries three authentication fields. auth_data is a single list of (authenticator_id, signature / proofs) entries over sig_hash, and authenticates the transaction against the signing policy. init_config, present only in the first transaction of a new account, is rlp([genesis_config, pop_entries]); sig_hash covers genesis_config and excludes pop_entries, which are signatures themselves. authorization_list carries generalized EIP-7702 tuples (authorized with signing policy of the account). All changes to an existing configuration go through the AuthConfigManager precompile (§9.1). The transaction consumes the account nonce like any other, so replay protection and ordering come for free.

9. Reconfiguration, Rotation and Recovery

9.1 Reconfiguration

Changes to the authenticator set, the signing policy, or the reconfiguration policy are authorized by satisfying the current reconfiguration policy, expressed in this same language. Installing a new policy additionally requires a proof of possession of every authenticator being installed. A configuration that cannot be satisfied therefore can never be installed (aka account is not bricked accidentally), and the change takes effect k blocks later, where k is Monad’s execution delay gap (3 today). The reconfiguration policy governs changes to itself in the same way.

Example. A spending key and a passkey, recoverable by two guardians.

  • authenticators: { id 0: secp256k1, main key }, { id 1: P-256, passkey }, { id 2: secp256k1, guardian1 }, { id 3: secp256k1, guardian2 }.
  • signing_policy = THRESHOLD(1, [ID(0), ID(1)]): the main key or the passkey may sign.
  • reconfiguration_policy = THRESHOLD(1, [ID(0), THRESHOLD(2, [ID(2), ID(3)])]): the main key alone, or guardian1 and guardian2 together, may change the configuration.

To rotate the spending key, the holder sends a reconfiguration satisfying the reconfiguration policy with ID(0) and a proof of possession of the new key (e.g., signature). If the main key is lost, guardian1 and guardian2 together install a fresh key; hence the reconfiguration mechanism also allows for recovery inherently.

AuthConfigManager precompile

All changes to an existing configuration are made by calling the precompile, from any transaction type and by any sender. The precompile checks that the calldata satisfies the reconfiguration policy of the specified target account, carries proofs of possession, and then installs the config while incrementing the config_version to prevent replay. The same precompile also activates a new account: the calldata carries the genesis configuration instead of a change, and is authorized by the address hash check together with the proofs of possession rather than by a reconfiguration policy.

9.2 Rotation and Upgrade

Rotation changes which keys control an account while keeping the address.

  • Rotate: replace an authenticator’s key, or add a new one and remove the old.
  • Upgrade scheme: add an authenticator under a new scheme (e.g. a post-quantum scheme), then retire the old one. The account is post-quantum in place; the address is unchanged.
  • Convert to multisig: add authenticators and change the policy from a single-signer threshold to a k-of-n threshold.

Because the address commits only to the genesis configuration, after any rotation the address no longer equals the hash of the live configuration. The address stays a stable identifier even after the live keys have changed.

9.3 Recovery

Recovery itself is just a clause of the reconfiguration policy (see the example above).

10. Account Lookup

Two directions:

10.1 Address to configuration

This is all the protocol needs and it is unaffected by rotation. A transaction names its sender; the account record is keyed by the address; the protocol reads the live configuration directly. The chain does not derive an address from a key.

10.2 Key to address

“Given a credential, which account does it control?” matters to wallets (the protocol never needs it), and rotation breaks it: the current key no longer hashes to the address. It matters on device restore, for guardians, and for credential-based discovery. We adopt an on-chain index:

Mechanism Description Implications
On-chain index Maintain key → address in state, updated on every rotation (the Aptos OriginatingAddress approach). The value is a set of addresses, since a shared guardian key legitimately controls several accounts. Trustless and universal. Costs state; needs a rule for keys controlling several accounts; publishes which key controls which account (a privacy cost); stale entries must be deleted on rotation.

11. Components Touched

Mempool, account state trie, new tx type, consensus (validation before voting).

Execution gains the AuthConfigManager precompile and the pending-configuration state it keeps.

12. Backward Compatibility

Account Transaction type Result
No configuration Existing (0, 1, 2) Unaffected
No configuration New Valid immediately under the implicit configuration: one secp256k1 authenticator at id 0, both policies ID(0).
Configured New Full policy evaluation
Configured Existing (0, 1, 2) Valid if and only if the recovered ECDSA key alone satisfies the effective signing policy.

12.1 Upgrade using existing wallet software

The first upgrade of a legacy account is an ordinary transaction to the AuthConfigManager precompile. No extra signature is needed to satisfy the reconfiguration policy, because the legacy account’s implicit reconfiguration policy is its ECDSA key, and the transaction’s own signature already satisfies it.

12.2 EIP-7702

An authorization tuple is honored if and only if its signature set satisfies the authority’s effective signing policy at the time the tuple is processed. A legacy account’s tuples are unaffected whereas a tuple signed by a retired key is refused. The new transaction type also carries generalized tuples, which carry their own signature set in place of the legacy v,r,s values. EIP-7702 delegations already in effect when the authorising keys are retired remain in effect until re-delegated / un-delegated using the new keys and signing policy.

5 Likes

From a user point of view, keeping the same address while recovering an account or improving security sounds really useful.

My main question is how simple the recovery process would feel for someone who isn’t technical. If users need to understand too many settings, they may avoid it. Will there be a straightforward default path, with clear warnings before anything important changes?

It would be nice if an EIP-712 (or similar) signed message could interact with the AuthConfigManager.

A transaction to the AuthConfigManager is nice but it could invalidate a 7702 delegation. It would also be less reliable if pre-signed, as the base fee could be volatile and the tx could no longer be priced correctly. Sitting on a pre-signed but unsubmitted auth change is a nice ux pattern, especially for privacy and for crosschain bridging (anything where you need to have access and then prove you no longer have access)

1 Like

Great job on the proposal! I personally think that the suggested abstraction is simple and neat.

I have a question - do you see any potential conflict if EIP-8141 is included in a future hard fork and Monad decides to support it, resulting in two competing authentication models on the chain?

Also, I think the 10.2 index should be left out of the core spec and delegated to wallets/indexers, it should be trivial to implement on the app layer

I dont understand, why doesn’t 8130 satisfy the above?

Base is running fully native code. Fully decidable means just removing the evm path on the keystore contract which is an easy change for Monad.

Addresses stay backward compatible. No config account behaves as legacy EOA.

Happy to work together on it!

Good thread, and it’s mostly (rightly) about the authorization side. I come at this from the operational and tooling side (I run infrastructure on Monad and build some read/indexing tooling), and the part I keep circling back to is what happens downstream once an account’s auth stops being a single key.

The write path is getting attention here; the read path isn’t specified at all. Without a standard way to ask “what’s this account’s current AuthConfig and config_version,” every indexer, explorer, and wallet decodes precompile or keystore storage by hand, and that breaks the first time the layout changes. A minimal eth_getAuthConfig, or config changes emitted as events so indexers follow config_version from logs instead of diffing state each block, would save a lot of fragmentation.

@chunter’s point about dropping the EVM path on the keystore contract is reasonable, but it sharpens a separate problem: the more auth verification lives off the EVM path, the harder it is for today’s simulation and tracing to represent it. eth_call and estimateGas already run without a valid signature, which is most of what a wallet does before signing. If auth becomes native and mandatory, there needs to be a defined skip-auth or override under simulation, or gas estimation and previews just break for these accounts.

None of this blocks the design, it’s just much cheaper to settle now than to retrofit once tooling has grown around a shape.

3 Likes

One thing that Frame transactions, Tempo transactions, and 8130 transactions get right is having the option of a clear separation between authorizing transaction fee payment vs authorizing a specific account to do something. As it is, this specification keeps these together, and I think we need the separation.

While you can of course have any contract you want (or a 7702 contract) decode authorization signature and calldata actions out of the transaction, this in practice means that there’s no standard way for wallets to handle this, or tooling to support viewing this data. The other transaction types (frame, tempo, 8130) have explicit ways to specify both what account is authorizing, and what that account is doing in the transaction, which ensures compatibility across the wallets/accounts on the chain, and good tooling support.

neat design, but the hard part is going to be keeping it evm-compatible. a new tx type + precompile that aren’t standard EVM means wallets, indexers and tooling all need monad-specific support to use it. that cuts against the whole “your ethereum stack just works” pitch. staying close to something like 7702 would help

new tx type isn’t the only way to access this; using the new tx type is an alternative to doing a transaction to the precompile (basically a smart contract). I agree that getting wallets to support a new tx type would be a painful uphill battle.

good point, agreed. reconfiguration through the AuthConfigManager precompile works with normal txs, so that part needs no wallet changes.

where the new tx type still helps is authenticating a regular tx under the new schemes: a standard tx only carries one ECDSA sig, so passkeys or threshold auth need somewhere to put the auth_data. the evm-compatible alternative is the smart-account / 7702 route (4337 userOps), which doesn’t need a new tx type.

one concrete thing: Monad mainnet doesn’t currently expose the RIP-7212 P-256 precompile. i tested 0x100 with a valid vector and it comes back empty. so the smart-account passkey path would have to verify P-256 in a solidity contract instead of natively. might be worth considering the precompile regardless of which auth direction you take.

What happens to protocols that rely on ecrecover after this, i.e. Permit2?

Monad supports the p-256 precompile, see docs at Precompiles - Monad Documentation

1 Like

From a user and product perspective, I believe this to be great addition to the Monad protocol. Great work!

Checked this rather than guessing. Generated a P-256 keypair, signed a digest, verified the signature locally so the vector is known-good, then called 0x100.

Mainnet and testnet both return 0x00...01. Corrupt one byte of s and both return empty, which is the trap: empty is what an invalid signature looks like, so a bad vector is indistinguishable from a missing precompile.

curl -s https://rpc.monad.xyz -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"0x0000000000000000000000000000000000000100","data":"0x0546716d28b1a3a1689bbd19283dfc8f3c01246d3eddb172373330a370cfce845da511d923e90e223e796c2c0be1f77903791740bd9a2b8883659c972c69748b01faa90fee2dd009d334a0987cefc43c3283d51848eb8ff796218e072b37c2847d57424a2b1411dd9082b60093b687a5113f84a304654d4beb79459f0115b743c36edf36f7aa7850da1524fbc5b3db3e1567d4e9acb05efb7ae996911335ff78"},"latest"]}'

Was your test an eth_call or a real transaction? I only checked the call path, and other chains have had 0x100 answer under eth_call while returning empty returndata inside a transaction, which would be a different problem entirely.

1 Like

maybe im missing something but how is the pending config supposed to work when the main key is compromised, in your example the main key and the 2 guardians can both authorize different changes and AuthConfigManager can be called by any sender so they could also come through different relayers, if one config is already pending and another valid one lands before activation does it replace it or get rejected or queued, who can cancel it and against which policy, also does config_version increment when the pending config is created or when it activates, and is the 3 block delay just because of deferred execution or is it supposed to give some recovery window too?

The current draft seems to focus mostly on the protocol layer rather than the wallet UX, so I don’t think the exact recovery flow or default presets are defined yet.

What is already built into the design is some protection against dangerous configurations — for example, a new configuration must include proof of possession of the new authenticators, and an unsatisfiable configuration cannot be installed. Changes also take effect only after a short delay.

But I agree that the wallet layer will matter a lot here. Ideally, most users should get a simple default setup like passkey + recovery method, while advanced threshold policies stay hidden unless they want them. Clear warnings before removing the last recovery method or changing the reconfiguration policy would be especially important.

2 Likes

you’re right, and thanks for actually running it. my vector was bad, i was signing the digest wrong so it didn’t match the hash i passed, and the precompile correctly returned empty for an invalid sig. i read that empty as “no precompile”, which is exactly the trap you described. it’s there, my mistake.

good question on eth_call vs a real tx too. i only checked the call path as well, so worth confirming with an actual tx before assuming the execution path matches.

1 Like

you’re right, should’ve checked the docs first. my bad

For Permit2, and app-layer smart contract logic that relies on ecrecover, the idea is to expose an authentication API from the protocol to the app layer. In order to be PQ-resistant, smart contracts would need to make use of this API rather than harcoding a specific scheme in their logic.

the 3 block delay is there for deferred execution.
the reconfiguration policy should be so that a single lost key is not able to reconfigure the account. recall that signing policy is different that reconfiguring policy, so you can imagine that the reconfiguration policy could require additional keys while signing could be done through a single master key.
If anyway, there is a race condition because of different combinations of keys leaked to the adversary versus held by the guardians, one could mitigate it by making the reconfiguring policy richer (introducing time component, priority of keys, etc.), but some sort of race condition would always remain afaict.