Skip to content

Recover, cancel, finish and rekey vaults - #1016

Open
Rigidity wants to merge 4 commits into
rigidity/vault-spendfrom
rigidity/vault-recovery
Open

Rigidity wants to merge 4 commits into
rigidity/vault-spendfrom
rigidity/vault-recovery

Conversation

@Rigidity

@Rigidity Rigidity commented Nov 15, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #954. Part of #945. Stacked on #1014.

Summary

Vaults can now be recovered, have a recovery cancelled or finished, and be rekeyed:

  • Start a recovery with the vault's recovery key, from the keychain or an entered mnemonic, which is then stored. The new custody key is a new mnemonic, a keychain key, or a key derived from a BLS wallet's seed. The vault is recreated in its recovery state with a memo of the new custody.
  • Cancel a pending recovery with the custody key.
  • Finish a recovery once its timelock has passed, which needs no signature.
  • Rekey instantly with the custody key: rotate the custody key, change the recovery key or the timelock. A rekey always moves the vault to the newer puzzle version, so it also upgrades Legacy vaults. It writes a memo and prompts for a config backup.
  • Alert when a recovery appears that this Sage instance didn't start: a red banner on every page with a one-click cancel, plus an OS notification through the browser Notification API.
  • New endpoints: start_vault_recovery, cancel_vault_recovery, finish_vault_recovery and rekey_vault. get_vault reports the pending recovery and why it can't be finished yet, if it can't.

Decisions

  • Recoveries without a memo are caught. Sync records every recovery start from the spend itself, in migrations/0012_vault_recoveries.sql. One is accepted only if it rebuilds the coin's puzzle hash from the earlier config. Before this, a recovery started without a memo left the vault showing as Active. Such a recovery can be cancelled, but not finished, since only the memo reveals the new custody.
  • "Started by this wallet" is a flag set when Sage submits a recovery. Sync never clears it. The alert only shows for recoveries without it.
  • Finish policy: Sage only finishes a recovery whose new custody includes a BLS key whose secret is in its keychain. Otherwise get_vault says why: the custody is unknown, the key isn't ours, or the timelock hasn't expired.
  • Fees:
    • Starting and finishing can't pay a fee from the vault, since the recovery path forbids it. An optional fee comes from a BLS wallet chosen by fingerprint, spent in the same transaction. That wallet isn't syncing while the vault is open, so its coins are checked against the chain first.
    • Cancelling and rekeying pay the fee from the vault's XCH.
  • Chain time only: the countdown uses the latest synced block timestamp, plus the peak's timestamp fetched from a peer, never the local clock.
  • Signing: the generic signing path refuses spends that change custody. So these endpoints sign the spend themselves when auto_submit is set, with the vault's keys and the fee wallet's.
  • Refusals: recovery and rekeying are refused while a vault spend is pending.
  • Shared builders: the copied start, cancel and finish helpers in the sync tests, RPC tests and sandbox now use the production builders.

Deferred

  • OS notifications use the browser Notification API. On desktop they need tauri-plugin-notification to be reliable. The in-app banner shows regardless.
  • A recovery is only recorded if the vault's earlier config is known when the spend syncs. A config imported later doesn't pick up an earlier recovery.
  • The UI offers a new mnemonic or a derived key for new keys. The API also takes an existing keychain key.
  • The sandbox has no block timestamps, so the countdown and the finish button can't be tried there. The simulator tests cover finishing with time travel.

Testing

  • sage-wallet: 7 new simulator tests:
    • start then cancel, and start then finish with time travel, for both puzzle versions
    • rekey, and upgrading a Legacy vault
    • a recovery without a memo
    • refusal while a spend is pending
    • the old key refused after a finish
  • sage-rpc: 4 new tests:
    • cancel and finish
    • an early finish erroring
    • a recovery started elsewhere, flagged and cancelled
    • a recovery to a key Sage doesn't hold, refused
    • rekey
  • Sandbox, on the top of the stack:
    • Minted a vault from alice, rekeyed it from the vault page, and spent from it with the new key.
    • Started a recovery from the vault page and cancelled it.
    • Imported the sandbox's recovering vault, which bob started recovering, and attached alice's custody key. The banner appeared, and its Cancel recovery button cancelled the recovery.
  • Checks: strict clippy and the sage-wallet and sage-rpc tests passed on this branch alone. Since then this branch has only gained the banner styling, which is frontend-only. The full set passed at the top of the stack: prettier, lint, tsc -b, unit tests, fmt, clippy, workspace tests, machete, the sqlx check, and a bindings regen with no changes.

Screenshots

Recovery pending A recovery this wallet didn't start
Recovery pending Alert

@Rigidity
Rigidity added this pull request to stack #970 November 15, 2026 05:50
@Rigidity Rigidity changed the title rigidity/vault recovery Recover, cancel, finish and rekey vaults Nov 15, 2026
…ecoveries from their spends

A recovery is started through the vault's recovery side, which recreates it
as a 1 of 2 of its custody and a timelocked member that finishes it. The
spend that starts a recovery reveals that member even if its memo is left
out, so sync records every recovery it sees, and whether this wallet started
it, so that one started elsewhere can be cancelled. Finishing needs the
recovery's memo, since that's the only place the new custody is revealed.

The recovery and rekey builders replace the test-only copies in the sync
tests.
…'s pending recovery

start_vault_recovery signs with the vault's recovery key, or a mnemonic that's
then attached, and moves the vault to a new custody key. A recovery can't pay
its own fee, so a fee is paid by a BLS wallet in the same transaction.
finish_vault_recovery only finishes a recovery whose new custody has a key in
the keychain, since finishing needs no signature. get_vault shows whether this
wallet started the pending recovery, and why it can't be finished yet,
measured against block timestamps rather than the local clock. The
confirmation summary shows what a spend through the recovery side does.
… about recoveries this wallet didn't start

The alert shows on every page of a vault wallet until the recovery is
cancelled or finished, with a one-click cancel, and a system notification
where the webview supports them. The countdown is measured against block
timestamps.
@Rigidity
Rigidity force-pushed the rigidity/vault-recovery branch from e3f7cc7 to 1956fca Compare November 15, 2026 06:00

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Recover, cancel, and rekey vaults

1 participant