Repository navigation
CSHLD-2073: Support v6 transaction size calculation in wasm-utxo - #448
Conversation
|
@claude review |
There was a problem hiding this comment.
Besides the inline findings, I checked the other callers of extract_unsigned_zcash_transaction, and neither one picks up the new v6 bytes. serialize() returns through serialize_v6() before it reaches the extraction call, and compute_txid now has its own is_ironwood_v6() guard, so v6 is still refused on both of these v4 paths.
Extended reasoning...
I checked the two other callers of the newly v6-aware extraction method in zcash/mod.rs (serialize and compute_txid) and confirmed that neither one encodes or double-SHA256-hashes v6 bytes on a v4 path.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🟣
packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs— pre-existing: JS callers still get a WASMunreachabletrap fromgetUnsignedTransaction()on some v6 PSBTs, including any PSBT aftercombine_ironwood_proof. The new v6 branch ofextract_unsigned_zcash_transactioncan return Err, andget_unsigned_tx_bytesstill calls.expect("Zcash unsigned tx encoding should not fail")on it. Fix: makeget_unsigned_tx_bytesand the wasmget_unsigned_txreturnResult<Vec<u8>, WasmUtxoError>, so every v6 error reaches JS as a catchable error rather than a panic.Why this was flagged
The wasm
get_unsigned_tx(src/wasm/fixed_script_wallet/mod.rs:1822) callsget_unsigned_tx_bytes, which.expectsextract_unsigned_zcash_transaction()(bitgo_psbt/mod.rs:1734-1736). The v6 branch (zcash/mod.rs:222-241) returns Err in several cases. One is a PSBT after the wasmcombine_ironwood_proof(zcash_psbt.rs:428), which callsmark_ironwood_extracted;require_no_shielded_output_ever_addedthen returns Err (ironwood.rs:623-627). Others are a missing consensus_branch_id into_v6_transaction(ironwood.rs:880-881) and a PCZT decode orpczt_action_datafailure. Each of these panics, and in wasm that is anunreachabletrap that can leave the module instance unusable. The base branch trapped on every v6 PSBT. The PR is meant to stop that trap, but this path still has it, so the problem is partly pre-existing. Nothing catches the error because the return type is a bareVec<u8>.Verification: pre-existing. The wasm
get_unsigned_tx(src/wasm/fixed_script_wallet/mod.rs:1822-1823) returnsself.psbt.get_unsigned_tx_bytes(), which (bitgo_psbt/mod.rs:1734-1736) still does.expect("Zcash unsigned tx encoding should not fail"). Aftercombine_ironwood_proof,require_no_shielded_output_ever_addedreturns Err (ironwood.rs:623-628), so JS gets a WASMunreachabletrap. On the base branch, every v6 PSBT already trapped by this route.
8d79067 to
87adf1b
Compare
Make extract_unsigned_zcash_transaction v6-aware so get_unsigned_tx_bytes (the JS getUnsignedTransaction()) can size and diff unsigned Ironwood transactions the way utxo-lib does for v4 — unblocking the wallet-platform pending-approval fee check, which currently has to fall back to the serialized-PSBT length because the wasm layer traps unreachable on Zcash PSBTs. For a v6 PSBT the embedded unsigned v6 transaction is serialized via encode_v6_transaction — the same construction unsigned_v6_txid commits to: transparent skeleton plus current action data (bundle-less for transparent-only PSBTs). Two unsigned-state compromises, both size-honest and txid-neutral: per-action spend-auth slots are padded with zero signatures and the Halo2 proof slot with zero bytes of orchard::Proof::expected_proof_size(n_actions) — each a fixed wire slot the finalized transaction carries anyway, so the byte length equals the final form's (an empty proof slot would under-price a shielded spend by several KB in fee checks built on this size). The proof itself is unknowable before combine_ironwood_proof; ZIP-244 commits to neither slot, so the txid is unchanged. compute_txid keeps refusing a v6 PSBT, now via an explicit guard with its own rationale: it applies double-SHA256 (ZIP-243 v4 semantics) to the extracted bytes, and hashing v6 wire bytes that way would produce a meaningless value — v6 txids are ZIP-244 via unsigned_v6_txid/v6_txid. Ticket: CSHLD-2073
ctutils 0.4.3 -> 0.4.6 fixes RUSTSEC-2026-0385 (patched release in the ^0.4 line, lockfile only). RUSTSEC-2026-0387 (crypto-bigint constant-time issues) is ignored in deny.toml: the patched version (>=0.7.6) is unreachable behind k256 0.13.4 (musig2/secp), and wasm-utxo's k256 usage does not expose the affected operations to attacker-controlled input. Master fails the same check — both advisories were published after the last green run, so this is advisory-DB drift, not a code change. Ticket: CSHLD-2073
87adf1b to
3859002
Compare
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
| { id = "RUSTSEC-2026-0387", reason = "crypto-bigint >=0.7.6 is unreachable behind k256 0.13.4 (musig2/secp); the affected constant-time ops are not exposed on attacker-controlled input in wasm-utxo's k256 usage" }, | ||
| ] |
There was a problem hiding this comment.
k256's Scalar add, sub and neg go through crypto-bigint add_mod/sub_mod/neg_mod during MuSig2 partial signing. wasm running under a JS JIT gives no constant-time guarantees anyway. Please change the reason in a follow up PR and get an approval from security team
There was a problem hiding this comment.
Follow-up PR opened with the corrected reason: #451 — the ignore now records that k256's Scalar add/sub/neg do reach crypto-bigint add_mod/sub_mod/neg_mod during MuSig2 partial signing, and that the constant-time concern is moot because wasm under a JS JIT gives no constant-time guarantees regardless of the crypto-bigint version. Security-team approval is requested on that PR (tagged in the body with the exact claim to validate). The same rationale text in this PR's description has been corrected.
Summary
Makes
extract_unsigned_zcash_transactionv6 (Ironwood)-aware, sogetUnsignedTransaction()returns the embedded unsigned v6 transaction bytes instead of trappingunreachable— giving wallet-platform the same size-calculation primitive utxo-lib provides for v4. Unblocks replacing the serialized-PSBT-length proxy in the wallet-platform pending-approval fee check (CSHLD-786 follow-up) and, later, restoring a wasm-based custodial integrity comparison for v6.Linear: CSHLD-2073
Changes
src/fixed_script_wallet/bitgo_psbt/zcash/mod.rs—extract_unsigned_zcash_transactionv6 branch: serializes the transparent skeleton plus current action data viaencode_v6_transaction, the same constructionunsigned_v6_txidcommits to (bundle-less for transparent-only PSBTs). Two unsigned-state compromises, both size-honest and txid-neutral: per-action spend-auth slots padded with zero signatures and the Halo2 proof slot padded with zero bytes oforchard::Proof::expected_proof_size(n_actions)— each a fixed wire slot the finalized transaction carries anyway, so the byte length equals the final form's (an empty proof slot would under-price a shielded spend by several KB in fee checks built on this size). The proof itself is unknowable beforecombine_ironwood_proof; ZIP-244 commits to neither slot, so the txid is unchangedcompute_txidkeeps refusing a v6 PSBT via an explicit guard: it applies double-SHA256 (ZIP-243, v4 semantics) to the extracted bytes, which would be a meaningless value for v6 — v6 txids are ZIP-244 viaunsigned_v6_txid/v6_txidtests/v6.rs—v4_paths_reject_a_v6_psbtupdated:extract_unsigned_zcash_transactionleaves the refusal set (v6-aware, likeserialize); newextract_unsigned_v6_tx_transparent_only_round_trips(decode + ZIP-244 txid equalsunsigned_v6_txid) andextract_unsigned_v6_tx_shielded_succeeds_pre_proof(succeeds pre-signing/pre-proof, deterministic; decodes the extracted bytes and asserts the proof slot is padded toexpected_proof_size(n_actions)and the padded slots leave the ZIP-244 txid unchanged)Test Plan
cargo test --lib→ 697 passed, 0 failed (includes the two new v6 extract tests and the updated refusal test)cargo check --target wasm32-unknown-unknown— clean (dev-dependencies do not ship in the wasm artifact)cargo clippy --all-targets --all-features -- -D warnings— cleancargo fmt -- --check— cleanCI advisory fix (3197d03)
cargo denyagainst two newly published RustSec advisories (master fails the same check — advisory-DB drift, not a code change):ctutils0.4.3 → 0.4.6 fixes RUSTSEC-2026-0385 (lockfile bump only, patched within the^0.4line); RUSTSEC-2026-0387 (crypto-bigint constant-time issues) is ignored indeny.toml. k256's Scalar add/sub/neg do reach crypto-bigintadd_mod/sub_mod/neg_modduring MuSig2 partial signing, but the constant-time concern is moot for this target: wasm under a JS JIT gives no constant-time guarantees regardless of the crypto-bigint version. The ignore's reason text was corrected accordingly in CSHLD-2073: correct RUSTSEC-2026-0387 ignore reason in deny.toml #451, which also tracks security-team sign-off; cargo-deny warns on unmatched ignores once k256 moves to elliptic-curve 0.14+ and the entry can be deleted.