feat(zcash): add support for v6 transaction format#327
Conversation
Groundwork for Zcash v6 (Ironwood/NU6.3) transaction support: - ZCASH_IRONWOOD_VERSION_GROUP_ID (0xD884B698), v4/v6 version headers - crate-local blake2b_256_personal for ZIP-244 hash trees (the fork's helper is pub(crate) and unreachable here); includes the block-aligned finalization fix Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the standalone v6/NU6.3 wire codec (S1). The v6 header is reordered vs v4 (consensusBranchId/lockTime/expiryHeight move to the front) so it is a separate codec, leaving the v4 path untouched: - IronwoodAction (820B), IronwoodBundle, ZcashV6Transaction types - decode_v6_transaction / encode_v6_transaction with round-trip tests - empty Sapling + empty Orchard v6 slots supported; non-empty rejected - v4 decoder now guards against v6 bytes instead of mis-parsing them Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the shared ironwood_digest (S3) used by both the v6 txid (S2) and the shielded sighash. Three-way BLAKE2b split over per-action byte ranges (compact/memos/noncompact) combined with flags + valueBalance; anchor, proof, and signatures are excluded per ZIP-244. Adds an orchard_v6_empty_digest helper for the empty Orchard slot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the five-component ZIP-244 hash tree (S2): header, transparent (txid form), sapling, orchard-v6, and ironwood digests combined under the ZcashTxHash_<branchId> personalization. Adds zcash_compute_v6_txid WASM export. v4 txid (sha256d) path is unchanged. Validated against a real testnet shielding transaction captured from the Ironwood reference (txid b5af4de4...): the parser, encoder (byte-identical re-encode), and txid all match the golden vector. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ironwood reuses the existing Orchard receiver (typecode 0x03), so no new typecode is needed. Implements ZIP-316 UA decoding: Bech32m + F4Jumble inverse + typecode scanning. - blake2b: add variable-length blake2b_var_personal (F4Jumble needs it) - parse_unified_address: ironwood=true -> Orchard/Ironwood receiver (43B); ironwood=false -> transparent receiver as P2PKH/P2SH scriptPubKey - WASM exports zcash_parse_unified_address and zcash_compute_v6_txid; TS wrappers ZcashBitGoPsbt.parseUnifiedAddress / computeV6Txid - F4Jumble validated against the ZIP-316 vector; UA parse validated against official ZIP-316 unified-address test vectors; TS integration tests Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two more real testnet transactions captured from the Ironwood reference: - Ironwood->Ironwood self-send (aadfe41f...): no transparent I/O, exercises the empty transparent-digest branch of the txid tree - Transparent->Ironwood 1 ZEC shield (667e8ed3...) Both parse, re-encode byte-identically, and reproduce their canonical txid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…elpers - resolve_unified_address_component(address, network, resolveShieldedComponent): intent-named form returning the shielded (Ironwood/Orchard, 43B) or the transparent (scriptPubKey) component - is_address_component_of(unified, candidate, network): true when candidate (another UA, or a transparent base58check address) is contained in the UA; reuses the ZCASH/ZCASH_TEST codecs to reduce a transparent address to its (typecode, hash) receiver form - WASM exports + ZcashBitGoPsbt.resolveUnifiedAddressComponent / isAddressComponentOf Validated against the real testnet wallet vector (wallet-data): the UA resolves to the wallet's Ironwood raw receiver and transparent PKH, and the wallet's transparent address is recognized as a component. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow the repo fixture convention: test data lives in test/fixtures, not inline or under src/. - move the 6 v6 golden .hex files to test/fixtures/zcash/, loaded in Rust via load_fixture (the CARGO_MANIFEST_DIR helper other fs-based tests use) instead of include_str! - add test/fixtures/zcash/unified_address.json holding the ZIP-316 mainnet vector and the testnet wallet vector; both Rust and TS load from it (removes the duplicated hardcoded consts and the test->src coupling) - extend TS computeV6Txid coverage to all three golden txids; add Rust and TS negative cases (malformed candidate, missing receiver) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Suggested split into multiple PRsThe code layers naturally by dependency, and this split also isolates each of the two main review findings into a different PR. Dependency shape
PR 1 — BLAKE2b-256 personalization helper (base)
~90 lines, self-contained, ships with its own unit tests. Small and easy to approve; unblocks the other two. PR 2 — ZIP-316 Unified Address parsing (off PR1)
PR 3 — v6 (Ironwood) codec + ZIP-244 txid (off PR1)
One thing to watch
Base branch for all three can stay Generated by Claude Code |
Code reviewReviewed the full diff (v6 codec, unified-address parsing, blake2b helper, WASM bindings, TS wrapper, tests, fixtures). Strengths
Issues1. (Robustness) Unbounded let n = usize::try_from(n_actions_ironwood).map_err(...)?;
let mut actions = Vec::with_capacity(n); // n is an uncapped CompactSize (up to 2^64)
for _ in 0..n { let a = take_bytes(&mut r, IRONWOOD_ACTION_SIZE)?; ... }
2. (CONVENTIONS §3 — boolean trap + duplicate API) UA accessor design
3. (Minor) Network handling
4. (Minor) Footguns
VerdictSolid, well-tested implementation with strong golden-vector validation. #1 is the one I'd fix before merge (untrusted pre-allocation); #2 is a real conventions violation worth resolving while the API is still draft. #3–#4 are polish. Generated by Claude Code |
There was a problem hiding this comment.
additions should go to zec-namespaced file, maybe take some zcash scoped funcs here with you as well
| //! ZIP-316 Unified Address (UA) parsing. | ||
| //! | ||
| //! A UA is a Bech32m string whose payload is F4Jumble-encoded (ZIP-316 §F4Jumble). | ||
| //! The un-jumbled payload is a sequence of receivers — each a | ||
| //! `CompactSize(typecode) ‖ CompactSize(length) ‖ data` record — followed by a | ||
| //! 16-byte padding equal to the human-readable part (HRP), zero-extended. | ||
| //! | ||
| //! Ironwood reuses the existing **Orchard** receiver (typecode `0x03`): an Orchard | ||
| //! unified receiver routes to the Ironwood pool once NU6.3 rules are active, so no | ||
| //! new typecode is required. | ||
| //! | ||
| //! Parsing here only needs Bech32m + F4Jumble⁻¹ + typecode scanning — no `orchard` | ||
| //! crate, no ZK machinery. |
There was a problem hiding this comment.
copy the spec to the repo
| * @returns The 32-byte txid in internal byte order (reverse for display) | ||
| * @throws If the bytes are not a valid v6 transaction | ||
| */ | ||
| static computeV6Txid(txBytes: Uint8Array): Uint8Array { |
There was a problem hiding this comment.
why not a method on a psbt? why take txBytes?
| * @returns The receiver bytes | ||
| * @throws If the address is malformed or lacks a receiver of the requested type | ||
| */ | ||
| static parseUnifiedAddress( |
There was a problem hiding this comment.
Same "opaque bytes" concern as the computeV6Txid thread above, on the return side: these return a bare Uint8Array whose meaning depends on the boolean arg — an Orchard receiver (43 bytes) when true, a transparent scriptPubKey (25 bytes) when false. The caller has to remember which shape they asked for.
Prefer a structured return: decode once into a typed UnifiedAddress with named accessors (.orchardReceiver, .transparentScript), or a discriminated union { kind: "shielded" | "transparent"; ... }. That also removes the boolean flag (CONVENTIONS §3) and collapses parseUnifiedAddress/resolveUnifiedAddressComponent, which are currently the same function under a renamed bool.
Generated by Claude Code
This branch adds the foundational Zcash v6 (Ironwood / NU6.3) transaction support to wasm-utxo, implemented as a standalone, hand-rolled codec (no librustzcash dependency) so the existing v4 transparent path is completely untouched and non-breaking. It introduces the v6 wire format parse/encode (IronwoodAction/IronwoodBundle/ZcashV6Transaction), the ZIP-244 five-component txid computation, the shared ironwood_digest utility, a crate-local BLAKE2b-personalization helper, and ZIP-316 Unified Address parsing(Bech32m + F4Jumble)