Orchard Migration Flow — Implementation Proposal (Path A)
1. Background
Following a security concern affecting the existing Orchard shielded pool, user funds need to be migrated to the new Ironwood pool. Within a single pool, balances and transactions remain private. However, when funds move between pools the amount crossing over is visible on-chain. To reduce the linkability of individual transfers to a specific wallet, the migration splits each user's balance into several smaller transfers spread over time with randomised anchor heights, so individual amounts and timing are harder to correlate.
Note: Ironwood addresses are identical to the user's existing Orchard address. The wallet data model will treat Ironwood as a distinct pool with its own identifier — no special address handling or memo-based tagging is needed. Self-send detection is handled entirely by the wallet's account logic.
Note: Previous migration - https://zips.z.cash/zip-0308
2. Chosen Approach: Background Scheduled Sending (Path A)
The migration runs as a two-phase process: a note-splitting phase followed by a transaction creation and scheduling phase. Sends are scheduled in the background and de-correlated from user behaviour. Where a scheduled window is missed, the app falls back to prompting the user on the next open and shifts the remaining schedule by the offset.
This approach was chosen after the initially favoured Path B (guided on-open sending) was reconsidered. Kris Nuttycombe (ZODL): "This is problematic; it means that we can't properly temporally de-correlate sends. I think we should instead take a scheduled approach. Statelessness and multi-device/recovery support are not requirements, and actually make what we want to do harder."
Why Path A
- Sends are de-correlated from when the user opens the app, which is the primary privacy goal.
- De-correlation works because background transfer doesn't run sync.
- Transactions can share the same anchor and they are de-correlated via different expiry and send height.
- Best-effort background scheduling is sufficient; the worst case (all sends happen on wallet open) is still no worse than Path B, while the best case (sends fully de-correlated from user behaviour) is significantly better.
- Pre-signed transactions can be stored using the existing transaction-resubmission infrastructure.
- Statelessness and multi-device recovery are explicitly not requirements for this flow (confirmed at the 17/06/2026 sync).
- Sync/transfer delay could be smaller for smaller transactions (<100ZEC).
- Sync now - transfer later vs transfer now - sync later as fallback
Dismissed Alternative: Guided On-Open Sending (Path B)
Path B was the initial recommendation. After team review it was dismissed primarily because user-triggered sends leak too much information:
- Scan ranges provided to the LWD server link all notes to a single user wallet.
- Each pool-crossing send reveals the exact amount, and if sends correlate with app opens, an observer can monitor the wallet across the entire migration.
- "This is a great way to break privacy." — Kris Nuttycombe, on using memo-based or send-on-open approaches that reveal sync timing.
Path B also provides no temporal de-correlation — every transfer is anchored at the moment of the user's interaction.
3. Two-Phase Architecture
Phase 1 — Note Splitting
Goal: Break the user's Orchard balance into appropriately-sized notes so that subsequent migration transactions do not need to wait on change from earlier ones.
- Implemented as a send-to-self (wallet-internal operation); no external receiver.
- Can happen at any time after the user initiates the migration, including before the network upgrade finalises.
- If the user transacts normally after note splitting, their experience is not degraded.
- Unhappy path: if the user does not split notes before the network upgrade, they must either perform the split afterwards or accept longer waits between migration transactions while change confirms.
- Note split is preferred and we should make it mandatory before migration.
- Kris to try some ideas for note split logic
Phase 2 — Transaction Creation and Scheduling
Goal: Broadcast migration transactions at pre-determined, de-correlated times using network-wide anchor-height buckets.
- Implemented in Rust backend and clear public interface in SDK will be available so we can start fast with mocked data and later replace it for real one.
- Anchor-height bucketing: anchor heights are chosen from shared, network-wide intervals (approximately every 6 hours / ¼ day). This prevents an observer from inferring when a specific wallet last synced. The longer the bucket interval, the stronger the privacy; the shorter, the faster the migration.
- Background scheduling (best effort): the app schedules each send for its target window. The OS delivers the task when conditions allow (charging, Wi-Fi, not in low-power mode).
- Fallback on wallet open: if a scheduled window is missed, the user is prompted to send the pending transaction on the next app open. Sync now - transfer later vs transfer now - sync later option for user.
- Sync should be decoupled from submission: background tasks must not trigger a wallet sync. Linking sync and transaction submission in the same background session would allow an observer to correlate them. If a sync is needed, it must be separated in time from the broadcast.
There could be special cases - will be resolved during impl. Best effort impl.
- One place for must sync is a transaction that outputs change back to Orchard. Spending this change requires sync.
4. Unified Cross-Platform Implementation Spec
This spec applies to both iOS and Android. Platform differences are called out where they affect implementation.
4.1 Migration Entry Point
- Show a dedicated migration screen when the user has a non-zero Orchard balance.
- Display the Orchard-specific balance at risk — not a unified total.
- Present the user with two options:
- Migrate immediately — single transfer, no delay, minimal privacy.
- Migrate with privacy — split over time using the scheduled approach; requires returning to the app if background tasks are missed.
- Show migration progress throughout: e.g. "Transfer 2 of 5 complete — next send scheduled in ~6 hours."
4.2 Transfer Split and Scheduling Algorithm
The balance is split into a set of transfers with randomised sizes. The exact split count and sizing heuristic is an **open point for the Core Team**. The schedule assigns each transfer to a future anchor-height bucket (~6-hour intervals by default).
We should aim for simple AI without many options for users as he may now know what is best.
- The app proposes the full schedule to the user before any transfer is sent.
- User confirmation is required before the schedule is committed.
- Individual transfers within the schedule do not require separate per-send confirmation once the schedule is accepted — they are broadcast automatically when their window arrives.
4.3 Destination
The destination is Ironwood only. Migration to Sapling is not offered or recommended.
4.4 Network Privacy (Tor / VPN)
Before the schedule is committed, the app presents a network privacy screen:
- No option is pre-selected or defaulted.
- A Tor toggle is offered. When enabled, all migration transaction broadcasts are routed through Tor.
- "If Tor is not available on your network or in your region, consider using a trusted VPN before proceeding."
- The user may proceed without Tor or VPN — the choice is theirs.
- A disclaimer explains the IP-correlation risk: pool-crossing transfers are on-chain visible; without network-level privacy, a server or network operator can correlate the broadcast IP with the on-chain event.
- Submission to a different server than the one sync is using could increase privacy e.g. second fastest.
4.5 Pre-Signed Transaction Storage
- Once the schedule is committed, all transactions are pre-signed and stored using the existing transaction-resubmission storage mechanism.
- Stored transactions must use distinct anchor heights so they are not linkable to each other.
- If a transaction becomes invalid (e.g. funds spent externally), the app must detect this and prompt the user to re-initiate the affected step.
- We can detect this when orchard balance is >0 and the next scheduled transaction that was not executed.
4.6 Background Scheduling
iOS
- Uses BGTaskScheduler (background processing task type for longer work).
- Scheduling is best-effort; the OS chooses the exact execution time within the requested window.
- Sync must not be triggered inside the background task. The task broadcasts only.
- If the task does not run, the next app opens the fallback prompt.
Android
- Uses WorkManager with a flex interval aligned to the anchor bucket.
- OEM battery optimisation (MIUI, One UI, etc.) may suppress tasks even with permissions granted; this is acknowledged as a known limitation.
- During onboarding, request battery-optimisation exemption to maximise reliability.
- Sync must not be triggered inside the Worker. The Worker broadcasts only.
- On every app launch, reconcile whether a pending transfer is overdue and surface the fallback prompt if needed.
4.7 Status Notifications
When a transfer is
successful we can show notification
failed - we must show notification with ability to retry, ideally separate screen that will not start sync
Android-specific: Notifications can be cleared by reboots or suppressed by OEM managers. Copy must explicitly disclaim: "Notification delivery is not guaranteed on all devices and battery modes. If you are not receiving reminders, check your battery settings and return to the app at the scheduled time."
On every app launch, the app reconciles notification state as the primary catch-up mechanism — do not rely on notification delivery alone.
4.8 Progress Visibility
- Show current step and total: "3 of 7 transfers complete."
- Show the scheduled estimated time for the next transfer.
- Show the Orchard-specific balance remaining at risk.
- Show a completion screen once all transfers are confirmed on-chain?
4.9 No "Automatic" Claims in Copy
Background scheduling is best-effort. Product copy must never describe the migration as automatic or guaranteed. The fallback (prompt on next open) must be presented as a normal part of the flow, not an error state.
4.10 Error handling
- Transaction send fails
- Network issue - retry
- Not enough funds - restart process with user?
- User reinstall app
- Migration is rescheduled
5. Open Points for Core Team
| Topic |
Status |
Notes |
| Transfer split algorithm |
Open |
Random split with randomised sizes assumed as baseline |
| Anchor bucket interval |
Open |
~6 hours proposed; longer = more privacy, shorter = faster migration |
| Privacy/speed slider for the user |
Closed |
Complicated decision and UI for user |
| Multi-server (community LWD) tx submission |
Closed |
Not broadcast but different if we want to do this |
| Sync before first scheduled send |
Open |
Define minimum sync requirement for spending pre-upgrade Orchard change |
| How to de-correlate transaction send and wallet sync |
Open |
We need to think about fallback for users where background sync never triggers |
| Note split maximum size |
Open |
Can we have just one one note split transaction for e.g. 1mil ZEC |
| Need to sync state |
Open |
How to know when sync is needed |
6. Summary
Implement background scheduled sending (Path A) on both iOS and Android with a unified two-phase flow: note splitting followed by scheduled, anchor-bucketed transaction broadcasts. De-couple sync from broadcast in background tasks. Offer Tor as an opt-in toggle with no default. Display the Orchard-specific balance and full schedule to the user before commitment. Use the existing transaction-resubmission infrastructure for pre-signed transaction storage. On missed windows, fall back to prompting on the next app open and shift the remaining schedule. Disclaim notification unreliability on Android and reconcile state on every launch.
Ironwood Migration — User Paths
Based on: Orchard Migration Flow — Implementation Proposal (Path A), sync 2026-06-17
User A — 1 ZEC, Privacy-Indifferent
Profile: Small balance, wants the migration done with minimum friction.
Pre-conditions
- The Ironwood network has been activated.
- A user has 1 ZEC in the Orchard pool (single note assumed).
Steps
1. Migration entry point
A user opens the app. The wallet detects a non-zero Orchard balance and surfaces the migration screen:
"You have 1 ZEC in the Orchard pool. This balance needs to be migrated to Ironwood."
Two options are presented:
- Migrate immediately — single transfer, no delay, minimal privacy.
- Migrate with privacy — split over time with background scheduling.
User selects Migrate immediately.
2. Network privacy screen
Tor toggle is shown (no pre-selection). User skips it:
"Continue without Tor / VPN"
A brief disclaimer is shown about IP-correlation risk. User proceeds.
3. Schedule preview
A single transfer is shown:
"1 transfer of ~1 ZEC to Ironwood — now"
User confirms.
4. Transfer executes
Transaction is broadcast immediately (foreground, no background scheduling required).
5. Completion
"Migration complete. 1 ZEC transferred to Ironwood."
Orchard balance shows 0. Ironwood balance shows 1 ZEC (minus fee).
Notes
- Note splitting is skipped or a no-op for a single small note.
- No WorkManager / BGTaskScheduler involvement.
- No notifications needed — migration finishes within the same session.
User B — 10 ZEC, Somewhat Privacy-Aware
Profile: Medium balance, understands the basics of privacy, willing to wait a day.
Pre-conditions
- The Ironwood network has been activated.
- A user has 10 ZEC in the Orchard pool across 2–3 notes.
- Battery optimisation exemption not yet granted.
Steps
1. Migration entry point
A user opens the app. Migration screen appears:
"You have 10 ZEC in the Orchard pool. Migrate to Ironwood to keep your funds secure."
The user selects Migrate with privacy.
2. Note splitting
The wallet analyses the current notes and proposes a split:
"To migrate privately, your balance will first be split into smaller notes. This is a wallet-internal transaction — no ZEC leaves your wallet."
Split proposal (example):
- Note 1: ~3.8 ZEC
- Note 2: ~2.9 ZEC
- Note 3: ~3.3 ZEC
The user confirms. The note-splitting transaction is broadcast and confirmed (1 block, ~75 seconds).
3. Schedule preview
After the split confirms, the wallet generates a migration schedule of 3 transfers with randomised amounts, each assigned to a future anchor-height bucket (~6-hour intervals / 288-block windows):
| Transfer |
Amount |
Estimated time |
| 1 of 3 |
~3.8 ZEC |
in ~6 hours |
| 2 of 3 |
~2.9 ZEC |
in ~12 hours |
| 3 of 3 |
~3.3 ZEC |
in ~18 hours |
"Your migration will complete in approximately 18 hours. Each transfer happens automatically in the background."
4. Battery optimisation exemption
Android prompt:
"To ensure automatic migration, allow Zodl to run in the background. Tap 'Allow' on the next screen."
The user grants exemption.
5. Network privacy screen
Tor toggle shown. The user proceeds without Tor.
Disclaimer displayed:
"Without Tor protection or a VPN, your network provider and the lwd server may be able to correlate your IP with on-chain transfers."
The user confirms the full schedule.
6. Background execution — Transfer 1
WorkManager schedules the first task within the next 288-block bucket. The device is charging and on Wi-Fi. The task runs at the target window:
- Pre-signed transaction 1 is broadcast.
- Transfer confirmed on-chain.
Notification:
"Zodl: Transfer 1 of 3 complete — next send in ~6 hours. 6.2 ZEC remaining in Orchard."
7. Background execution — Transfer 2
The user is asleep. The second task runs automatically.
Notification (next morning):
"Zodl: Transfer 2 of 3 complete — next send in ~6 hours. 3.3 ZEC remaining in Orchard."
8. Background execution — Transfer 3
Final task executed. All ZEC transferred.
Notification:
"Zodl: Migration complete. All 10 ZEC successfully transferred to Ironwood."
9. Completion
The user opens the app the next evening. Progress screen shows:
"3 of 3 transfers complete. Migration to Ironwood succeeded."
Orchard balance: 0. Ironwood balance: ~10 ZEC (minus fees).
User C — 10,000 ZEC, Highly Privacy-Conscious
Profile: Large balance, understands on-chain linkability, wants maximum temporal de-correlation. Enables Tor.
Pre-conditions
- The Ironwood network has been activated.
- A user has 10,000 ZEC across several existing Orchard notes.
- Battery optimisation exemption not yet granted.
Steps
1. Migration entry point
The user opens the app. Migration screen:
"You have 10,000 ZEC in the Orchard pool. This balance is at risk and should be migrated to Ironwood."
The screen displays the **Orchard-specific balance only** (not a unified total).
The user selects **Migrate with privacy**.
2. Note splitting — extended
Wallet proposes splitting into ~10 notes with randomised sizes (capped at ~20,000 ZEC per note; for 10,000 ZEC this produces notes in the ~800–1,200 ZEC range):
"To migrate privately, your balance will first be split into smaller notes. This is a wallet-internal transaction — no ZEC leaves your wallet."
Example split:
- Notes of approximately: 1,150 / 890 / 1,040 / 1,200 / 780 / 1,100 / 920 / 1,060 / 970 / 890 ZEC
The wallet waits for confirmation before proceeding (~75 seconds).
3. Schedule preview — 10 transfers over ~2.5 days
| Transfer |
Amount |
Estimated time |
| 1 of 10 |
~1,150 ZEC |
in ~6 hours |
| 2 of 10 |
~890 ZEC |
in ~12 hours |
| 3 of 10 |
~1,040 ZEC |
in ~18 hours |
| 4 of 10 |
~1,200 ZEC |
in ~24 hours |
| 5 of 10 |
~780 ZEC |
in ~30 hours |
| 6 of 10 |
~1,100 ZEC |
in ~36 hours |
| 7 of 10 |
~920 ZEC |
in ~42 hours |
| 8 of 10 |
~1,060 ZEC |
in ~48 hours |
| 9 of 10 |
~970 ZEC |
in ~54 hours |
| 10 of 10 |
~890 ZEC |
in ~60 hours |
The user reviews the full schedule. Copy:
"Transfers are spread over ~2.5 days to de-correlate them from your app usage. Each transfer broadcasts at a different anchor height so they cannot be linked to each other."
User confirms.
4. Battery optimisation exemption
Android prompt shown. The user grants exemption immediately.
5. Network privacy screen
Tor toggle shown. The user enables Tor.
Tor availability check runs. If Tor is available:
"All migration transactions will be broadcast through Tor."
If Tor is unavailable:
"Tor is not available on this network. Consider using a trusted VPN before proceeding, or continue without."
The user proceeds with Tor enabled.
6. Background execution — Transfers 1–10 over 2.5 days
Each pre-signed transaction is stored in the database. WorkManager schedules each task in its 288-block bucket. Sync is not triggered inside background tasks — the task broadcasts only.
For each completed transfer, a notification is delivered:
"Zodl: Transfer N of 10 complete — X ZEC remaining in Orchard."
The user monitors progress by opening the app periodically:
"6 of 10 transfers complete — the next send is scheduled in ~4 hours. 3,840 ZEC remaining in Orchard."
7. Change output handling
If any transfer generates change back to Orchard, the wallet detects that a sync is required before the next spend. The schedule is adjusted to include a sync step, decoupled in time from the broadcast:
"A sync is needed before transfer 8 can proceed. This will happen automatically."
8. Completion
After ~2.5 days, user opens app:
"Migration complete. All 10,000 ZEC transferred to Ironwood."
Orchard balance: 0. Ironwood balance: ~10,000 ZEC (minus fees). Full transfer history visible.
Additional User Paths (Brainstormed)
D — User Transacts During Migration
User has 10 ZEC, migration schedule committed (3 transfers). Between transfer 1 and 2, user sends 5 ZEC to an external address. The note earmarked for transfer 2 is now partially or fully spent.
On the next app open, the wallet detects an invalid state: Orchard balance > 0, but queued migration transaction is no longer valid. Prompt:
"One of your scheduled migration transfers is no longer valid because the funds were spent. Please restart this step."
The remaining unspent Orchard notes are re-evaluated and a new partial schedule is proposed.
E — User Ignores Migration for Weeks
The user has 10 ZEC. Migration entry point was dismissed. The user returns 3 weeks later.
On open:
"You still have 10 ZEC in the Orchard pool. Your migration has not started. Start now?"
All missed windows are collapsed — the schedule is created fresh from the current moment, starting at the next available anchor-height bucket.
F — User Installs App on New Device Mid-Migration
The user has 10 ZEC, 2 of 5 transfers complete. Gets a new phone and restores from seed.
The wallet detects unspent Orchard funds, treats it as a fresh migration, and prompts the user to start again. Transfers 1–2 are already confirmed on-chain; only the remaining balance needs new scheduling.
G — User Denies Battery Optimisation Exemption
Migration is scheduled but battery exemption is denied. The app proceeds but shows a persistent warning:
"Automatic migration may be delayed. Check your battery settings if transfers are not happening on schedule. You can always send it manually from this screen."
Fallback-on-open is the primary mechanism for this user. More frequent prompts expected.
H — Negligible Dust Remaining After Migration
After the last scheduled transfer, a tiny amount (e.g., 0.00001 ZEC) remains due to fee rounding. The wallet evaluates whether this is below a significance threshold. If it is:
"Migration complete. A negligible amount of ZEC remains in Orchard — no action required."
If it is above the threshold, a final small transfer is added to the schedule.
I — Note Split Generates Change Before Network Upgrade
The user initiates note splitting before the network upgrade finalises. The split transaction includes a change output back to Orchard. After the upgrade activates we can proceed to the migration step.
Status shown:
"Start migration."
J — First App Open After Network Upgrade, No Prior Note Split
The user opens the app for the first time after the network upgrade without having split notes beforehand. The wallet sets the minimum anchor height for migration at this first open. The user is guided through the note-splitting step before the schedule is proposed. Migration transactions do not proceed until the split confirms.
Ironwood Migration — Error Cases
Scope: Medium user (10 ZEC, migrate with privacy, 3-transfer schedule over ~18 hours).
Based on: Orchard Migration Flow — Implementation Proposal (Path A), sync 2026-06-17
EC-1 — Background Task Missed: OS Suppressed Window by 1 Hour
Trigger: The OS delays the WorkManager task (e.g., device in Do Not Disturb, low-battery idle, or task deferred by OS scheduler). Transfer 2 was due at 02:00, runs at 03:00 instead.
What happens:
-
WorkManager schedules transfer 2 for the 288-block bucket beginning at ~02:00.
-
The device is in Do Not Disturb mode; the task fires at ~03:00 (1-hour slip).
-
The pre-signed transaction still references a valid anchor height — it has not expired.
-
The task broadcasts the transaction successfully.
-
Notification delivered:
"Zodl: Transfer 2 of 3 complete. Next send in ~5 hours. 3.3 ZEC remaining in Orchard."
-
Transfer 3 schedule shifts by the same offset: originally 08:00, now ~09:00.
User experience: Invisible. Migration continues normally. No user action required.
App behaviour:
- On every launch, the app reconciles scheduled times vs. actual completion. If transfer 3 is now "overdue" by the shifted offset, the reconciliation catches it.
- No error screen is shown for a slip of < ~2 hours while the task eventually ran.
EC-2 — Background Task Never Runs: OEM Battery Restriction
Trigger: MIUI / One UI aggressive battery optimisation kills the WorkManager task before it fires. Transfer 2 window (12h mark) passes without execution. Transfer 3 window (18h mark) also passes. The user opens the app the next morning.
What happens:
-
WorkManager task is scheduled. OS terminates the process before execution.
-
No notification is sent (task never ran).
-
The user opens the app ~22 hours after confirming the schedule.
-
App launch reconciliation detects: transfer 2 is overdue.
-
Fallback screen shown (not an error — copy must treat this as normal):
"2 transfers are ready to send. Background delivery was delayed — this can happen on some devices."
Options:
- Send now — broadcasts both overdue transactions immediately (explain privacy trade-off: sends are now correlated with app open).
- Reschedule — re-assigns both to the next two available 288-block buckets from now.
-
After the user's choice, a guidance banner is shown:
"To allow automatic migration, check your battery settings. Notification delivery is not guaranteed on all devices and battery modes — return to the app at the scheduled time if you are not receiving reminders."
Deep-link to battery optimisation settings included.
User experience: Minor disruption. The user must actively confirm the sends. Privacy guarantee is weakened if "Send now" is chosen (both sends happen within the same app session), but this is the worst case described in Path A — no worse than Path B, still acceptable.
App behaviour:
- Fallback prompt is the primary catch-up mechanism — notification delivery is not relied upon.
- If the user ignores the prompt and closes the app, it reappears on the next launch until all transfers are complete.
- "Send now" broadcasts the pre-signed transactions in sequence (not simultaneously) to avoid bundling them in the same block.
EC-3 — Background Task Runs But Network Is Unavailable
Trigger: Transfer 2 window arrives. WorkManager task fires correctly. However, the device has no internet connectivity (airplane mode, weak signal, Wi-Fi disconnected).
What happens:
-
WorkManager task starts. The pre-signed transaction is loaded from the database.
-
Broadcast attempt fails: network unreachable.
-
Task retries within its execution window (up to the WorkManager time limit).
-
If all retries fail, the task exits. Transaction is still stored in the database — not discarded.
-
The task is rescheduled for the next available window by WorkManager's backoff policy.
-
If the next window also fails (network still down), on next app launch:
"Transfer 2 could not be sent — no network connection was available. Send now or retry later."
Options:
- Send now — user ensures connectivity and taps to broadcast (explain privacy trade-off: sends are now correlated with app open).
- Retry in background — re-schedules using the next 288-block bucket.
-
Once connectivity is restored and the transaction broadcasts successfully, notification confirms:
"Zodl: Transfer 2 of 3 complete — delayed due to connectivity. 3.3 ZEC remaining in Orchard."
User experience: One missed window. Transfer 2 is delayed until connectivity returns. Transfer 3 schedule is shifted accordingly.
App behaviour:
- The pre-signed transaction is valid until its anchor height expires (transaction expiry window). If connectivity is unavailable for an extended period and the transaction expires, the app must detect this (invalid transaction state) and fall into EC-4 handling.
- Sync is not triggered inside the background task — even if a sync could help, it must remain decoupled.
---
EC-4 — Background Task Cannot Send: Already Spent Note (Invalid Transaction)
Trigger: Between transfer 1 and transfer 2, the user makes a regular Orchard spend (e.g., pays for something). The note earmarked for transfer 2 is now fully or partially spent. The pre-signed transaction is invalid.
What happens:
-
WorkManager task starts for transfer 2.
-
Transaction broadcast rejected by the network: the input note has already been spent.
-
Task marks transfer 2 as invalid and stores this state.
-
Notification (failure — must be shown):
"Zodl: Transfer 2 could not be sent. Your migration funds were partially spent. Please open the app."
-
The user opens the app. The wallet detects the invalid state:
- Orchard balance > 0 (funds still exist, just different notes).
- No valid queued migration transaction for the remaining balance.
-
Screen shown:
"A migration transfer is no longer valid because some funds were spent. We'll create a new transfer for your remaining Orchard balance."
This is not an error screen that triggers a sync. The screen is self-contained.
- The user confirms. The wallet constructs a new migration transaction using the remaining unspent Orchard notes.
- A revised schedule is proposed for the remaining balance (e.g., if 3.3 ZEC remains, a single transfer is scheduled at the next 288-block bucket).
- User confirms revised schedule. Migration resumes.
User experience: Minor disruption. One confirmation step required. The migration does not fail permanently — it adapts to the new balance.
App behaviour:
- Detection condition: `orchardBalance > 0 AND nextScheduledMigrationTransaction == null OR invalid`.
- The new transaction must use a fresh anchor height (not the expired/invalid one).
- If the user spent most of the Orchard balance and only dust remains, the negligible-amount path applies (see User Path H in the happy-path document).
---
EC-5 — (Additional) Transaction Expires Before Broadcast
Trigger: All background tasks fail (network down for 3+ days, severe OEM restrictions). The pre-signed transaction reaches its expiry block height before it can be broadcast.
What happens:
-
On next app launch, the wallet detects: queued transaction exists but is expired (past its expiry height).
-
Orchard balance > 0, transaction state = expired.
-
Treated the same as EC-4 (invalid transaction):
"A scheduled migration transfer has expired. We'll create a new one for your remaining Orchard balance."
-
A new transaction is constructed with a fresh anchor height and expiry.
-
The user confirms. Migration resumes.
---
EC-6 — (Additional) Wallet Closed Before Note Split Confirms
Trigger: User initiates note splitting, then closes the app before the split transaction confirms on-chain.
What happens:
-
The split transaction was broadcast but is unconfirmed.
-
The user re-opens the app.
-
App detects: split transaction is pending.
-
Status shown:
"Waiting for your split transaction to confirm (~1 block, ~75 seconds). The migration schedule will be created automatically."
-
Once confirmed, migration scheduling proceeds as normal.
If the split transaction expired (very unlikely within a few blocks but possible if the node was down):
- The app detects the unconfirmed and expired split transaction.
- Prompt: "Your split transaction failed. Retry?"
- The user confirms, new split transaction broadcast.
---
EC-7 — (Additional) Reboot Clears Notification State Mid-Migration
Trigger: Device reboots between migration steps. System notifications cleared. WorkManager tasks need to be re-registered (WorkManager handles this automatically on Android if `RECEIVE_BOOT_COMPLETED` permission is declared and used).
What happens:
-
Device reboots. WorkManager tasks are re-enqueued on boot via the boot receiver.
-
Notifications that were previously shown are cleared from the notification tray.
-
No missing transfers — tasks will still execute in their windows.
-
On the next app launch, the migration progress screen shows the current state accurately:
"1 of 3 transfers complete — next send in ~4 hours."
Copy in the migration screen must account for this:
"Notifications may be cleared when a device restarts. Return to this screen at any time to check your progress."
App behaviour:
- On-launch reconciliation is the primary status mechanism, not notification delivery.
- WorkManager's persistence across reboots must be verified during implementation.
Orchard Migration Flow — Implementation Proposal (Path A)
1. Background
Following a security concern affecting the existing Orchard shielded pool, user funds need to be migrated to the new Ironwood pool. Within a single pool, balances and transactions remain private. However, when funds move between pools the amount crossing over is visible on-chain. To reduce the linkability of individual transfers to a specific wallet, the migration splits each user's balance into several smaller transfers spread over time with randomised anchor heights, so individual amounts and timing are harder to correlate.
Note: Ironwood addresses are identical to the user's existing Orchard address. The wallet data model will treat Ironwood as a distinct pool with its own identifier — no special address handling or memo-based tagging is needed. Self-send detection is handled entirely by the wallet's account logic.
Note: Previous migration - https://zips.z.cash/zip-0308
2. Chosen Approach: Background Scheduled Sending (Path A)
The migration runs as a two-phase process: a note-splitting phase followed by a transaction creation and scheduling phase. Sends are scheduled in the background and de-correlated from user behaviour. Where a scheduled window is missed, the app falls back to prompting the user on the next open and shifts the remaining schedule by the offset.
This approach was chosen after the initially favoured Path B (guided on-open sending) was reconsidered. Kris Nuttycombe (ZODL): "This is problematic; it means that we can't properly temporally de-correlate sends. I think we should instead take a scheduled approach. Statelessness and multi-device/recovery support are not requirements, and actually make what we want to do harder."
Why Path A
Dismissed Alternative: Guided On-Open Sending (Path B)
Path B was the initial recommendation. After team review it was dismissed primarily because user-triggered sends leak too much information:
Path B also provides no temporal de-correlation — every transfer is anchored at the moment of the user's interaction.
3. Two-Phase Architecture
Phase 1 — Note Splitting
Goal: Break the user's Orchard balance into appropriately-sized notes so that subsequent migration transactions do not need to wait on change from earlier ones.
Phase 2 — Transaction Creation and Scheduling
Goal: Broadcast migration transactions at pre-determined, de-correlated times using network-wide anchor-height buckets.
There could be special cases - will be resolved during impl. Best effort impl.
4. Unified Cross-Platform Implementation Spec
This spec applies to both iOS and Android. Platform differences are called out where they affect implementation.
4.1 Migration Entry Point
4.2 Transfer Split and Scheduling Algorithm
The balance is split into a set of transfers with randomised sizes. The exact split count and sizing heuristic is an **open point for the Core Team**. The schedule assigns each transfer to a future anchor-height bucket (~6-hour intervals by default).
We should aim for simple AI without many options for users as he may now know what is best.
4.3 Destination
The destination is Ironwood only. Migration to Sapling is not offered or recommended.
4.4 Network Privacy (Tor / VPN)
Before the schedule is committed, the app presents a network privacy screen:
4.5 Pre-Signed Transaction Storage
4.6 Background Scheduling
iOS
Android
4.7 Status Notifications
When a transfer is
successful we can show notification
failed - we must show notification with ability to retry, ideally separate screen that will not start sync
Android-specific: Notifications can be cleared by reboots or suppressed by OEM managers. Copy must explicitly disclaim: "Notification delivery is not guaranteed on all devices and battery modes. If you are not receiving reminders, check your battery settings and return to the app at the scheduled time."
On every app launch, the app reconciles notification state as the primary catch-up mechanism — do not rely on notification delivery alone.
4.8 Progress Visibility
4.9 No "Automatic" Claims in Copy
Background scheduling is best-effort. Product copy must never describe the migration as automatic or guaranteed. The fallback (prompt on next open) must be presented as a normal part of the flow, not an error state.
4.10 Error handling
5. Open Points for Core Team
6. Summary
Implement background scheduled sending (Path A) on both iOS and Android with a unified two-phase flow: note splitting followed by scheduled, anchor-bucketed transaction broadcasts. De-couple sync from broadcast in background tasks. Offer Tor as an opt-in toggle with no default. Display the Orchard-specific balance and full schedule to the user before commitment. Use the existing transaction-resubmission infrastructure for pre-signed transaction storage. On missed windows, fall back to prompting on the next app open and shift the remaining schedule. Disclaim notification unreliability on Android and reconcile state on every launch.
Ironwood Migration — User Paths
Based on: Orchard Migration Flow — Implementation Proposal (Path A), sync 2026-06-17
User A — 1 ZEC, Privacy-Indifferent
Profile: Small balance, wants the migration done with minimum friction.
Pre-conditions
Steps
1. Migration entry point
A user opens the app. The wallet detects a non-zero Orchard balance and surfaces the migration screen:
"You have 1 ZEC in the Orchard pool. This balance needs to be migrated to Ironwood."
Two options are presented:
User selects Migrate immediately.
2. Network privacy screen
Tor toggle is shown (no pre-selection). User skips it:
"Continue without Tor / VPN"
A brief disclaimer is shown about IP-correlation risk. User proceeds.
3. Schedule preview
A single transfer is shown:
"1 transfer of ~1 ZEC to Ironwood — now"
User confirms.
4. Transfer executes
Transaction is broadcast immediately (foreground, no background scheduling required).
5. Completion
"Migration complete. 1 ZEC transferred to Ironwood."
Orchard balance shows 0. Ironwood balance shows 1 ZEC (minus fee).
Notes
User B — 10 ZEC, Somewhat Privacy-Aware
Profile: Medium balance, understands the basics of privacy, willing to wait a day.
Pre-conditions
Steps
1. Migration entry point
A user opens the app. Migration screen appears:
"You have 10 ZEC in the Orchard pool. Migrate to Ironwood to keep your funds secure."
The user selects Migrate with privacy.
2. Note splitting
The wallet analyses the current notes and proposes a split:
"To migrate privately, your balance will first be split into smaller notes. This is a wallet-internal transaction — no ZEC leaves your wallet."
Split proposal (example):
The user confirms. The note-splitting transaction is broadcast and confirmed (1 block, ~75 seconds).
3. Schedule preview
After the split confirms, the wallet generates a migration schedule of 3 transfers with randomised amounts, each assigned to a future anchor-height bucket (~6-hour intervals / 288-block windows):
"Your migration will complete in approximately 18 hours. Each transfer happens automatically in the background."
4. Battery optimisation exemption
Android prompt:
"To ensure automatic migration, allow Zodl to run in the background. Tap 'Allow' on the next screen."
The user grants exemption.
5. Network privacy screen
Tor toggle shown. The user proceeds without Tor.
Disclaimer displayed:
"Without Tor protection or a VPN, your network provider and the lwd server may be able to correlate your IP with on-chain transfers."
The user confirms the full schedule.
6. Background execution — Transfer 1
WorkManager schedules the first task within the next 288-block bucket. The device is charging and on Wi-Fi. The task runs at the target window:
Notification:
"Zodl: Transfer 1 of 3 complete — next send in ~6 hours. 6.2 ZEC remaining in Orchard."
7. Background execution — Transfer 2
The user is asleep. The second task runs automatically.
Notification (next morning):
"Zodl: Transfer 2 of 3 complete — next send in ~6 hours. 3.3 ZEC remaining in Orchard."
8. Background execution — Transfer 3
Final task executed. All ZEC transferred.
Notification:
"Zodl: Migration complete. All 10 ZEC successfully transferred to Ironwood."
9. Completion
The user opens the app the next evening. Progress screen shows:
"3 of 3 transfers complete. Migration to Ironwood succeeded."
Orchard balance: 0. Ironwood balance: ~10 ZEC (minus fees).
User C — 10,000 ZEC, Highly Privacy-Conscious
Profile: Large balance, understands on-chain linkability, wants maximum temporal de-correlation. Enables Tor.
Pre-conditions
Steps
1. Migration entry point
The user opens the app. Migration screen:
"You have 10,000 ZEC in the Orchard pool. This balance is at risk and should be migrated to Ironwood."
The screen displays the **Orchard-specific balance only** (not a unified total).
The user selects **Migrate with privacy**.
2. Note splitting — extended
Wallet proposes splitting into ~10 notes with randomised sizes (capped at ~20,000 ZEC per note; for 10,000 ZEC this produces notes in the ~800–1,200 ZEC range):
"To migrate privately, your balance will first be split into smaller notes. This is a wallet-internal transaction — no ZEC leaves your wallet."
Example split:
The wallet waits for confirmation before proceeding (~75 seconds).
3. Schedule preview — 10 transfers over ~2.5 days
The user reviews the full schedule. Copy:
"Transfers are spread over ~2.5 days to de-correlate them from your app usage. Each transfer broadcasts at a different anchor height so they cannot be linked to each other."
User confirms.
4. Battery optimisation exemption
Android prompt shown. The user grants exemption immediately.
5. Network privacy screen
Tor toggle shown. The user enables Tor.
Tor availability check runs. If Tor is available:
"All migration transactions will be broadcast through Tor."
If Tor is unavailable:
"Tor is not available on this network. Consider using a trusted VPN before proceeding, or continue without."
The user proceeds with Tor enabled.
6. Background execution — Transfers 1–10 over 2.5 days
Each pre-signed transaction is stored in the database. WorkManager schedules each task in its 288-block bucket. Sync is not triggered inside background tasks — the task broadcasts only.
For each completed transfer, a notification is delivered:
"Zodl: Transfer N of 10 complete — X ZEC remaining in Orchard."
The user monitors progress by opening the app periodically:
"6 of 10 transfers complete — the next send is scheduled in ~4 hours. 3,840 ZEC remaining in Orchard."
7. Change output handling
If any transfer generates change back to Orchard, the wallet detects that a sync is required before the next spend. The schedule is adjusted to include a sync step, decoupled in time from the broadcast:
"A sync is needed before transfer 8 can proceed. This will happen automatically."
8. Completion
After ~2.5 days, user opens app:
"Migration complete. All 10,000 ZEC transferred to Ironwood."
Orchard balance: 0. Ironwood balance: ~10,000 ZEC (minus fees). Full transfer history visible.
Additional User Paths (Brainstormed)
D — User Transacts During Migration
User has 10 ZEC, migration schedule committed (3 transfers). Between transfer 1 and 2, user sends 5 ZEC to an external address. The note earmarked for transfer 2 is now partially or fully spent.
On the next app open, the wallet detects an invalid state: Orchard balance > 0, but queued migration transaction is no longer valid. Prompt:
"One of your scheduled migration transfers is no longer valid because the funds were spent. Please restart this step."
The remaining unspent Orchard notes are re-evaluated and a new partial schedule is proposed.
E — User Ignores Migration for Weeks
The user has 10 ZEC. Migration entry point was dismissed. The user returns 3 weeks later.
On open:
"You still have 10 ZEC in the Orchard pool. Your migration has not started. Start now?"
All missed windows are collapsed — the schedule is created fresh from the current moment, starting at the next available anchor-height bucket.
F — User Installs App on New Device Mid-Migration
The user has 10 ZEC, 2 of 5 transfers complete. Gets a new phone and restores from seed.
The wallet detects unspent Orchard funds, treats it as a fresh migration, and prompts the user to start again. Transfers 1–2 are already confirmed on-chain; only the remaining balance needs new scheduling.
G — User Denies Battery Optimisation Exemption
Migration is scheduled but battery exemption is denied. The app proceeds but shows a persistent warning:
"Automatic migration may be delayed. Check your battery settings if transfers are not happening on schedule. You can always send it manually from this screen."
Fallback-on-open is the primary mechanism for this user. More frequent prompts expected.
H — Negligible Dust Remaining After Migration
After the last scheduled transfer, a tiny amount (e.g., 0.00001 ZEC) remains due to fee rounding. The wallet evaluates whether this is below a significance threshold. If it is:
"Migration complete. A negligible amount of ZEC remains in Orchard — no action required."
If it is above the threshold, a final small transfer is added to the schedule.
I — Note Split Generates Change Before Network Upgrade
The user initiates note splitting before the network upgrade finalises. The split transaction includes a change output back to Orchard. After the upgrade activates we can proceed to the migration step.
Status shown:
"Start migration."
J — First App Open After Network Upgrade, No Prior Note Split
The user opens the app for the first time after the network upgrade without having split notes beforehand. The wallet sets the minimum anchor height for migration at this first open. The user is guided through the note-splitting step before the schedule is proposed. Migration transactions do not proceed until the split confirms.
Ironwood Migration — Error Cases
Scope: Medium user (10 ZEC, migrate with privacy, 3-transfer schedule over ~18 hours).
Based on: Orchard Migration Flow — Implementation Proposal (Path A), sync 2026-06-17
EC-1 — Background Task Missed: OS Suppressed Window by 1 Hour
Trigger: The OS delays the WorkManager task (e.g., device in Do Not Disturb, low-battery idle, or task deferred by OS scheduler). Transfer 2 was due at 02:00, runs at 03:00 instead.
What happens:
WorkManager schedules transfer 2 for the 288-block bucket beginning at ~02:00.
The device is in Do Not Disturb mode; the task fires at ~03:00 (1-hour slip).
The pre-signed transaction still references a valid anchor height — it has not expired.
The task broadcasts the transaction successfully.
Notification delivered:
"Zodl: Transfer 2 of 3 complete. Next send in ~5 hours. 3.3 ZEC remaining in Orchard."
Transfer 3 schedule shifts by the same offset: originally 08:00, now ~09:00.
User experience: Invisible. Migration continues normally. No user action required.
App behaviour:
EC-2 — Background Task Never Runs: OEM Battery Restriction
Trigger: MIUI / One UI aggressive battery optimisation kills the WorkManager task before it fires. Transfer 2 window (12h mark) passes without execution. Transfer 3 window (18h mark) also passes. The user opens the app the next morning.
What happens:
WorkManager task is scheduled. OS terminates the process before execution.
No notification is sent (task never ran).
The user opens the app ~22 hours after confirming the schedule.
App launch reconciliation detects: transfer 2 is overdue.
Fallback screen shown (not an error — copy must treat this as normal):
"2 transfers are ready to send. Background delivery was delayed — this can happen on some devices."
Options:
After the user's choice, a guidance banner is shown:
"To allow automatic migration, check your battery settings. Notification delivery is not guaranteed on all devices and battery modes — return to the app at the scheduled time if you are not receiving reminders."
Deep-link to battery optimisation settings included.
User experience: Minor disruption. The user must actively confirm the sends. Privacy guarantee is weakened if "Send now" is chosen (both sends happen within the same app session), but this is the worst case described in Path A — no worse than Path B, still acceptable.
App behaviour:
EC-3 — Background Task Runs But Network Is Unavailable
Trigger: Transfer 2 window arrives. WorkManager task fires correctly. However, the device has no internet connectivity (airplane mode, weak signal, Wi-Fi disconnected).
What happens:
WorkManager task starts. The pre-signed transaction is loaded from the database.
Broadcast attempt fails: network unreachable.
Task retries within its execution window (up to the WorkManager time limit).
If all retries fail, the task exits. Transaction is still stored in the database — not discarded.
The task is rescheduled for the next available window by WorkManager's backoff policy.
If the next window also fails (network still down), on next app launch:
"Transfer 2 could not be sent — no network connection was available. Send now or retry later."
Options:
Once connectivity is restored and the transaction broadcasts successfully, notification confirms:
"Zodl: Transfer 2 of 3 complete — delayed due to connectivity. 3.3 ZEC remaining in Orchard."
User experience: One missed window. Transfer 2 is delayed until connectivity returns. Transfer 3 schedule is shifted accordingly.
App behaviour:
---
EC-4 — Background Task Cannot Send: Already Spent Note (Invalid Transaction)
Trigger: Between transfer 1 and transfer 2, the user makes a regular Orchard spend (e.g., pays for something). The note earmarked for transfer 2 is now fully or partially spent. The pre-signed transaction is invalid.
What happens:
WorkManager task starts for transfer 2.
Transaction broadcast rejected by the network: the input note has already been spent.
Task marks transfer 2 as invalid and stores this state.
Notification (failure — must be shown):
"Zodl: Transfer 2 could not be sent. Your migration funds were partially spent. Please open the app."
The user opens the app. The wallet detects the invalid state:
Screen shown:
"A migration transfer is no longer valid because some funds were spent. We'll create a new transfer for your remaining Orchard balance."
This is not an error screen that triggers a sync. The screen is self-contained.
User experience: Minor disruption. One confirmation step required. The migration does not fail permanently — it adapts to the new balance.
App behaviour:
---
EC-5 — (Additional) Transaction Expires Before Broadcast
Trigger: All background tasks fail (network down for 3+ days, severe OEM restrictions). The pre-signed transaction reaches its expiry block height before it can be broadcast.
What happens:
On next app launch, the wallet detects: queued transaction exists but is expired (past its expiry height).
Orchard balance > 0, transaction state = expired.
Treated the same as EC-4 (invalid transaction):
"A scheduled migration transfer has expired. We'll create a new one for your remaining Orchard balance."
A new transaction is constructed with a fresh anchor height and expiry.
The user confirms. Migration resumes.
---
EC-6 — (Additional) Wallet Closed Before Note Split Confirms
Trigger: User initiates note splitting, then closes the app before the split transaction confirms on-chain.
What happens:
The split transaction was broadcast but is unconfirmed.
The user re-opens the app.
App detects: split transaction is pending.
Status shown:
"Waiting for your split transaction to confirm (~1 block, ~75 seconds). The migration schedule will be created automatically."
Once confirmed, migration scheduling proceeds as normal.
If the split transaction expired (very unlikely within a few blocks but possible if the node was down):
---
EC-7 — (Additional) Reboot Clears Notification State Mid-Migration
Trigger: Device reboots between migration steps. System notifications cleared. WorkManager tasks need to be re-registered (WorkManager handles this automatically on Android if `RECEIVE_BOOT_COMPLETED` permission is declared and used).
What happens:
Device reboots. WorkManager tasks are re-enqueued on boot via the boot receiver.
Notifications that were previously shown are cleared from the notification tray.
No missing transfers — tasks will still execute in their windows.
On the next app launch, the migration progress screen shows the current state accurately:
"1 of 3 transfers complete — next send in ~4 hours."
Copy in the migration screen must account for this:
"Notifications may be cleared when a device restarts. Return to this screen at any time to check your progress."
App behaviour: