Recovery & bootstrap
A SPHINCS signature is dispatched purely by its 3,688-byte length and is accepted in both account states. Its calldata tail carries two addresses instead of one:
userOp.callData = [ any account call ][ 20 bytes currentKey ][ 20 bytes nextOwner ]On success the account emits BackupSignerUsed, sets activated = true if it was not already, and calls _rotate(currentKey, nextOwner): currentKey is burned (even though it never signed and whatever state it was in) and nextOwner is activated (it must be fresh, i.e. AUTH_NONE). The backup key itself is untouched. Everything below is a use of that single rule.
Recovery
Use this when the FORS+C rotation chain is broken: the device holding the active key is gone, the wallet's local state no longer matches the chain, or a bug left the account with no active key.
Identify the stuck key
Read OwnerRotated events (or authState(addr)) to find the key the wallet believes is active. If none can be found, any throwaway address works as currentKey; burning an unknown address is harmless.
Derive a fresh key
Derive a new FORS+C keypair S_r from a key stream that has never been used on this account. Its address must be AUTH_NONE, otherwise validation reverts with next owner not fresh.
Sign with the cold key
Build the UserOp with whatever call is needed (it can be a no-op execute or the real transaction), append currentKey ‖ addr(S_r) to the calldata, and sign userOpHash with the SPHINCS backup key.
Continue on FORS+C
After inclusion, S_r is the active key. The wallet resumes normal FORS+C rotation from there and puts the backup key back in cold storage.
Recovery never touches other devices' chains. If device B still has an active key, it keeps working; only the key named as currentKey is burned.
Cross-chain bootstrap
The activation Merkle root commits to a fixed chain list. On a chain that was not in that list there is no proof to give, but the backup key is chain-independent and already bound into the address, so it can activate the account there:
callData = execute(...) ‖ bytes20(throwaway) ‖ bytes20(addr(S_0))
signature = SPHINCS_sign(backupKey, EntryPoint.getUserOpHash(userOp))The pre-activation SPHINCS op flips activated, burns the throwaway currentKey (the zero address is fine, nothing is active yet), and activates S_0. From then on the chain is used exactly like a committed one. Sign once per chain: userOpHash includes the chain id, so nothing is replayable.
The infrastructure must exist on the new chain first. Deploy the verifiers and factory deterministically (script/Deploy.s.sol or deploy-4337/), then let the bootstrap UserOp's initCode create the account at the same address it has everywhere else. See Multichain addresses.
Co-equal use
Nothing restricts a SPHINCS signature to emergencies. It can authorise any calldata, exactly like a FORS+C op, and it always ends by re-seeding the FORS+C chain. Reasons to avoid it for routine use anyway:
- Gas. Verification costs about 105k versus about 35k for FORS+C, and the signature is 3,688 bytes of calldata instead of 2,448.
- Exposure. Every use means the cold key leaves cold storage.
- Budget. The key supports up to 2^22 signatures. That is plenty, but routine use means counting.
Enrolling a device from the backup
Because addSigner is reachable through the account's own execute, a single SPHINCS op can both recover and enroll:
callData = execute(address(this), 0, addSigner(addr(S^B_0))) ‖ bytes20(currentKey) ‖ bytes20(addr(S^A_0))Validation activates S^A_0 (device A's new chain); execution activates S^B_0 (device B's chain). Both devices then rotate independently. See Signers & devices.
What is not covered
- A compromised backup key. The backup is co-equal and never rotated, so its compromise is a full compromise of the account. There is no onchain path to replace it today; the mitigation is keeping it cold and monitoring
BackupSignerUsed. - A lost backup key. The account keeps working on FORS+C, and new devices can still be enrolled from any active device, but recovery from a broken rotation chain and bootstrap on uncommitted chains are no longer possible. The seed-derivation rules for making the backup key re-derivable from the master seed are discussed in the protocol spec's Annex B.