Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/wasm-utxo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [
default = []
inspect = ["dep:num-bigint", "dep:serde_json", "dep:hex"]
# Pulls in orchard's halo2 circuit/proving-key machinery so this crate can produce its own
# Ironwood zk-proofs locally, instead of relying on an external proof service. Heavy (halo2
# Ironwood zk-proofs locally, instead of relying on an external prover. Heavy (halo2
# circuit synthesis + proving-key generation), so it's opt-in and off by default — the shipped
# WASM build (browser consumers) must not link the circuit. Standalone tooling (the CLI) enables
# it explicitly.
Expand Down
2 changes: 1 addition & 1 deletion packages/wasm-utxo/cli/src/psbt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ pub enum PsbtCommand {
/// Network (must be zec or tzec)
#[arg(long, short, value_enum)]
network: NetworkArg,
/// Halo2 proof bytes from the external proof service, hex-encoded
/// Halo2 proof bytes from the external prover, hex-encoded
#[arg(long)]
proof: Option<String>,
/// Produce the proof locally instead of supplying one via --proof (heavier: builds a
Expand Down
280 changes: 268 additions & 12 deletions packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodBitGoPsbt.ts

Large diffs are not rendered by default.

16 changes: 11 additions & 5 deletions packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodTypes.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/**
* Shared Zcash Ironwood (v6) types for the shielded-spend flow.
*
* These describe what the build service, the client and KMS each see as the PSBT moves
* through the build → KMS → sign → prove → combine flow. Parse-facing types live in
* These describe what each party sees as the PSBT moves through the
* build → ciphertexts → sign → prove → combine flow. Parse-facing types live in
* `ZcashIronwoodBitGoPsbt.ts`; this module holds the shared vocabulary.
*/

import { type ZcashIronwoodWitness } from "./ZcashIronwoodWitness.js";

/** One shielded note to spend, as passed to `addShieldedSpends`. */
export type ZcashShieldedSpendRequest = {
/** Raw 43-byte Orchard recipient (diversifier ‖ pk_d). */
Expand All @@ -18,9 +20,13 @@ export type ZcashShieldedSpendRequest = {
rseed: Uint8Array;
/** The note commitment the on-chain action carried. */
cmx: Uint8Array;
/** Merkle witness (position + auth path) proving `cmx` under the bundle's anchor. */
witness: { position: number; authPath: Uint8Array[] };
/** The spend's nullifier, from the note store or from KMS. */
/**
* Merkle witness proving `cmx` under the bundle's anchor — a validated
* {@link ZcashIronwoodWitness}, built with `ZcashIronwoodWitness.build(cmx, position,
* authPath, anchor)`.
*/
witness: ZcashIronwoodWitness;
/** The spend's nullifier, from the note store or derived by the viewing-key holder. */
nullifier: Uint8Array;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1493,7 +1493,7 @@ impl BitGoPsbt {
///
/// Copies per input: partial_sigs, tap_key_sig, tap_script_sigs, proprietary.
///
/// Not supported for Zcash v6 (Ironwood) PSBTs: `deserialize_stripped` exists to accept an HSM
/// Not supported for Zcash v6 (Ironwood) PSBTs: `deserialize_stripped` exists to accept a signer
/// response that has dropped everything but the signatures, but v6 bytes route to
/// `deserialize_v6`, which requires the consensus branch id and the PCZT — so a stripped v6
/// response would fail with a "missing its Ironwood PCZT" error describing a corrupt PSBT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ pub enum IronwoodPsbtError {
UnknownAction,
/// The action index names a fabricated (dummy) half, not a real spend or output.
DummyAction,
/// Ciphertexts first: these real outputs' `out_ciphertext`s are still pending — the KMS
/// ciphertext round must run before randomizers, sighashes, or proof requests.
/// Ciphertexts first: these real outputs' `out_ciphertext`s are still pending — the
/// ciphertexts must be installed before randomizers, sighashes, or proof requests.
OutCiphertextPending {
/// The pending real outputs' action indices.
action_indices: Vec<usize>,
Expand Down Expand Up @@ -76,6 +76,15 @@ pub enum IronwoodPsbtError {
},
/// The operation has nothing to act on: there is no pending item, or the batch is empty.
NothingPending,
/// A caller-supplied byte field has the wrong length.
BadLength {
/// The field's name as the caller spelled it (`outCiphertext`, `anchor`, …).
field: &'static str,
/// The field's fixed size.
expected: usize,
/// The supplied length.
actual: usize,
},
/// The Ironwood Constructor rejected the request; the wrapped
/// [`IronwoodBuildError`](crate::zcash::ironwood_build::IronwoodBuildError) carries the
/// reason (and its own `IronwoodBuildError.*` code).
Expand Down Expand Up @@ -119,7 +128,7 @@ impl std::fmt::Display for IronwoodPsbtError {
IronwoodPsbtError::OutCiphertextPending { action_indices } => write!(
f,
"real outputs {action_indices:?} are still pending their out_ciphertext; \
run the KMS ciphertext round first"
install the out_ciphertexts first"
),
IronwoodPsbtError::OutCiphertextConflict { action_index } => write!(
f,
Expand Down Expand Up @@ -158,6 +167,11 @@ impl std::fmt::Display for IronwoodPsbtError {
IronwoodPsbtError::NothingPending => {
write!(f, "there is nothing pending for this operation")
}
IronwoodPsbtError::BadLength {
field,
expected,
actual,
} => write!(f, "{field} must be {expected} bytes, got {actual}"),
IronwoodPsbtError::Build(e) => write!(f, "{e}"),
IronwoodPsbtError::Pczt(e) => write!(f, "{e}"),
IronwoodPsbtError::Verify(e) => write!(f, "{e}"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -965,8 +965,8 @@ impl ZcashBitGoPsbt {

/// Like [`Self::combine_ironwood_proof`], but produces the `zkproof` itself (via
/// [`crate::zcash::ironwood_build::create_proof`]) rather than requiring it from an external
/// proof service. Behind the `orchard-proving` feature — standalone tooling (e.g. the CLI)
/// that has no external proof service to call can enable it; the shipped WASM build must not,
/// prover. Behind the `orchard-proving` feature — standalone tooling (e.g. the CLI)
/// that has no external prover to call can enable it; the shipped WASM build must not,
/// since it would link the halo2 circuit into every consumer. Consumes `self`.
#[cfg(feature = "orchard-proving")]
pub fn combine_ironwood_proof_locally<R: rand::RngCore + rand::CryptoRng>(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! The Ironwood **spend** PSBT workflow: the keyless Constructor entry point
//! ([`ZcashBitGoPsbt::add_shielded_spends`]) and the one-input-kind rule.
//!
//! The build service (IMS) selects notes and calls [`ZcashBitGoPsbt::add_shielded_spends`]
//! with the notes' plaintext, their witnesses, the KMS-derived nullifiers and the requested
//! outputs — and no key at all. The constructed PCZT is stored in the PSBT together with the
//! The caller selects notes and calls [`ZcashBitGoPsbt::add_shielded_spends`] with the notes'
//! plaintext, their witnesses, their nullifiers and the requested outputs — and no key at
//! all. The constructed PCZT is stored in the PSBT together with the
//! per-output Unified Addresses.
//!
//! The PSBT never carries the wallet's spend-validating key `ak`, nor the spends' randomizers
Expand All @@ -25,20 +25,21 @@ use super::{IronwoodOutputRequest, ZcashBitGoPsbt};
impl ZcashBitGoPsbt {
/// Constructor role: build the shielded spends (and optionally shielded outputs) as a
/// single **keyless** orchard PCZT and store it in the PSBT — the deshield and
/// shielded→shielded Constructor for a build service that holds no viewing keys.
/// shielded→shielded Constructor for a caller that holds no viewing keys.
///
/// Each entry of `spends` carries the spent note's plaintext (`recipient`/`value`/`rho`/
/// `rseed`/`cmx`), its Merkle witness, and the nullifier (from the note store, or from
/// KMS's `generate-nullifiers`). Each entry of `outputs` is a requested shielded output
/// (its `ovk` must be `None`: KMS owns encryption in the spend flow). The bundle has
/// `rseed`/`cmx`), its Merkle witness, and the nullifier (from the note store, or derived
/// by the viewing-key holder). Each entry of `outputs` is a requested shielded output
/// (its `ovk` must be `None`: encryption belongs to the viewing-key holder in the spend
/// flow). The bundle has
/// `max(spends, outputs)` actions; the actions are shuffled, so the returned
/// [`IronwoodActionMap`](crate::zcash::ironwood_build::IronwoodActionMap) gives, for each
/// `spends[i]`/`outputs[i]`, the action index it landed at.
///
/// Every real spend carries [`PLACEHOLDER_RK`](crate::zcash::ironwood_build::PLACEHOLDER_RK)
/// as its `rk` and no `alpha`; the signer installs the randomized `rk = ak.randomize(alpha)`
/// before any sighash is computed. Every output's `out_ciphertext` is a random placeholder —
/// KMS replaces it under its FVK `ovk`. No FVK ever enters the stored
/// before any sighash is computed. Every output's `out_ciphertext` is a random placeholder,
/// replaced under the FVK's `ovk`. No FVK ever enters the stored
/// PCZT.
///
/// Rules enforced here:
Expand Down Expand Up @@ -89,7 +90,7 @@ impl ZcashBitGoPsbt {
self.store_ironwood_pczt_and_unified_addresses(pczt, action_map, outputs)?;
// Every real output starts pending its `out_ciphertext`: the
// little-endian bitset over the bundle's actions starts all-clear, so
// `pending_out_ciphertexts` reports the full request until KMS's ciphertexts land.
// `pending_out_ciphertexts` reports the full request until the ciphertexts land.
super::propkv::set_ironwood_out_ciphertexts_set(
&mut self.psbt,
vec![0u8; action_count.div_ceil(8)],
Expand Down Expand Up @@ -124,7 +125,7 @@ impl ZcashBitGoPsbt {
Ok(())
}

// ---- The KMS out_ciphertext / proof workflow ----
// ---- The out_ciphertext / proof workflow ----

/// The stored PCZT, raw and deserialized, or [`IronwoodPsbtError::NotSpendBundle`] if
/// absent.
Expand All @@ -142,7 +143,7 @@ impl ZcashBitGoPsbt {
///
/// The ciphertexts-first gate runs on bundles mid-way through the signer's randomizer writes,
/// which the full verifier rejects, so it uses this classification alone; the paths that hand
/// data to KMS run the verifier first.
/// the PCZT out run the verifier first.
fn real_output_indices(pczt: &orchard::pczt::Bundle) -> Vec<usize> {
pczt.actions()
.iter()
Expand Down Expand Up @@ -188,9 +189,8 @@ impl ZcashBitGoPsbt {
self.pending_out_ciphertexts_in(&pczt)
}

/// The KMS out-ciphertext request: the raw PCZT bytes plus the real
/// outputs still pending their `out_ciphertext`. KMS decrypts nothing here — it
/// re-encrypts each pending output under its FVK `ovk` and returns one 80-byte
/// The out-ciphertext request: the raw PCZT bytes plus the real outputs still pending
/// their `out_ciphertext`. The viewing-key holder decrypts nothing here — it re-encrypts each pending output under its FVK `ovk` and returns one 80-byte
/// ciphertext per action index, applied with [`Self::add_out_ciphertexts`].
///
/// Errors with [`IronwoodPsbtError::NotSpendBundle`] when no PCZT is stored or the
Expand Down Expand Up @@ -284,7 +284,7 @@ impl ZcashBitGoPsbt {
Ok(())
}

/// Apply the KMS out-ciphertext response: one `(action_index,
/// Apply the out-ciphertext response: one `(action_index,
/// out_ciphertext)` pair per pending real output. **Atomic** — every entry is validated
/// before anything is written, so one bad entry stores nothing.
///
Expand Down
Loading
Loading