native account abstraction lets an account hold many keys. it does not say what “add a key” or “revoke a key” means. each account model defines that in its own way, and wallets have to follow all of them.
erc-8403 is my draft for that part. this post describes it first, and then asks what the same account looks like with post-quantum keys on top of eip-8288.
the account stores one root
an authority is a signing key, or more generally a rule like a spend limit or an unlock time. each one is a leaf:
leaf = keccak256(authority_id || predicate)
authority_id is a stable 32-byte handle for the slot. predicate is what the account checks, for a key it is the public key. the account commits to the tree of leaves as one 32-byte root.
a transaction carries two things:
- a membership proof: this leaf is under the account’s root
- a witness: the holder of this leaf approved this transaction. for a key it is a signature over the transaction hash
both are required. the leaf set is public, so the proof alone says nothing about who sends. the account checks both in its own authorization step and fails closed.
add, rotate and revoke are one operation, a new root. the address does not change.
the draft binds this to eip-8141 frame transactions (the check runs in the account’s VERIFY frame) and, as an informative part, to the eip-8130 keystore.
two tiers
the account picks how it reads its root, and that sets how fast a revoke works.
- tier 1 reads a recent root that the transaction names, through eip-8272, and reads no mutable storage. the check stays cheap for the public mempool. the price: a revoked key works until its old root leaves the window
- tier 2 reads the current root from the account’s own slot. a revoke works from the next block, and the check is no longer in the cheap tier
an account that needs a fast revoke has to use tier 2.
one key set on many chains
the root lives on one home chain. on another chain the transaction carries a second proof, a storage proof that this root sits in the account’s slot on the home chain, checked against a home root that chain already imports. no keys are copied. a revoke at home reaches the other chain when the new home root does.
the draft does not define how the home root gets there. it needs an existing mechanism, and where there is none the account has weaker options, which the draft lists with their limits.
recovery
a root can’t give back its own leaves. if the account keeps the leaf records in its own storage, a wallet that lost everything reads them from any node, rebuilds the tree and compares the root.
what runs today
a 3 min demo on two local nethermind nodes, code here:
- keys are added and revoked, the address stays. the wallet deletes its copy of the tree and rebuilds it from account storage
- chain two keeps no keys. its accounts check a signed home block and a storage proof of the root
- a thief makes 7 attempts: revoked keys with old proofs, a proof for another account, a copied signature, a forged home block. one gets through. the tier 2 account refuses the revoked key at once, the tier 1 account accepts the old root until its window closes, and the thief takes 2 eth
what the demo simplifies: the account is minimal, and any key in the tree can set a new root. the home chain has one signer. the account counts the tier 1 window itself, not through eip-8272. the keys are secp256k1.
that last one is the rest of this post.
what eip-8288 solves, and what it leaves
a hash-based signature is big. eip-8288 puts it at 2 to 3 kB and 150,000 to 200,000 gas. its answer is to take the signature out of the block. the transaction carries a 96-byte triple (scheme, data_hash, verification_key_hash), and one recursive stark per block proves that every triple is valid. a leansphincs triple costs 3000 gas in the current draft.
this solves the cost of checking a signature. it does not say how an account decides which key may sign, how to add or revoke a key, or how to move from secp256k1 to a pq key and keep the address. that is the part the erc covers. the erc does not care what the predicate is, so a pq key is one more leaf.
step 1: a leansphincs key as a leaf
eip-8288 uses the 32-byte leansphincs public key directly as verification_key_hash. the leaf holds that key, and the VERIFY frame does this:
- reads the membership proof from frame data and checks the leaf against the root
- walks the frames with
FRAMEPARAMandFRAMEDATACOPYand looks for a dependency(0x10, msghash, pubkey)wherepubkeyis the key in the leaf - checks that
msghashbinds to this transaction
the evm does a few keccak calls for the merkle path. the signature never enters the evm, the block stark covers it.
step 3 has a gap today. test case 1 in eip-8288 wants msghash to be the transaction’s signature hash. that transaction can’t be built: compute_sig_hash covers the data of the dependency frame, and msghash sits in that data, so the hash would have to contain itself. #12435 (open) proposes that a zero data_hash stands for the signature hash of the containing transaction. with it, the account would check for a zero data_hash next to its key.
this is the base. it is all an account needs to hold pq keys and rotate them. i have not run it with a pq key yet.
step 2, optional: move the check into a proof
in step 1 the contract does the check, and the chain sees which key signed. step 2 moves the whole check out of the contract.
a stark proves one kind of thing: “this program ran on this input and finished without a failed check”. so there has to be a program. here it is two lines:
- the input is signed by a leansphincs key
- that key is a leaf under root R
the input is the transaction’s signature hash. the direct idea would be to pass R as input too, but the public input of scheme 0x11 is one 32-byte data_hash, and the signature hash takes it. so R goes into the program as a constant. the wallet compiles the program for its own root, and the account stores the key hash of that program in place of the root.
the account then checks for one triple, (0x11, 0, vk_hash). no merkle path, no hashing in the evm. a new root is a new program and a new hash, so add, rotate and revoke are still one 32-byte write.
it is the same shape as a required ci check. the maintainer does not read the pull request, he looks for a green check from one exact workflow file. the file is the program, its hash is what the account stores, the green check is the proof.
i ran the proof side on leanvm (b977f5fa), outside any node:
- 8 keys in the tree, one of them signs a 32-byte input. 11,776 vm cycles
- proving takes 0.1 to 0.3 s on a 12-core server. the proof is 162 to 235 KiB, verification is 10 to 20 ms
- the proof is refused for another input, and by the program of another root
- a key outside the tree, a member’s signature over another input, and a revoked key after a rotation give no valid run, so there is nothing to prove
what this does not show: 0x11 in a client (the scheme is reserved in the current draft), the zero data_hash rule, the contract side, and the block aggregator taking such a proof. leanvm’s recursion guest verifies raw signatures and proofs of itself today, not proofs of another program.
what step 2 buys:
- the key set stays off chain. the block stark does not show which key signed or how many keys the account has. vitalik’s recent post lists this for 2030 as “privacy of account policy”
- any policy fits one proof, like 3 of 5 keys, and the contract does not change
what it costs:
- gas. the triple costs 30000, step 1 costs 3000 plus a few keccak calls
- size. the mempool carries a 162 to 235 KiB proof per transaction until it is aggregated. a leansphincs signature is 4.9 kB
- the privacy is not there yet. leanvm proofs are not zero knowledge, so mempool peers see the direct proof with no hiding guarantee
- upgrades. the key hash changes when the proof system changes, see the isa post. an account that stores only that hash can lock itself out. the erc helps here: the program key can be one leaf in the tree, next to a plain key from step 1, and it is rotated like any other authority
- the account takes the one stark the public mempool allows per transaction
open questions
- will the
0x11key commit to the whole program, as leanvm does today? step 2 needs that - can the block aggregator take proofs of many small per-account programs, and at what cost?
- is a zero
data_hashthe right way to name the signature hash, or should such signatures move out of frame data?
links
- erc-8403: Add ERC: Account Authority Lifecycle by AnkushinDaniil · Pull Request #1979 · ethereum/ERCs · GitHub
- discussion: https://ethereum-magicians.org/t/erc-8403-account-authority-lifecycle/29570
- eip-8288: EIP-8288: In-mempool signature and proof aggregation
- demo code: GitHub - AnkushinDaniil/erc8403-demo: Runnable demo of ERC-8403 (Account Authority Lifecycle) on two local Nethermind nodes · GitHub
- isa post: Exposing an ISA for post-quantum proofs on Ethereum