Ironwood (NU6.3) SDK + Orchard→Ironwood migration bridge - #71
Merged
Conversation
This was referenced Jul 16, 2026
peachbits
force-pushed
the
zcash-ironwood
branch
8 times, most recently
from
July 28, 2026 17:49
cf18bc5 to
537d8ca
Compare
peachbits
marked this pull request as ready for review
July 28, 2026 23:26
2 tasks
j0ntz
approved these changes
Jul 29, 2026
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8ac2ce2. Configure here.
Points update-sources at the Swift SDK's 2.7.0-rc.2 tag and bumps the Android SDK (and the incubator) to the same release - the first published line carrying Ironwood. The Swift side is pinned by commit rather than tag name, so the checkout is immutable even if the tag is ever moved. The FFI is upstream's own release asset now, so there is no Edge-hosted xcframework to build or host. update-sources does not repeat that asset's URL: it reads the url/checksum pair out of the pinned checkout's own binaryTarget, which is the one place upstream writes down which FFI build matches this source - so a source/binary mismatch is unrepresentable. LIBZCASHLC_XCFRAMEWORK_SHA256 remains as the reviewed copy of that checksum: the build stops if the two disagree, so bumping the SDK pin has to change native code deliberately rather than silently, and the download is verified against it before anything is unpacked. Also carries the one other build input Ironwood needs: TreeState.ironwoodTree (field 7) in the bundled lightwalletd proto. update-checkpoints writes the GetTreeState response verbatim, so a field missing from our proto is never decoded and silently never written - every checkpoint generated after activation would have shipped without Ironwood tree state. Pre-activation output is unchanged, so existing checkpoints need no regeneration.
Reports the Ironwood pool alongside transparent/sapling/orchard on both platforms, and folds it into the deprecated summed fields so funds do not appear to vanish from them once value starts crossing into Ironwood.
Detection plus the v1 sweep, identical on both platforms. Synchronizer.proposeOrchardToIronwoodMigration builds the sweep: the SDK spends every Orchard note to the account's own address with the fee chosen so no Orchard change remains, leaving Sapling and transparent funds untouched, and it executes through the ordinary createTransfer pipeline. Neither SDK reports a payment value on the proposal, so both platforms quote the crossing amount the same way - the Orchard balance minus that fee - and fail rather than quote a figure they cannot source. Tools.getIronwoodActivationHeight is stateless and served from consensus constants (ZIP 258), which neither SDK exposes, so it answers on both platforms without an SDK call. There is deliberately no migration state to poll: the app decides whether to offer a sweep from the activation height, sync state and Orchard balance, and broadcasting the sweep spends those notes, which withdraws the offer on its own.
The synchronizer's event stream only carries transactions newly found in scanned blocks or newly mined, and the native module drops every event until JavaScript attaches a listener. A transaction that settled while nothing was listening - mined while the app was closed, or during a failed sync - is therefore neither newly found nor newly mined on the next launch, so it was never reported again: it stayed at height 0, "pending", forever. Synchronizer.subscribe now asks native for the current transaction set once its listeners are attached. Driving it from JavaScript is what makes the delivery reliable: emitting during initialize would race the app's own subscribe call and lose, because hasListeners is still false there. Both platforms are affected. iOS never dumps the existing set outside its restart path. Android's collector does deliver the full list, but on its first emission during initialize, which is the same race - one it usually wins, which is not the same as being correct. Re-sending known transactions is harmless: the app updates only those whose height or amount actually changed, and its new-transaction notification path is not reached for transactions it already has.
peachbits
force-pushed
the
zcash-ironwood
branch
from
July 31, 2026 21:12
629134c to
64ce8f0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Ironwood (NU6.3) v1: SDK 2.7.0-rc.2 + the Orchard sweep
Stacked on #69. Restructured 2026-07-21 to v1-only (the previous full migration-lifecycle bridge moved to the stacked v2 PR): v1 ships what the app needs for the fork with no privacy machinery, per the two-phase product decision. Retargeted to the released SDK 2.7.0-rc.2, which ships Ironwood and exposes the sweep as a single
proposeOrchardToIronwoodMigrationproposal — identical on iOS and Android. That replaced the earlier prototype's migration engine (schedules, note splitting, a state machine), so this PR no longer bridges any of it.What's here
libzcashlc.xcframework.zip, whichupdate-sourcesdownloads and verifies against the checksum the SDK's ownPackage.swiftdeclares. Also carriesTreeState.ironwoodTree(field 7) for checkpoint generation, and bumps zcash-android-sdk (plus the incubator) from 2.5.2 to 2.7.0-rc.2 — now on Maven Central. That SDK ships Kotlin 2.3 metadata, which the app already provides (kotlinVersion = "2.3.20"). Swift dependency versions are unchanged (grpc 1.27.3 / NIO 2.95.0 / protobuf 1.35.1 / SQLite 0.15.5), so the vendored deps binary and piratechain consumer mode are unaffected.Synchronizer.proposeOrchardToIronwoodMigrationbuilds the sweep (the SDK spends every Orchard note to the wallet's own address, fee chosen so no Orchard change remains, Sapling and transparent untouched, all-or-nothing), which the app broadcasts through the ordinarycreateTransferpipeline.Tools.getIronwoodActivationHeightanswers from consensus constants (ZIP 258) — no SDK exposes an accessor — so it works on Android today. There is deliberately no migration state to poll: whether to offer the sweep follows from activation height, sync state and the Orchard balance, and broadcasting spends those notes. Android calls the same API on its own SDK — no runtime seam, no feature flag, no opt-in source set.Verification
update-sourcesregen from cleared caches against upstream's FFI (RC=0); vendored checkout is exactly the tag; vendored deps rebuilt.Follow-ups
Note
High Risk
Touches wallet balances, transaction reporting, and native SDK/FFI pins across iOS and Android; migration proposals affect how Orchard funds are swept at NU6.3.
Overview
Upgrades iOS and Android Zcash SDKs to 2.7.0-rc.4 for Ironwood (NU6.3): Android dependencies bump, Swift sources pin to the release commit, and
update-sourcespullslibzcashlcfrom the SDK’s ownPackage.swiftbinary target with checksum verification instead of a custom Edge FFI build.Balance and migration API:
BalanceEventaddsironwoodAvailableZatoshi/ironwoodTotalZatoshion both platforms; deprecated wallet totals in JS now include Ironwood. Cross-platform bridges exposeSynchronizer.proposeOrchardToIronwoodMigration(Orchard sweep proposal for normalcreateTransfer) andTools.getIronwoodActivationHeight(ZIP 258 consensus heights). Checkpoint tooling addsTreeState.ironwoodTreeto the bundled lightwalletd proto so generated checkpoints retain Ironwood tree state after activation.Transaction delivery fix:
emitExistingTransactionsplus asubscribe()call replays the full transaction set once JS listeners are attached, fixing txs that mined while the app was closed or before listeners existed and would otherwise stay pending forever. Android records emitted txs to avoid duplicate full replays from the collector.Reviewed by Cursor Bugbot for commit 8ac2ce2. Bugbot is set up for automated code reviews on this repo. Configure here.