winnow

Back up and restore

Keep the backup file and your recovery words. The file restores known funds and history; signing recovery also requires the phone’s key.

Before you restore

Automatic iCloud recovery. Winnow automatically saves an encrypted copy including this phone’s signing key when iCloud is available. Beginner mode shows the backup status and does not require a recovery-word checklist. The wallet remains usable without iCloud, with a Not backed up warning and a manual backup shortcut. Advanced contains manual exports, recovery words and the switch to disable automatic cloud backup. On a replacement device, select the automatically discovered iCloud backup with the same Apple Account and Passwords & Keychain enabled. Winnow finds saved backups for the selected network and asks you to authenticate before restoring. Keep a manual file and recovery words too; cloud and Keychain synchronization can be delayed. Updates wait for pending payments to confirm. New backups also include saved people, payment cards, sender names, receive labels, display name and interface mode. Restoring resumes automatic backups using an independent record for the replacement device. Other owners’ signing keys are not included; older cloud copies lack names and labels.

  1. Wait for pending sends to confirm, then choose Back up wallet and save a fresh file.
  2. Keep recovery words separately. The default file excludes them; choosing Include recovery phrase creates a file containing your phone’s signing secret.
  3. Back up every other signer separately. Your file includes shared accounts and tracked funds, but not another signer’s private key.
  4. Import a file from your own trusted wallet. Review the catch-up report and keep the original backup until the restored wallet is checked.

On a replacement device, a backup without recovery words restores a watch-only wallet. The manual JSON import screen does not accept the words separately. To restore signing through this screen, the imported backup must include them. You can create that file with Include recovery phrase on the original phone; protect it like the words themselves.

Winnow scans forward from the file’s recorded height. It does not search the entire past chain from a recovery phrase. Older files may omit shared accounts, so make a fresh backup after creating one.

What a backup cannot restore

Backup file format

JSON schema and validation rules

JSON, one object:

{
    "version": 2,
    "network": "signet",
    "descriptor": "tr([fp/86'/1'/0']tpub…/<0;1>/*)#checksum",
    "mnemonic": "word …",
    "lastKnownHeight": 150000,
    "nextReceiveIndex": 4,
    "nextChangeIndex": 2,
    "utxos": [
      {
        "txid": "<display hex>",
        "vout": 0,
        "amount": 50000,
        "scriptPubKey": "5120…",
        "chain": 0,
        "index": 3,
        "height": 149000,
        "isCoinbase": true
      }
    ],
    "transactions": [
      {
        "txid": "<display hex>",
        "height": 149000,
        "received": 50000,
        "spent": 0,
        "fee": 250,
        "replacedBy": "<replacement transaction id, when fee-bumped>"
      }
    ]
  }

Rules the importer enforces before any network call:

Writers emit version 2. Readers accept versions 1 and 2 for ordinary descriptor-derived coins; version compatibility does not enable unsupported signing data.

Shared and extra-device accounts are included in current exports. Each participant still needs their own key backup. See shared-wallet backups.


3. Verification

Wallet.importing seeds state exactly as the bundle claims. That is intentional: the phone must be able to watch the claimed scripts in order to check them. watchScripts() unions every known UTXO script onto the descriptor gap window — BIP158 basic filters include the prevout scriptPubKey, and without that union a later spend would never fetch the block. Wallet.verifyImport then runs FilterSync from lastKnownHeight + 1 and builds an ImportReport:

Field Meaning
scannedFromHeight / scannedToHeight The forward window actually scanned.
confirmedUTXOs Claimed coins remaining after the scan. This does not independently prove their historical existence or amount.
spentSinceBundle Claimed UTXOs the scan saw spent. Stale or wrong bundle.
discoveredUTXOs Found by the scan, absent from the bundle (payments after export). Informational.

matchesBundle is spentSinceBundle.isEmpty. A stale export (you spent after you wrote the file) is a report the user can see, not a silent rewrite. Discovered UTXOs are applied — the point of the forward scan is to catch up — but a claimed-and-spent coin is never left in the spendable set as if the bundle were current.

The report core (ImportReport.make) is a pure function of the bundle plus the MatchEffects of the scanned blocks, so it is unit-tested without a network. Import is outside the current payment journey. An earlier UI suite captured the report (14-import-report.png); its 18.79s verification measurement was recorded on 2026-08-17 using a signet iPhone 17 Pro Max simulator (screenshots/timings.json), not the current test.

Cost is proportional to how stale the bundle is, not to how old the wallet is. A current bundle needs little catch-up work; the phone still synchronizes headers and validates the file.


4. What a lying bundle can and cannot do

The bundle is trusted as a starting claim, then checked against filters. That is a weaker assumption than “the file is true,” and a stronger one than “the file is untrusted input from the internet.”

A malicious bundle cannot:

A malicious bundle can:

This is the import analogue of read-side §2.7.1 (filters can lie by omission). Here the file can lie by omission or by a too-high height. The chain check catches spends inside the scanned window. It does not reconstruct a past the bundle refused to include.


5. Export (the writer)

Winnow writes version 2 JSON and reads versions 1 and 2. Back up wallet (in Advanced settings, or the manual-backup shortcut when beginner mode is not backed up) writes descriptor + known UTXOs + history (including fee when known) + the next unused receive and change indices + lastKnownHeight = the live FilterSync frontier (nextScanHeight − 1). The app persists that frontier back into WalletState after each sync pass — apply(match:) alone does not move it — so an export after ordinary app use resumes where the phone actually stopped, not at the creation/import height. The mnemonic is excluded by default; including it is a hot backup and takes an explicit toggle plus a confirm. The screen confirms whether recovery words are included and offers a save/share button. It does not display the JSON or recovery words. The share-sheet file is a unique, backup-excluded temp that is deleted when the backup screen closes, the app goes into the background, the seed toggle flips, or the write fails. A missing keystore entry or an xprv-only wallet throws mnemonicUnavailable rather than a raw keystore error.

A Winnow-native wallet that is never exported cannot be recovered here from the 12 words alone — that is the no-back-scan rule, not a missing feature of import. Export is the other half.

Pending (height 0) change outputs stay in the UTXO set: they are live spendable state. Export is refused while a send is still pending — commit has already removed the parent inputs, so a mid-send bundle would ship only the unconfirmed change. If that send never confirms, a forward-only restore cannot put the original coins back. This also applies to shared and extra-device account payments. Wait for the send to confirm, then export.


6. What this is not


Keep the original backup

A successful forward scan checks activity within its scanned window. Missing older coins, invented claims and an incorrect starting height remain reasons to trust the source of the file, not just the report.