Skip to content

feat(wallet): implement NUT-XX Efficient Wallet Recovery#1735

Draft
a1denvalu3 wants to merge 8 commits intocashubtc:mainfrom
a1denvalu3:feat/nut-xx-efficient-wallet-recovery
Draft

feat(wallet): implement NUT-XX Efficient Wallet Recovery#1735
a1denvalu3 wants to merge 8 commits intocashubtc:mainfrom
a1denvalu3:feat/nut-xx-efficient-wallet-recovery

Conversation

@a1denvalu3
Copy link
Copy Markdown
Contributor

Summary

This pull request implements the new NUT-XX Efficient Wallet Recovery specification for the cdk wallet, reducing recovery complexity from O(T) to O(log N) and maintaining the required Depth Invariant.

Changes

  • Database Schema: Added keyset_counter to ProofInfo across SQLite and PostgreSQL, with corresponding migrations.
  • Depth Invariant: Added Wallet::ensure_depth_invariant that triggers a consolidation swap if unspent proofs exceed the T - d threshold or the compaction limit d.
  • Sagas Integration: Injected keyset counters on recv_proofs/change_proofs correctly across issue, melt, receive, and swap sagas to maintain accuracy.
  • Fast Recovery Algorithm:
    • Implemented find_t (binary search).
    • Implemented scan_gap (gap scan) to handle gaps securely.
    • Bound the new recovery behind Wallet::restore_fast.
  • Options & Compatibility: Replaced restore logic to accept a new RecoveryOptions with RecoveryStrategy::Fast (default) and RecoveryStrategy::LinearScan to fall back to the old NUT-13 scan.
  • FFI & CLI Support: Added bindings for the RecoveryOptions across UniFFI (cdk-ffi) and added a --legacy-scan flag to the cdk-cli for backwards compatibility.
  • One-Time Consolidation: Automatically run ensure_depth_invariant at startup when Wallet::recover_incomplete_sagas verifies keysets to migrate pre-existing keyset_counter = NULL proofs.

@github-project-automation github-project-automation bot moved this to Backlog in CDK Mar 16, 2026
@a1denvalu3 a1denvalu3 marked this pull request as draft March 16, 2026 12:51
@a1denvalu3 a1denvalu3 removed the status in CDK Mar 16, 2026
@a1denvalu3
Copy link
Copy Markdown
Contributor Author

a1denvalu3 commented Mar 16, 2026

The compaction logic is handled primarily through an automatic, internal SwapSaga that is triggered whenever the wallet detects that the 'Depth Invariant' ( $T - d$) constraint is at risk.

Here is the specific breakdown of how it works:

  1. Triggering ensure_depth_invariant:
    Before running any wallet operation that generates new signatures (like a standard swap, receive, or issue), the wallet calls Wallet::ensure_depth_invariant(keyset_id, new_outputs). This predicts what the keyset counter ($) will be after the operation.

  2. Evaluating the Threshold:
    It fetches all Unspent proofs for the active keyset and checks three conditions against the depth limit ( = 100$):

    • Count Limit: If the total number of unspent proofs for the keyset plus the new_outputs exceeds 00$, all proofs for that keyset are marked for consolidation.
    • Depth Limit: Otherwise, it checks each proof's keyset_counter ($). If \le T - 100$, the specific proof is too old and is marked for consolidation.
    • Legacy Proofs: Any proof with a missing keyset_counter (e.g., loaded from an older DB schema) is automatically marked for consolidation.
  3. Executing the Compaction Swap:
    If any proofs are marked, the wallet instantiates an internal SwapSaga to exchange the offending proofs for fresh ones.

  4. Bypassing the Loop (skip_invariant: true):
    A standard SwapSaga naturally checks the depth invariant before running. Since this compaction is a swap, doing an invariant check inside the compaction swap would result in an infinite recursive loop. To solve this, the internal swap is initialized via saga.prepare(..., skip_invariant: true).

  5. Mint Interaction:
    saga.execute().await? is called. The selected, outdated proofs are sent to the Mint to be melted down, and new proofs are issued in their place.

Because the new proofs are freshly issued, their keyset_counter values represent the newest sequence indices at the top of the derivation path. This safely resolves the gap, compacts the outputs, and guarantees the entire wallet state successfully meets the strict (\log N)$ fast-recovery invariant before the user's primary operation proceeds.

- Add keyset_counter to ProofInfo for Depth Invariant tracking
- Add SQL migrations for keyset_counter in SQLite and Postgres
- Implement ensure_depth_invariant to enforce keyset_counter > T - d and compaction
- Implement O(log N) binary search find_t for fast recovery via restore
- Implement scan_gap to handle network gaps in nonce space during fast recovery
- Expose RecoveryOptions and RecoveryStrategy (Fast and LinearScan)
- Support automatic consolidation on startup for missing keyset_counter proofs
- Expose fast recovery options through cdk-ffi and cdk-cli
@a1denvalu3 a1denvalu3 force-pushed the feat/nut-xx-efficient-wallet-recovery branch 2 times, most recently from 93283cf to c40125d Compare March 20, 2026 08:03
@a1denvalu3 a1denvalu3 force-pushed the feat/nut-xx-efficient-wallet-recovery branch from c40125d to c121a60 Compare March 20, 2026 08:10
Ensure that the NUT-XX depth invariant is checked and maintained when generating new proofs during mint and melt (change) operations.
Added a multi-threaded integration test to verify the depth invariant is properly
maintained when melting tokens concurrently.
Implemented the addition to NUT-27 from the NUT-342 (Efficient Wallet Recovery)
spec proposal, which adds the nut_342 boolean flag to the backup payload to
signal that the wallet maintained the depth invariant.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant