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.
- Wait for pending sends to confirm, then choose Back up wallet and save a fresh file.
- Keep recovery words separately. The default file excludes them; choosing Include recovery phrase creates a file containing your phone’s signing secret.
- Back up every other signer separately. Your file includes shared accounts and tracked funds, but not another signer’s private key.
- 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
- Other signers’ keys. Neither the iCloud copy nor a file holds a Core cosigner’s, extra device’s or other owner’s key. Back each one up separately.
- Signing from a file without words. It restores a watch-only wallet; the import screen has no separate word entry.
- Coins from words alone. Winnow scans forward from a backup’s recorded height and never searches the past chain from a phrase.
- A send still pending. Manual export is refused until it confirms, and iCloud updates wait for it, so the saved date can lag.
- Approvals in progress. MuSig2 nonce and signing sessions are not saved; start a new exchange after recovery.
- App context in a file. Saved people, payment cards, sender names and receive labels are only in the iCloud copy.
- Watch-only and xprv-only wallets in iCloud. Automatic backup needs recovery words; use a file for these.
- Device settings. Peer routing, manual peers and the explorer choice stay on the old device.
- Another Apple Account. A cloud backup stays with the first Apple Account that received it; later account changes cannot overwrite it.
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:
versionis1or2. Version 1 remains readable for existing descriptor-only exports; writers emit version 2. Unknown versions fail closed.networkissignetormainnet(the two networks the app opens).- At least one of
descriptorormnemonicis present. - If both are present they must agree: the BIP86 descriptor derived from the mnemonic equals the bundle descriptor, or import throws
descriptorMismatch. - Mnemonic-only: the app derives the canonical
tr([fp/86'/coin'/0']xpub/<0;1>/*)and stores the mnemonic in the keychain (spendable). - Descriptor-only: watch-only. The descriptor must be that same single-sig shape. Signing is impossible until a secret is supplied by some later path (there is none today — watch-only stays watch-only).
- For each UTXO,
scriptPubKeymust be exactly what the descriptor derives at(chain, index). A bundle cannot attach an arbitrary output to this wallet by lying about coordinates. - Legacy
silentPaymentTweaksigning data is unsupported. A bundle carrying it is refused so the metadata cannot be silently lost. Keep the original wallet file and use compatible wallet software to recover those coins. isCoinbaseis emitted only when true. It preserves the consensus 100-confirmation maturity rule across export and restore; older bundles omit it and are treated as false.txidvalues are display (big-endian) hex on the wire and in the file; internally they are reversed to Bitcoin’s byte order, same as the rest of the stack.chainis0receive /1change (BIP44 external/internal, matching the multipath descriptor).- Scanning resumes at
lastKnownHeight + 1.creationHeightof the new wallet state islastKnownHeight. nextReceiveIndex/nextChangeIndexare the next unused BIP86 derivation indices. Writers emit them so a restore does not reissue an address whose coins have already been spent (UTXO-derived maxima go to 0 when every receive coin is gone). Older files omit the keys; the importer then usesmax(claimed index) + 1, and never goes below that floor if a file names a smaller cursor.vaultsis an optional list of shared and extra-device accounts, including each account’s descriptor, address positions, and tracked coins. Old files omit it. The app validates ownership before replacing local state, then watches these accounts during the same forward scan as the everyday wallet. Live signing nonces and the other devices’ private keys are never included.rawTransactionon a history entry is optional base64-encoded transaction data without witnesses. Its transaction ID must match the entry. It supplies the addresses and amounts shown when you open a payment. Older backups remain readable; when details are missing, Winnow can fetch that one known block from a Bitcoin peer, check its hash and Merkle root, and find the transaction. This does not rescan history or change balances. Recipient names and address counters stay in the phone’s separate local recipient file; they are not included in the manual wallet file. Encrypted iCloud recovery includes this app context.feeon a history entry is optional. The writer includes it only when every input of that transaction was ours (the only case a filter client can compute the fee exactly). Older v1 files omit the key; readers treat absence as unknown. This is display state — omitting it does not create or destroy money. The on-deviceobservedFeeRatessamples FeePolicy uses are not in the bundle; a restored wallet falls back to the static presets until it observes new sends.
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:
- Make this wallet spendable for an output it does not derive. ScriptPubKey /
(chain, index)disagreement fails import. - Change the descriptor behind a mnemonic. Mismatch fails import.
A malicious bundle can:
- Invent an older coin or misstate its amount while supplying a script the wallet derives. A forward scan does not prove that historical output exists. The claim may remain in the displayed balance and report; Bitcoin would reject an invalid spend.
- Omit a UTXO you actually own. Verify will not invent it unless a forward-scan block happens to pay you again (or still contains it — it won’t, it’s old). Omission of old coins is the residual lie, and it is the same lie as any incomplete export. The mitigation is: export from software you trust, inspect the report, do not import a bundle a stranger handed you as a way to “recover” funds.
- Omit history entries. The history list is for display; the spendable set is the UTXOs. Missing history does not create money.
- Point
lastKnownHeighttoo high, so the forward scan skips the window in which a claimed coin was spent. ThenspentSinceBundlestays empty and a spent coin looks live until some later spend attempt fails, or a restore from a trustworthy earlier backup. A too-high height is the dangerous lie. The importer does not currently prove the height against a checkpoint the previous wallet signed. Mitigation: treat a bundle from an unknown height as suspect; prefer exports that name a height you can see on a header explorer you already use; the report shows the window that was actually scanned. - Point
lastKnownHeighttoo low. That only makes the forward scan longer. Harmless except for bandwidth.
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
- Not a BIP39-only import. A 12-word phrase without history would force a back-scan. The onboarding UI does not offer “type your words and we’ll find your coins.”
- Not an xpub watcher that asks an indexer. Descriptor-only import is watch-only and still uses filters, from the bundle height forward. External explorer links never get to skip the bundle.
- Not SLIP-39 / Passport / SeedQR / output descriptors of every flavor. v1 is the everyday BIP86 wallet this app itself creates. That is the export we can specify and the import we can verify. Broader descriptor import is a format-version bump, not a silent widening of
tr(KEY).
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.