Skip to content

feat(wasm-utxo): add Ironwood PCZT constructor/combine bridge - #341

Merged
veetragjain merged 1 commit into
masterfrom
veetragjain/cshld-1293-wasm-utxo-implement-ironwood-pczt-constructor-combine-bridge
Jul 30, 2026
Merged

feat(wasm-utxo): add Ironwood PCZT constructor/combine bridge#341
veetragjain merged 1 commit into
masterfrom
veetragjain/cshld-1293-wasm-utxo-implement-ironwood-pczt-constructor-combine-bridge

Conversation

@veetragjain

@veetragjain veetragjain commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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.

@linear-code

linear-code Bot commented Jul 28, 2026

Copy link
Copy Markdown

CSHLD-1293

@OttoAllmendinger OttoAllmendinger 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.

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_pcztsend to proverwith_zkproofdeserialize_pcztcombine (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 carries binding_sig (public) instead of bsk (secret); or
  • strip bsk from the outbound payload and retain it locally, re-injecting after the prover returns (mirror of with_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_signature returning None is mapped to BindingSignatureFailed ("spend-auth signatures did not verify against the sighash"), but None can also indicate a missing/invalid bsk. The message may misdirect debugging — consider more neutral wording.
  • with_zkproof not 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

@veetragjain
veetragjain force-pushed the veetragjain/cshld-1293-wasm-utxo-implement-ironwood-pczt-constructor-combine-bridge branch 2 times, most recently from e904ab9 to 5818b99 Compare July 29, 2026 11:16
Base automatically changed from veetragjain/wasm-utxo-orchard-pczt-serialization to master July 29, 2026 13:24
@OttoAllmendinger
OttoAllmendinger force-pushed the veetragjain/cshld-1293-wasm-utxo-implement-ironwood-pczt-constructor-combine-bridge branch from 5818b99 to c173d92 Compare July 29, 2026 13:24
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>
@veetragjain
veetragjain force-pushed the veetragjain/cshld-1293-wasm-utxo-implement-ironwood-pczt-constructor-combine-bridge branch from c173d92 to d1f5cff Compare July 29, 2026 14:38
@veetragjain

Copy link
Copy Markdown
Contributor Author
  1. (Test coverage) The stack still never exercises the feat(wasm-utxo): add ZIP-244 v6 signature digests (shielded + transparent) #338 unshield digest.
    This golden test — like the transparent one in feat(wasm-utxo): add ZIP-244 v6 signature digests (shielded + transparent) #338 — uses the shield1zec fixture, which has one transparent input. So compute_v6_sig_digest's inputs.is_empty() fallback (feat(wasm-utxo): add ZIP-244 v6 signature digests (shielded + transparent) #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.

Unshielding transaction support will be added later, current PR only focuses on shielding transaction support

combine: apply_binding_signature returning None is mapped to BindingSignatureFailed ("spend-auth signatures did not verify against the sighash"), but None can also indicate a missing/invalid bsk. The message may misdirect debugging — consider more neutral wording.

Modified the message.

@veetragjain
veetragjain marked this pull request as ready for review July 29, 2026 14:41
@veetragjain
veetragjain requested review from a team as code owners July 29, 2026 14:41
@veetragjain

Copy link
Copy Markdown
Contributor Author
  1. (Security) bsk transits the external proof service — this confirms the open question from feat(wasm-utxo): add orchard (no-circuit) dep + Ironwood PCZT serialization #339.

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`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit(non-blocking): can we remove the reference to local dev doc

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.

please keep it

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.

actually it is not committed to the repo, so please either add it in a subsequent commit or remove the reference

@abhi-bitgo abhi-bitgo left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

lgtm

@OttoAllmendinger OttoAllmendinger 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.

Re-review (rev c173d92, rebased onto master)

Resolved since first pass

  • BindingSignatureFailed message. 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/invalid bsk.
  • The #338 unshield-digest gap I flagged here is now covered by #338's new shielded_sig_digest_unshield_case_differs_from_txid unit 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 / combine cleanly map the four PCZT roles; action_to_ironwood is 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 against compute_v6_sig_digest) remains the strongest test in the stack.
  • nonempty correctly scoped to native dev-deps; with_zkproof idempotence + 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

@veetragjain
veetragjain merged commit c7b0dff into master Jul 30, 2026
13 checks passed
@veetragjain
veetragjain deleted the veetragjain/cshld-1293-wasm-utxo-implement-ironwood-pczt-constructor-combine-bridge branch July 30, 2026 08:19
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.

4 participants