feat(wasm-utxo): add Ironwood PCZT constructor/combine bridge - #341
Conversation
OttoAllmendinger
left a comment
There was a problem hiding this comment.
Review
Overview
Adds zcash::ironwood_build, wiring the three local orchard PCZT roles (Constructor → IO-Finalizer/Signer → Extractor) to the v6 IronwoodBundle, with the Prover delegated externally, plus with_zkproof to splice the proof into the serialized PCZT. The role separation, the "why the proof can be filled in after signing" doc, and the type-level R: RngCore + CryptoRng bound are all clean. The golden_shield1zec_shielded_signatures_verify test is excellent — it verifies both the real dummy spend-auth signature and the real binding signature against compute_v6_sig_digest, an independent byte-correctness oracle for the shielded digest.
Issues
1. (Security) bsk transits the external proof service — this confirms the open question from #339.
The flow is finalize_shield_io (sets bsk + spend-auth sigs) → serialize_pczt → send to prover → with_zkproof → deserialize_pczt → combine (calls apply_binding_signature, which needs bsk). Because combine runs after the prover round-trip and consumes bsk, bsk must be serialized into the payload the proof service sees (with_zkproof's docstring: "wasm-utxo sends the signed PCZT", and that PCZT carries bsk per #339's BundleWire). So the binding-signature signing key is disclosed to a service whose only job is proving.
By this PR's own docstring the binding signature "signs the same build-time sighash and has no dependency on the [proof / transparent signatures]." That means it can be applied before delegating to the prover, so the disclosure is avoidable. Two options:
- apply the binding signature locally in
finalize/pre-serialize, so the outbound PCZT carriesbinding_sig(public) instead ofbsk(secret); or - strip
bskfrom the outbound payload and retain it locally, re-injecting after the prover returns (mirror ofwith_zkproof).
Not obviously exploitable for theft (transparent inputs still need their own signatures the service lacks), but it hands binding-signature authority to a semi-trusted service for no functional reason — a least-privilege regression worth closing. Can you confirm whether the production prover call strips bsk?
2. (Test coverage) The stack still never exercises the #338 unshield digest.
This golden test — like the transparent one in #338 — uses the shield1zec fixture, which has one transparent input. So compute_v6_sig_digest's inputs.is_empty() fallback (#338 finding: wrong for outputs-but-no-inputs) remains untested at every layer that consumes it. If unshield (shielded → transparent) is a supported v6 flow, it's currently unverified end-to-end.
Minor
combine:apply_binding_signaturereturningNoneis mapped toBindingSignatureFailed("spend-auth signatures did not verify against the sighash"), butNonecan also indicate a missing/invalidbsk. The message may misdirect debugging — consider more neutral wording.with_zkproofnot validating proof length (deferred to the Extractor) is reasonable and documented; fine as-is.
Praise
Dual-signature golden oracle, CSPRNG bound at the type level, nonempty correctly scoped to native dev-deps, and the with_zkproof idempotence / version-rejection tests are all nicely done.
Note: finding #1 is really a property of the #339 wire format and this PR's combine ordering together — a fix likely touches both BundleWire.bsk handling (#339) and the combine-after-prover ordering here.
Generated by Claude Code
e904ab9 to
5818b99
Compare
5818b99 to
c173d92
Compare
Add `zcash::ironwood_build`, the bridge between the `orchard` PCZT roles and the v6 `IronwoodBundle` wire type, for building transparent -> Ironwood shielding transactions without linking the halo2 prover: - `construct_shield_pczt` (Constructor): one output note + dummy spend as an `orchard::pczt::Bundle`, action data fixed, no proof/sigs. - `finalize_shield_io` (IO Finalizer/Signer): derive `bsk`, sign dummy spends over the shielded sighash. - `pczt_action_data`: effects-only view (commitments/ciphertexts/flags/value/ anchor) for computing the ZIP-244 sighash and txid. - `combine` (Transaction Extractor): given the signed+proven PCZT, verify spend-auth sigs, apply the binding signature, map to an encodable `IronwoodBundle`. The Prover role is delegated to an external service; its proof bytes are spliced into the serialized PCZT via `ironwood_pczt::with_zkproof`. Tests (all no-circuit): build->finalize->combine produces an encodable v6 tx with a stable txid; the combined tx cross-checks against zebra-chain; and a golden oracle reconstructs the orchard bundle from the real on-chain `shield1zec` action data and verifies both the real dummy spend-auth signature and the real binding signature against `compute_v6_sig_digest` — the shielded twin of PR1's transparent sighash oracle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c173d92 to
d1f5cff
Compare
Unshielding transaction support will be added later, current PR only focuses on shielding transaction support
Modified the message. |
Already addressed |
| //! | ||
| //! The **Prover** role (halo2) is intentionally absent — proving is delegated to an external | ||
| //! service so the shipped WASM never links the circuit. See | ||
| //! `docs/ironwood-proof-service-contract.md`. |
There was a problem hiding this comment.
nit(non-blocking): can we remove the reference to local dev doc
There was a problem hiding this comment.
actually it is not committed to the repo, so please either add it in a subsequent commit or remove the reference
OttoAllmendinger
left a comment
There was a problem hiding this comment.
Re-review (rev c173d92, rebased onto master)
Resolved since first pass
- ✅
BindingSignatureFailedmessage. Now reads "failed to bind the bundle (invalid spend-auth signatures, or a missing/invalid binding signing key)" and the variant doc lists both causes — no longer misdirects toward "signatures didn't verify" when the real cause is a missing/invalidbsk. - The #338 unshield-digest gap I flagged here is now covered by #338's new
shielded_sig_digest_unshield_case_differs_from_txidunit test, so #341's shield-only golden test is fine as-is.
One still-open item (documentation, security-relevant clarity — not a code bug)
The module docstring "Why the proof can be filled in after signing" describes the order as build → sighash → sign (transparent + dummy spends) → prove → combine. That's the finalize-before-prove order — i.e. the PCZT handed to the prover would already carry bsk (set by finalize_shield_io). The actual consumer in #342 inverts this safely: it ships the Constructor PCZT (no bsk) to the prover and runs finalize_shield_io locally inside combine_ironwood_proof, after the proof returns. Since the dummy-spend signature and bsk depend only on the sighash (not the proof), both orders produce a valid tx — but only the #342 order keeps bsk off the wire to the prover.
Recommend the docstring state explicitly that finalize_shield_io (which derives bsk) runs locally, after the prover round-trip, so an integrator following this module's narrative doesn't serialize bsk to the proof service. The end-to-end test (build_finalize_combine_produces_encodable_tx) also uses the finalize-then-serialize order for convenience — a one-line comment noting the production order is inverted (per #342), and that's why bsk doesn't leak, would prevent copy-paste of the leaky order. Pure documentation; no logic change.
Fresh pass — everything else LGTM
construct_shield_pczt/finalize_shield_io/pczt_action_data/combinecleanly map the four PCZT roles;action_to_ironwoodis correctly generic over authorization state; CSPRNG bound at the type level throughout.golden_shield1zec_shielded_signatures_verify(verifying both the real dummy spend-auth and the real binding signature againstcompute_v6_sig_digest) remains the strongest test in the stack.nonemptycorrectly scoped to native dev-deps;with_zkproofidempotence + version/empty rejection tests are good.
Verdict: clean — only a documentation clarification on the bsk/prover ordering remains, and it's non-blocking. Leaving formal approval to @OttoAllmendinger.
Generated by Claude Code
Add
zcash::ironwood_build, the bridge between theorchardPCZT roles and the v6IronwoodBundlewire type, for building transparent -> Ironwood shielding transactions without linking the halo2 prover:construct_shield_pczt(Constructor): one output note + dummy spend as anorchard::pczt::Bundle, action data fixed, no proof/sigs.finalize_shield_io(IO Finalizer/Signer): derivebsk, sign dummy spends over the shielded sighash.pczt_action_data: effects-only view (commitments/ciphertexts/flags/value/ anchor) for computing the ZIP-244 sighash and txid.combine(Transaction Extractor): given the signed+proven PCZT, verify spend-auth sigs, apply the binding signature, map to an encodableIronwoodBundle.The Prover role is delegated to an external service; its proof bytes are spliced into the serialized PCZT via
ironwood_pczt::with_zkproof.Tests (all no-circuit): build->finalize->combine produces an encodable v6 tx with a stable txid; the combined tx cross-checks against zebra-chain; and a golden oracle reconstructs the orchard bundle from the real on chain
shield1zecaction data and verifies both the real dummy spend-auth signature and the real binding signature againstcompute_v6_sig_digest— the shielded twin of PR1's transparent sighash oracle.