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
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ export class ZcashIronwoodBitGoPsbt extends ZcashBitGoPsbt<
/**
* Parse the transaction through the Zcash parse, which surfaces the shielded side the
* generic parse refuses to drop: per-action output indices, the shielded spends, and the
* bundle's value balance. The plaintext spend info is unverified until the key-free
* verifier lands (plan PR 4).
* bundle's value balance. The key-free verifier checks the bundle before any plaintext
* value is returned.
*/
override parseTransactionWithWalletKeys(
walletKeys: WalletKeysArg,
Expand Down
10 changes: 5 additions & 5 deletions packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
* 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 flow in `docs/ironwood-shielded-spend-plan.md` §2. Parse-facing types live in
* through the build → KMS → sign → prove → combine flow. Parse-facing types live in
* `ZcashIronwoodBitGoPsbt.ts`; this module holds the shared vocabulary.
*/

/** One shielded note to spend, as passed to `addShieldedSpends` (plan PR 5). */
/** One shielded note to spend, as passed to `addShieldedSpends`. */
export type ZcashShieldedSpendRequest = {
/** Raw 43-byte Orchard recipient (diversifier ‖ pk_d). */
recipient: Uint8Array;
Expand All @@ -24,7 +24,7 @@ export type ZcashShieldedSpendRequest = {
nullifier: Uint8Array;
};

/** One shielded output to create, as passed to `addShieldedSpends` (plan PR 5). */
/** One shielded output to create, as passed to `addShieldedSpends`. */
export type ZcashShieldedOutputRequest = {
/** Raw 43-byte Orchard recipient. */
recipient: Uint8Array;
Expand Down Expand Up @@ -73,7 +73,7 @@ export type ZcashShieldedSpend = {
signed: boolean;
};

/** The client-side verification report of a shielded bundle (plan PR 4). */
/** The client-side verification report of a shielded bundle. */
export type IronwoodVerificationReport = {
/** The bundle's real spends, as verified. */
spends: ZcashShieldedSpend[];
Expand All @@ -85,7 +85,7 @@ export type IronwoodVerificationReport = {
anchor: Uint8Array;
};

/** Orchestration state of the shielded side of a spend PSBT (plan PR 7). */
/** Orchestration state of the shielded side of a spend PSBT. */
export type IronwoodShieldedStatus = {
inputKind: ZcashInputKind;
/** Action indices whose `out_ciphertext` is still pending. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,7 @@ impl BitGoPsbt {
..Default::default()
};

// Rule 0 (plan §3): the Zcash arm rejects a transparent input on a PSBT that already
// One input kind: the Zcash arm rejects a transparent input on a PSBT that already
// spends shielded notes. A stored PCZT with only dummy spends — the shielding flow —
// is fine.
if let BitGoPsbt::Zcash(zcash, _) = self {
Expand Down Expand Up @@ -901,7 +901,7 @@ impl BitGoPsbt {
value: u64,
options: ReplayProtectionOptions,
) -> Result<usize, String> {
// Rule 0 (plan §3): the Zcash arm rejects a transparent input on a PSBT that already
// One input kind: the Zcash arm rejects a transparent input on a PSBT that already
// spends shielded notes (see `add_input_at_index`).
if let BitGoPsbt::Zcash(zcash, _) = self {
zcash.reject_transparent_input_with_spend_bundle()?;
Expand Down Expand Up @@ -1170,7 +1170,7 @@ impl BitGoPsbt {
script_id: ScriptId,
options: WalletInputOptions,
) -> Result<usize, String> {
// Rule 0 (plan §3): the Zcash arm rejects a transparent input on a PSBT that already
// One input kind: the Zcash arm rejects a transparent input on a PSBT that already
// spends shielded notes (see `add_input_at_index`).
if let BitGoPsbt::Zcash(zcash, _) = self {
zcash.reject_transparent_input_with_spend_bundle()?;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Typed errors for the Ironwood PSBT workflow (plan §6.4).
//! Typed errors for the Ironwood PSBT workflow.
//!
//! Every new Ironwood method on [`super::ZcashBitGoPsbt`] returns
//! [`IronwoodPsbtError`], whose variant name is the JS `err.code`
Expand All @@ -11,7 +11,7 @@
/// An Ironwood PSBT workflow error.
#[derive(Debug, strum::IntoStaticStr)]
pub enum IronwoodPsbtError {
/// Rule 0 (plan §3): a v6 PSBT spends either transparent inputs or shielded notes,
/// One input kind: a v6 PSBT spends either transparent inputs or shielded notes,
/// never both. [`super::ZcashBitGoPsbt::add_shielded_spends`] rejects a PSBT that
/// already carries transparent inputs.
MixedInputKinds,
Expand All @@ -21,6 +21,43 @@ pub enum IronwoodPsbtError {
/// A caller-supplied Unified Address is not a valid Unified Address, has no Orchard
/// receiver, or its Orchard receiver is not the output's `recipient`.
BadUnifiedAddress(String),
/// The PSBT carries no Ironwood PCZT, or its bundle is not a spend bundle (no real
/// spends), and the operation needs one.
NotSpendBundle,
/// An action index the operation named does not exist in the bundle.
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.
OutCiphertextPending {
/// The pending real outputs' action indices.
action_indices: Vec<usize>,
},
/// The action already has an `out_ciphertext`, and the entry carries different bytes;
/// re-delivering identical bytes is accepted.
OutCiphertextConflict {
/// The offending action index.
action_index: usize,
},
/// Randomizers installed: these real spends still carry the placeholder `rk` — the signer's
/// randomizers must be installed before the proof request.
RandomizerPending {
/// The pending real spends' action indices.
action_indices: Vec<usize>,
},
/// Signed before proven: these real spends have no `spend_auth_sig` yet — the proof is
/// requested only after the wallet has signed.
SpendSignaturePending {
/// The unsigned real spends' action indices.
action_indices: Vec<usize>,
},
/// The bundle is frozen: a spend-auth signature (or a transparent
/// partial signature, as defense in depth) exists, so ciphertexts can no longer be
/// installed.
AlreadySigned,
/// The operation has nothing to act on: there is no pending item, or the batch is empty.
NothingPending,
/// The Ironwood Constructor rejected the request; the wrapped
/// [`IronwoodBuildError`](crate::zcash::ironwood_build::IronwoodBuildError) carries the
/// reason (and its own `IronwoodBuildError.*` code).
Expand All @@ -29,6 +66,10 @@ pub enum IronwoodPsbtError {
/// [`IronwoodPcztError`](crate::zcash::ironwood_pczt::IronwoodPcztError) carries the
/// reason.
Pczt(crate::zcash::ironwood_pczt::IronwoodPcztError),
/// The key-free verifier rejected the bundle; the wrapped
/// [`IronwoodVerifyError`](crate::zcash::ironwood_build::IronwoodVerifyError) carries
/// the failing check (and its own `IronwoodVerifyError.<check>` code).
Verify(crate::zcash::ironwood_build::IronwoodVerifyError),
}

impl std::fmt::Display for IronwoodPsbtError {
Expand All @@ -44,8 +85,46 @@ impl std::fmt::Display for IronwoodPsbtError {
"an Ironwood PCZT is already present; only one call is supported"
),
IronwoodPsbtError::BadUnifiedAddress(e) => write!(f, "invalid unified address: {e}"),
IronwoodPsbtError::NotSpendBundle => {
write!(f, "this PSBT carries no Ironwood spend bundle")
}
IronwoodPsbtError::UnknownAction => {
write!(f, "the named action index does not exist in the bundle")
}
IronwoodPsbtError::DummyAction => write!(
f,
"the named action is a fabricated dummy, not a real spend or output"
),
IronwoodPsbtError::OutCiphertextPending { action_indices } => write!(
f,
"real outputs {action_indices:?} are still pending their out_ciphertext; \
run the KMS ciphertext round first"
),
IronwoodPsbtError::OutCiphertextConflict { action_index } => write!(
f,
"action {action_index}'s out_ciphertext is already set to different bytes"
),
IronwoodPsbtError::RandomizerPending { action_indices } => write!(
f,
"real spends {action_indices:?} still carry the placeholder rk; install the \
signer's randomizers first"
),
IronwoodPsbtError::SpendSignaturePending { action_indices } => write!(
f,
"real spends {action_indices:?} are not signed yet; the proof is requested \
only after the spend-auth signatures are installed"
),
IronwoodPsbtError::AlreadySigned => write!(
f,
"the bundle is frozen: a signature exists, so out_ciphertexts can no longer \
be installed"
),
IronwoodPsbtError::NothingPending => {
write!(f, "there is nothing pending for this operation")
}
IronwoodPsbtError::Build(e) => write!(f, "{e}"),
IronwoodPsbtError::Pczt(e) => write!(f, "{e}"),
IronwoodPsbtError::Verify(e) => write!(f, "{e}"),
}
}
}
Expand All @@ -71,6 +150,7 @@ impl crate::error::WasmErrorCode for IronwoodPsbtError {
match self {
IronwoodPsbtError::Build(error) => crate::error::WasmErrorCode::code(error),
IronwoodPsbtError::Pczt(error) => crate::error::WasmErrorCode::code(error),
IronwoodPsbtError::Verify(error) => crate::error::WasmErrorCode::code(error),
other => format!("IronwoodPsbtError.{}", <&'static str>::from(other)),
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub struct IronwoodOutputRequest {
/// (see [`ZcashBitGoPsbt::replace_ironwood_spend_rk`]).
///
/// Oracle-only: production spends go through
/// [`crate::zcash::ironwood_construct::IronwoodKeylessSpendSpec`] (plan PR 5).
/// [`crate::zcash::ironwood_construct::IronwoodKeylessSpendSpec`].
#[cfg(any(test, feature = "orchard-proving"))]
#[derive(Clone)]
pub struct IronwoodSpendRequest {
Expand Down Expand Up @@ -281,7 +281,7 @@ impl ZcashBitGoPsbt {
///
/// Real spends leave the placeholder `rk` (= the spend's `ak`) in the stored PCZT.
///
/// Oracle-only (plan PR 5): production spends go through
/// Oracle-only: production spends go through
/// [`Self::add_shielded_spends`], the keyless Constructor. This keyed path needs the FVK
/// on the PSBT side and is compiled only under `test` / `orchard-proving`.
#[cfg(any(test, feature = "orchard-proving"))]
Expand Down Expand Up @@ -342,8 +342,19 @@ impl ZcashBitGoPsbt {
let (pczt, action_map) =
ironwood_build::construct_spend_pczt_multi(&spend_specs, &output_specs, anchor, rng)
.map_err(|e| e.to_string())?;
self.store_ironwood_pczt_and_unified_addresses(pczt, action_map, outputs)
.map_err(|e| e.to_string())
let action_count = pczt.actions().len();
let action_map = self
.store_ironwood_pczt_and_unified_addresses(pczt, action_map, outputs)
.map_err(|e| e.to_string())?;
if !spends.is_empty() {
// The keyed Constructor encrypts every out_ciphertext under the FVK's ovk at
// construction, so its spend bundle starts with every ciphertext set.
super::propkv::set_ironwood_out_ciphertexts_set(
&mut self.psbt,
vec![0xff; action_count.div_ceil(8)],
);
}
Ok(action_map)
}

/// Reject a caller-supplied Unified Address whose Orchard receiver is not exactly `recipient`:
Expand Down Expand Up @@ -611,6 +622,9 @@ impl ZcashBitGoPsbt {
let bytes = super::propkv::get_ironwood_pczt(&self.psbt)
.ok_or_else(|| "no Ironwood PCZT stored in PSBT".to_string())?;
let pczt = ironwood_pczt::deserialize_pczt(&bytes).map_err(|e| e.to_string())?;
// Ciphertexts first: the ciphertext round comes before the randomizers.
self.require_out_ciphertexts_set_in(&pczt)
.map_err(|e| e.to_string())?;
ironwood_build::check_spend_rk_replaceable(&pczt, action_index, &rk)
.map_err(Self::ironwood_action_error(action_index))?;

Expand All @@ -633,7 +647,10 @@ impl ZcashBitGoPsbt {
let bytes = super::propkv::get_ironwood_pczt(&self.psbt)
.ok_or_else(|| "no Ironwood PCZT stored in PSBT".to_string())?;
let mut pczt = ironwood_pczt::deserialize_pczt(&bytes).map_err(|e| e.to_string())?;
// Gate before the sighash: it is not final while any real spend's rk is the placeholder.
// Gate before the sighash: it is not final while any real output's out_ciphertext
// or any real spend's rk is still a placeholder.
self.require_out_ciphertexts_set_in(&pczt)
.map_err(|e| e.to_string())?;
ironwood_build::require_spend_rks_replaced(&pczt).map_err(|e| e.to_string())?;

let (amounts, scripts) = self.transparent_input_amounts_and_scripts()?;
Expand Down Expand Up @@ -702,12 +719,12 @@ impl ZcashBitGoPsbt {
Ok(true)
}

/// The keyless verification of the stored PCZT (plan §5): run every check a party
/// The keyless verification of the stored PCZT: run every check a party
/// without viewing keys can run before surfacing any plaintext value, so a tampered
/// bundle fails loudly with the failing check instead of leaking unverified plaintext.
/// Verification only reads the PCZT: its plaintext stays in place for the prover.
///
/// The stored spend-validating key (plan PR 5's `0x08` key) is passed through when
/// The stored spend-validating key is passed through when
/// present; until then the `Rk` check is skipped and only the key-free checks run.
pub(crate) fn keyless_verify(
&self,
Expand Down Expand Up @@ -805,7 +822,7 @@ impl ZcashBitGoPsbt {
if self.require_no_shielded_output_ever_added()? {
return Err("no Ironwood PCZT stored in PSBT".to_string());
}
// The keyless verifier (plan §5) runs every key-free check — including `ValueSum`,
// The keyless verifier runs every key-free check — including `ValueSum`,
// which cross-checks the committed `value_sum` against the plaintext note values —
// before any accounting flows out of here. The plaintext note values are NOT bound
// after deserialization: the proofs bind only `cv_net`/`cm`.
Expand Down Expand Up @@ -966,13 +983,17 @@ impl ZcashBitGoPsbt {
}

/// Transaction Extractor role: given the external prover's `proof` bytes, finalize the
/// transparent inputs, apply the shielded binding signature, and produce the broadcast-ready v6
/// transaction bytes.
/// transparent inputs, apply the shielded binding signature, and produce the broadcast-ready
/// v6 transaction bytes.
///
/// Shielding and shielded spends share this shape: signing is done first, then the
/// prover's proof (from [`Self::proof_request`] for a spend bundle) is passed straight in.
///
/// The PSBT must already carry every transparent input's signatures (via
/// [`Self::add_v6_transparent_signature`]) and the PCZT (from [`Self::add_ironwood_output`]).
/// `rng` seeds both the dummy spend-auth signature (in the IO finalizer) and the binding
/// signature (in the Transaction Extractor). Consumes `self`.
/// [`Self::add_v6_transparent_signature`]) and the PCZT (from [`Self::add_ironwood_output`]
/// or [`Self::add_shielded_spends`]). `rng` seeds both the dummy spend-auth signature (in
/// the IO finalizer) and the binding signature (in the Transaction Extractor). Consumes
/// `self`.
pub fn combine_ironwood_proof<R: rand::RngCore + rand::CryptoRng>(
self,
proof: Vec<u8>,
Expand Down
Loading
Loading