feat(wasm-utxo): surface Ironwood shielded outputs in parsed-tx accounting and address encoding - #354
Conversation
9fbe8c3 to
a7ae550
Compare
|
@claude review this for correctness, architecture, reusability, and how much this affects existing functionality |
davidkaplanbitgo
left a comment
There was a problem hiding this comment.
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_txidcorrectly distinguishes "no shielded output yet" (Nonebundle) from "extracted" (errors), via the newIronwoodExtractedproprietary-map marker — needed since bare PCZT-absence is ambiguous between those two states.- Fee/spend accounting (
shielded_output()): computed before pushing the shielded entry intoparsed_outputs, sosum_output_values's zip againstpsbt.unsigned_tx.outputstill lines up positionally. Correct. ironwood_shielded_output_infoerrors ifactions.len() != 1instead of silently reading action 0 — correctly defensive against a future padded/shuffled bundle type.- Nit worth flagging: the synthesized shielded
ParsedOutput.derivation_pathis alwaysNone, sois_external()is alwaystrueand the shielded value always counts towardspend_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 byparse_outputs_with_wallet_keysandparse_transaction_with_wallet_keys— no duplicated PCZT-reading logic. to_output_script_or_shielded_receiver_with_coincomposes cleanly with the existinglooks_like_unified_for_network/UnifiedAddress::parserather than reimplementing UA sniffing.- Minor nit:
ua_fixtures()(loadszcash/unified_address.json) is duplicated verbatim in three places —unified_address.rsmodule-level tests, the newencode_orchard_receiver_testssubmodule in the same file, andnetworks.rs's newshielded_outputtest 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 existingf4jumble_inv— genuinely new logic, not copy-paste.
Impact on non-ZEC coins
Effectively zero, confirmed concretely:
shielded_output()matches onlyBitGoPsbt::Zcash(..), returnsOk(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 whenlooks_like_unified_for_networkmatches, which requireshrp_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 tofalse.ParsedOutput.is_shielded/isShieldeddefaultsfalseeverywhere 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.
a7ae550 to
6b45a10
Compare
| /// 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> { |
There was a problem hiding this comment.
zcash crate should have this right? Any reason for hand rolling it here?
There was a problem hiding this comment.
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
…nting and address encoding Ticket: CSHLD-1378
6b45a10 to
6a9d4bb
Compare
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
unreachabletrap) 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