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. Recovery words restore the wallet, not your shared multisig accounts: only a backup file or the iCloud copy carries those.
Before you restore
Automatic iCloud backup. Winnow saves an encrypted copy containing this phone’s signing secret and wallet state when iCloud is available. Simple mode shows upload status and offers a manual backup shortcut when iCloud is unavailable. Advanced contains manual export, recovery words and the switch to disable automatic backup. A successful save date confirms the upload; keep a manual file and recovery words as well. Payments not yet confirmed are in it too, so a restore announces them again. The encrypted copy also includes saved people, payment cards, sender names, receive labels, display name and interface mode. To restore on another device, use the same Apple Account with iCloud and Passwords & Keychain enabled. The steps below describe manual file backup and import.
- Choose Back up wallet and save a fresh file. A payment not yet confirmed goes in the file with the coins it spends.
- 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 multisig accounts and their 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 multisig 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 another signer’s or a Ledger’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.
- Shared accounts from words alone. Recovery words restore this wallet’s key and nothing else. Multisig accounts come back only from a backup file or the iCloud copy.
- 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, in iCloud. iCloud updates wait for pending payments to confirm, so the saved date can lag. A manual file carries the pending send.
- 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.lastKnownBlockHashis optional: the display hex hash of blocklastKnownHeight. Writers include it once the wallet has recorded it, so a later restore check can compare where it starts against headers; this version records it without checking it. Older files omit it.vaultsis an optional list of multisig accounts: each account'str(NUMS, sortedmulti_a(k, …))descriptor, address positions and tracked coins, under the key the shared-account Winnow used for all its accounts. Old files omit it. The app validates every account before replacing local state, then watches them during the same forward scan as the everyday wallet. A file carrying any other kind of account, such as a MuSig2 one, is refused, so those funds are never silently dropped from a restore. Other signers' private keys are never included. Each account also carries itshistory(the same entries astransactions, for the account's own receives and spends), at most onedraft(the PSBT this phone is collecting signatures for, as base64, with the person it pays) andinFlightspends (transactions this phone broadcast that no block has taken, witness included, so a restored phone can announce them again or give up on them). Old files omit all three. A backup no longer waits for an account's payments to confirm: pending coins and reserved inputs travel as they are, and the restored phone keeps waiting for the same blocks.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. The encrypted iCloud backup 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.blockHashon a history entry is optional: the display hex hash of the block the transaction confirmed in, recorded so a later restore check can compare the entry against headers instead of fetching the block; this version records it without checking it. A pending entry (height 0) never carries one; a malformed or pending hash is refused. Older files omit it.signersis an optional list of the signers the phone knows (Settings → Signers): the Ledgers it paired and the keys other people sent, each a name with one or more key expressions, and a paired Ledger's master fingerprint. Public keys only. Old files omit it. The list is not a wallet's, so on restore it joins the phone's own rather than replacing it: a signer already on the phone stays, one it does not have is added. A list that does not parse is refused with the file, never restored in part.
Writers emit version 2. Readers accept versions 1 and 2 for ordinary descriptor-derived coins; version compatibility does not enable unsupported signing data.
Multisig accounts, their Ledger registrations and the signer list are included in current exports, file and iCloud alike. Each signer still needs their own key backup. A file from the shared-account Winnow that carries a MuSig2 account is refused; restore it in that app instead.
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 Simple 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. Since 0.9.2 a send no block has taken yet goes in the bundle's pendingSends: its raw transaction, the coins it spends, its change and fee. Import checks each one against the descriptor (its inputs are exactly those coins, the wallet's own, none spent twice), keeps the coins reserved by it and hands the transaction back to the relay, so the restored phone keeps announcing it until a block takes it. Shared accounts carry their drafts and spends in flight the same way.
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.