Skip to content

Ironwood (NU6.3) SDK + Orchard→Ironwood migration bridge - #71

Merged
peachbits merged 4 commits into
zcash-spm-deps-binaryfrom
zcash-ironwood
Aug 1, 2026
Merged

Ironwood (NU6.3) SDK + Orchard→Ironwood migration bridge#71
peachbits merged 4 commits into
zcash-spm-deps-binaryfrom
zcash-ironwood

Conversation

@peachbits

@peachbits peachbits commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

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 proposeOrchardToIronwoodMigration proposal — 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

  1. Pin SDK 2.7.0-rc.2 + build inputs — the first released SDK line carrying Ironwood, replacing the unreleased branch head this PR previously rode (that branch rebased out from under us twice). The Edge-hosted one-time FFI build is gone: the release ships its own libzcashlc.xcframework.zip, which update-sources downloads and verifies against the checksum the SDK's own Package.swift declares. Also carries TreeState.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.
  2. BalanceEvent: Ironwood pool fields — both platforms; the deprecated summed fields include the pool.
  3. The migration surface, identical on both platformsSynchronizer.proposeOrchardToIronwoodMigration builds 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 ordinary createTransfer pipeline. Tools.getIronwoodActivationHeight answers 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

  • Full update-sources regen from cleared caches against upstream's FFI (RC=0); vendored checkout is exactly the tag; vendored deps rebuilt.
  • The Swift bridge compiles against the real 2.7.0-rc.2, and every commit compiles the Android module against zcash-android-sdk 2.7.0-rc.2 (the first time that code has been type-checked).
  • develop-worktree Release build: BUILD SUCCEEDED, 0 undefined zcash/grpc/NIO symbols with the piratechain consumer co-installed, the Orchard->Ironwood FFI linked and the old migration symbols absent; app installs, launches and runs clean.

Follow-ups

  • Swap to the final 2.7.0 tag when it ships (pins-only commit). A 2.8.0-rc.1 iOS prerelease exists, but Maven's newest Android is 2.7.0-rc.2, so rc.2 is where both platforms align.
  • The full guided-migration lifecycle bridge is the stacked v2 PR (held for live validation).

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-sources pulls libzcashlc from the SDK’s own Package.swift binary target with checksum verification instead of a custom Edge FFI build.

Balance and migration API: BalanceEvent adds ironwoodAvailableZatoshi / ironwoodTotalZatoshi on both platforms; deprecated wallet totals in JS now include Ironwood. Cross-platform bridges expose Synchronizer.proposeOrchardToIronwoodMigration (Orchard sweep proposal for normal createTransfer) and Tools.getIronwoodActivationHeight (ZIP 258 consensus heights). Checkpoint tooling adds TreeState.ironwoodTree to the bundled lightwalletd proto so generated checkpoints retain Ironwood tree state after activation.

Transaction delivery fix: emitExistingTransactions plus a subscribe() 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.


Comment thread android/src/main/java/app/edge/rnzcash/RNZcashModule.kt
Comment thread android/src/main/java/app/edge/rnzcash/RNZcashModule.kt
Comment thread android/src/main/java/app/edge/rnzcash/IronwoodMigration.kt Outdated
Comment thread ios/RNZcash.swift
Comment thread android/src/main/java/app/edge/rnzcash/RNZcashModule.kt

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread android/src/main/java/app/edge/rnzcash/RNZcashModule.kt Outdated
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
peachbits merged commit 2e7be00 into zcash-spm-deps-binary Aug 1, 2026
4 checks passed
@peachbits
peachbits deleted the zcash-ironwood branch August 1, 2026 03:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants