Skip to content

feat(wasm-utxo): surface Ironwood shielded outputs in parsed-tx accounting and address encoding - #354

Open
veetragjain wants to merge 1 commit into
masterfrom
veetragjain/cshld-1378-wasm-utxo-ironwood-inspection-verification-surface
Open

feat(wasm-utxo): surface Ironwood shielded outputs in parsed-tx accounting and address encoding#354
veetragjain wants to merge 1 commit into
masterfrom
veetragjain/cshld-1378-wasm-utxo-ironwood-inspection-verification-surface

Conversation

@veetragjain

Copy link
Copy Markdown
Contributor

Zcash v6 (Ironwood) shielding transactions had several gaps where the shielded side of the transaction was silently invisible or, worse, caused a panic:

  • parseTransactionWithWalletKeys/parseOutputsWithWalletKeys never saw the shielded output (it lives in the PSBT's proprietary-map PCZT, not unsigned_tx.output), so minerFee silently absorbed the shielded amount and spendAmount undercounted the send. ParsedOutput gains an isShielded field; a shielded entry is now synthesized from the PCZT and folded into fee/spend totals.

  • unsignedTxId() panicked (wasm unreachable trap) on any v6 PSBT, since PsbtAccess::unsigned_tx_id fell through to the v4/Sapling txid path, which explicitly rejects v6 and whose Err was swallowed by an .expect(). It now computes the proper ZIP-244 v6 txid, including before a shielded output has been added.

  • The shielded output's raw Orchard/Ironwood receiver is now exposed via ParsedOutput.script (43 raw bytes, read from the PCZT's plaintext recipient field — no decryption involved).

  • toOutputScriptWithCoin gains an optional canBeShieldedOutput param: when set and the given address is a ZIP-316 unified address with an Orchard/Ironwood receiver, returns that raw receiver instead of a scriptPubKey; a malformed UA or one with no Orchard receiver throws rather than silently falling back to transparent decoding.

Adds Rust and TS test coverage for all of the above.

Ticket: CSHLD-1378

@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown

CSHLD-1378

@veetragjain
veetragjain force-pushed the veetragjain/cshld-1378-wasm-utxo-ironwood-inspection-verification-surface branch from 9fbe8c3 to a7ae550 Compare August 7, 2026 21:29
@veetragjain
veetragjain marked this pull request as ready for review August 7, 2026 21:39
@veetragjain
veetragjain requested review from a team as code owners August 7, 2026 21:39
@davidkaplanbitgo

Copy link
Copy Markdown
Contributor

@claude review this for correctness, architecture, reusability, and how much this affects existing functionality

@davidkaplanbitgo davidkaplanbitgo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Correctness

Verified via cargo test --lib shielded (11/11 pass) and a rebuilt wasm-bindgen output (confirms can_be_shielded_output and encodeOrchardReceiver/isShielded land correctly on the JS surface).

  • unsigned_v6_txid correctly distinguishes "no shielded output yet" (None bundle) from "extracted" (errors), via the new IronwoodExtracted proprietary-map marker — needed since bare PCZT-absence is ambiguous between those two states.
  • Fee/spend accounting (shielded_output()): computed before pushing the shielded entry into parsed_outputs, so sum_output_values's zip against psbt.unsigned_tx.output still lines up positionally. Correct.
  • ironwood_shielded_output_info errors if actions.len() != 1 instead of silently reading action 0 — correctly defensive against a future padded/shuffled bundle type.
  • Nit worth flagging: the synthesized shielded ParsedOutput.derivation_path is always None, so is_external() is always true and the shielded value always counts toward spend_amount. Correct for today's only use case (shield transparent → Orchard, no wallet-derived Orchard keys exist), but it's a latent trap if this ever extends to wallet-owned shielded outputs — worth a comment calling that out explicitly.

Reusability / Maintainability

  • Good factoring: shielded_output() is a single private helper shared by parse_outputs_with_wallet_keys and parse_transaction_with_wallet_keys — no duplicated PCZT-reading logic.
  • to_output_script_or_shielded_receiver_with_coin composes cleanly with the existing looks_like_unified_for_network/UnifiedAddress::parse rather than reimplementing UA sniffing.
  • Minor nit: ua_fixtures() (loads zcash/unified_address.json) is duplicated verbatim in three places — unified_address.rs module-level tests, the new encode_orchard_receiver_tests submodule in the same file, and networks.rs's new shielded_output test module. Worth hoisting into a shared test helper (e.g. test_utils::fixtures) instead of a third copy-paste.
  • f4jumble (forward direction) is a reasonable, well-commented addition mirroring the existing f4jumble_inv — genuinely new logic, not copy-paste.

Impact on non-ZEC coins

Effectively zero, confirmed concretely:

  • shielded_output() matches only BitGoPsbt::Zcash(..), returns Ok(None) immediately for every other coin in the two touched hot paths.
  • to_output_script_or_shielded_receiver_with_coin's UA-sniffing only fires when looks_like_unified_for_network matches, which requires hrp_for_network(coin) to resolve — only zec/tzec do, so it's a no-op branch elsewhere.
  • toOutputScriptWithCoin's new optional 3rd param is backward compatible — no other caller in the repo (webui, other tests) breaks; all pass 2 args and it defaults to false.
  • ParsedOutput.is_shielded/isShielded defaults false everywhere except the synthesized Zcash entry — no behavior change for non-Zcash consumers, just one extra boolean field on the wire.

Summary

Change is well-isolated to the Zcash/Ironwood path, non-ZEC coins are unaffected, and the accounting logic checks out against tests. Main actionable item: dedupe the ua_fixtures() test helper; optional: add a comment noting the always-external shielded-output assumption.

@veetragjain
veetragjain force-pushed the veetragjain/cshld-1378-wasm-utxo-ironwood-inspection-verification-surface branch from a7ae550 to 6b45a10 Compare August 12, 2026 05:32
Comment thread packages/wasm-utxo/src/address/networks.rs
Comment thread packages/wasm-utxo/src/address/networks.rs
Comment thread packages/wasm-utxo/src/address/networks.rs
/// Apply F4Jumble in place (the 4-round unkeyed Feistel network forwards) — the exact inverse of
/// [`f4jumble_inv`]: a Feistel network inverts by running the same per-round updates in reverse
/// round order, so this applies `g(0), h(0), g(1), h(1)`.
fn f4jumble(msg: &mut [u8]) -> Result<(), UnifiedAddressError> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

zcash crate should have this right? Any reason for hand rolling it here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Since we already have blake2b and f4jumble crates as dependencies, just reused them for UA encoding and decoding. I've documented them in this doc - packages/wasm-utxo/docs/zip-0316-unified-address.md

Comment thread packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs
@veetragjain
veetragjain requested a review from Ranjna-G August 12, 2026 15:31
@veetragjain
veetragjain force-pushed the veetragjain/cshld-1378-wasm-utxo-ironwood-inspection-verification-surface branch from 6b45a10 to 6a9d4bb Compare August 12, 2026 15:33
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.

3 participants