feat(chain/zec): NU6.3 Ironwood support — bump librustzcash to zcash_protocol 0.10.0 #check-lint-warning

Problem

NU6.3 "Ironwood" activates on Zcash mainnet at block 3,428,143 (~2026-07-28 13:00 UTC) with a new consensus branch ID 0x37A5165B (ZIP 258). The ZIP-243/244 sighash commits to the branch ID, so from that height every ZEC signature must use the new ID.

The librustzcash rev currently pinned by chain/zec (8766e053, 2026-06-03) predates NU6.3: BranchId::for_height tops out at NU6.2, so after activation bifrost would keep signing with 0x5437f330 and every ZEC outbound would be consensus-invalid (sendrawtransaction rejected network-wide).

Same pattern as the NU6.2 upgrade (!796 (merged)).

Changes

  • Cargo.toml / Cargo.lock: bump all librustzcash crates to rev 993e39ff (the zcash_protocol-0.10.0 release commit — sets the NU6.3 mainnet activation height); orchard 0.14 → 0.15.0-pre.2, [patch.crates-io]-pinned to the same zcash/orchard rev (475ef0ff) that librustzcash itself pins. Lock diff kept minimal (only the affected crates).
  • tx.rs: Orchard bundle version is now derived from the consensus branch of the target height via bundle_version_for_branch(branch, ValuePool::Orchard) (orchard v2 pre-NU6.3, v3 after); adapted to the new fallible 4-arg orchard::builder::Builder::new.
  • config.rs: the Orchard circuit changed at NU6.3, so proving keys are now built and cached per OrchardCircuitVersion; building the insecure pre-NU6.2 circuit is refused.
  • network.rs: regtest nu6_3 activation at height 6; regtest zebrad configs (go/docker-regtest-setup.sh, js/regtest-setup.sh) activate "NU6.3" = 6.
  • orchard_sender / sapling_sender: new BuildConfig::Standard fields (ironwood_anchor: None, orchard_pool_bundle_type: BundleType::DEFAULT), NoteVersion::V2 for reconstructed Orchard-pool notes, TxBuilder lost its lifetime parameter.

Validation

  • cargo check clean for uniffi (prod), uniffi,shielded, and --tests.
  • lib/libzec.a and lib/sim/libzec.a rebuilt from the new sources (source-hash updated; FFI symbols identical — interface.udl untouched, so the Go bindings are unchanged).
  • Regtest/mocknet zebra images bumped 5.0.0 → 6.0.0 (chain/zec/{go,js}/docker-compose.yml, build/docker/docker-compose.yml) and mocknet start-zebra.sh activates "NU6.3" = 6 — zebra 5.0.0 rejects the unknown "NU6.3" activation key, so regtest would not boot otherwise.
  • e2e feature not checked locally (missing libssl-dev on the build host only) — needs CI + regtest e2e run.
  • The transparent path (what bifrost signs today) has no format change at NU6.3 — v4/v5 txs stay valid, only the branch ID in the sighash changes.
  • Companion infra: node-launcher!464 bumps zebra-daemon to 6.0.0 (zebra 5.0.0 stalls at the activation height; zcashd auto-shuts-down at block 3,417,100, ~2026-07-18).

Review notes / decisions for the team

  • orchard_pool_bundle_type: BundleType::DEFAULT preserves the previous builder behavior (2-action padding).
  • preload_provers() now preloads only the post-NU6.3 circuit; the NU6.2 prover is lazy-loaded if a pre-activation shielded build is requested.
  • ZIP 2006 makes the Orchard pool spend-only from NU6.3 (no new transfers into Orchard) — anything that sends to Orchard addresses after activation (e.g. shielded quote support, !813) may need to target the Ironwood pool separately; out of scope here.

Operational note for the activation window (~block 3,428,143, 2026-07-28)

The signer selects the branch ID from the current chain tip (ptx.Height), which is correct, but any ZEC outbound signed shortly before activation that fails to confirm before block 3,428,143 becomes permanently consensus-invalid (the sighash committed to the NU6.2 branch ID) and will need re-signing; note the self-change path sets ExpiryHeight: 0 (never expires), so such a tx never expires out of the checkpoint on its own. Consider briefly halting ZEC outbounds (mimir) a few blocks around activation, as was done for prior NUs.

Post-first-CI review fixes (applied in this branch)

  1. Static libs rebuilt — lib/libzec.a / lib/sim/libzec.a + source-hash regenerated (chain-bindings ✅).
  2. Regtest/mocknet zebra 5.0.0 → 6.0.0 (3 compose files + build/docker/mocknet/zec/start-zebra.sh NU6.3=6) — zebra 5.0.0 rejects the unknown "NU6.3" config key, so regtest wouldn't boot (zec-js-test-lint ✅).
  3. JS builder (chain/zec/js) had NU6.2 hardcoded in 3 places — replaced with a height-aware consensusBranchId(); the pure-TS builder would have produced invalid signatures on mainnet from July 28.
  4. ZIP 2006 behavior change (needs product review): from NU6.3 the Orchard pool is spend-only — the orchard builder now rejects outputs to foreign Orchard receivers ("Cross-address transfers are disabled"). tx.rs now routes unified addresses to their Sapling (or transparent) receiver once cross-address is no longer permitted. After activation, UAs can no longer be paid into Orchard until Ironwood-pool sends are implemented.
  5. Tx format pinned to v5 post-NU6.3 — TxVersion::suggested_for_branch(Nu6_3) returns V6, but in test-simulation zebra's generate miner accepted v6 outbounds into the mempool and then never mined them (they sat until expiry, stalling every ZEC outbound: fail to get tx from mempool). v5 remains consensus-valid after NU6.3 (ZIP 229 only adds v6), so bifrost now pins V5 — only the sighash/txid branch id changes at activation. Worth reporting the v6 generate behavior upstream to Zebra. Golden vectors in go/zec/zec_test.go regenerated (v5 header + branch 0x37a5165b; deterministic RFC6979 signatures).
  6. Simulation suites adapted to ZIP 2006 — with regtest activating NU6.3 at block 6, transparent funds can no longer be shielded into Orchard, so Orchard-source scenarios (ZecSwaps, ZECRefundActor, ZecMemoLimits) are unfundable; they now use Sapling as the shielded source. The Orchard actor code is kept for when Ironwood-pool support lands.
  7. v5 pin extended to the shielded senders — the T→Z / Z→T paths use zcash_primitives' TxBuilder, which also suggests v6 from NU6.3; every builder now calls propose_version(TxVersion::V5).
  8. Outbound-matching slash bug (the most subtle one): normalize_address_for_outbound preferred the Orchard receiver, so the state machine scheduled outbounds to orchard-only UA strings while the signer (correctly, per ZIP 2006) paid the Sapling receiver — the observed outbound didn't match the scheduled txout item (tx out match mismatch) and nodes were slashed while the outbound stayed unresolved. Normalization now prefers Sapling → transparent → Orchard-last-resort, which is valid on both sides of the activation. Note for review: orchard-only UAs stop being payable at NU6.3 entirely — mayanode may want to reject them as swap destinations at/after activation.
  9. Simulation ensureMined deadline 3s → 30s — on loaded CI runners a shielded tx can take several mocknet blocks to enter a template; the 3-second deadline failed intermittently and its "tx not found" message was misleading.

Verified locally against a real zfnd/zebra:6.0.0 regtest (NU6.3 active at block 6): v5 transactions with Sapling bundles built by this branch are accepted and mined by the generate miner, including under CPU load with a 0.7s block cadence.


#check-lint-warning — the versioned-functions lint flags the semver parameter threaded into matchActionItemV127 and the NormalizeZcashAddressForOutbound(version) calls inside handleV127/handleV129. The change is intentional and behaviour-preserving for old versions: for network versions below 1.131.0 the added dispatch resolves to normalize_address_for_outbound_v127 (orchard-first), byte-identical to the previous unversioned behaviour those functions had; the Sapling-first order only activates at the coordinated GTE(1.131.0) gate (see Itzamna's review threads).

Edited by Kukulkan Maya

Merge request reports

Loading
Loading