What changed, and why it matters
This commit adds support for a new Zcash transaction format called Ironwood PCZT to the Keystone hardware wallet firmware. It also hardens the wallet against several real attack scenarios: legacy code paths now refuse to handle the new format (instead of mis-processing it), a bug where an attacker could make a payment to themselves look like the user's own change is now blocked, and malformed transaction bundles with contradictory value sums are rejected. The changes are mostly defensive and include many new regression tests.
Review the new cypherpunk path thoroughly, especially the Ironwood signing key collection and the redaction logic, because the diff is large and the security boundary between legacy and cypherpunk paths is new. Ensure the new tests run in CI and that the UI layer (gui_zcash.c) correctly surfaces the new rejection messages to users.
Security signals we found
Rejects legacy code paths for v6/Ironwood PCZTs to prevent mis-handling
Blocks internal-OVK change-spoofing attack in parse and check paths
Rejects empty Sapling bundle with non-zero value_sum before signing
Validates Orchard/Ironwood user_address against decoded recipient
Adds Ironwood bundle redaction in signed QR-sized responses
Stamps firmware version in signed PCZT global proprietary field
Adds numerous regression tests for the above behaviors
Evidence from the diff
The patch extends the Zcash PCZT (Partially Created Zcash Transaction) parser/checker/signer in the Keystone 3 firmware to handle v6/Ironwood transactions while preserving Orchard and transparent behavior. Key changes: (1) legacy multi_coins parse/check/sign paths reject v6 or Ironwood PCZTs rather than mishandling them; (2) new cypherpunk path validates Ironwood bundles with the same Orchard logic; (3) internal-OVK change-spoofing is rejected in both parse and check paths; (4) empty Sapling bundles with non-zero value_sum are rejected; (5) Orchard/Ironwood spend/output value and ownership decoding is enforced; (6) signed responses stamp firmware version and redact optional PCZT fields (including Ironwood bundle data) before QR return. The diff is large (+2036/-270) and adds extensive unit tests.
Changed components
rust/apps/zcash/src/errors.rsrust/apps/zcash/src/lib.rsrust/apps/zcash/src/pczt/check.rsrust/apps/zcash/src/pczt/mod.rsrust/apps/zcash/src/pczt/parse.rsrust/apps/zcash/src/pczt/sign.rsrust/apps/zcash/src/pczt/structs.rsrust/rust_c/src/zcash/structs.rssrc/ui/gui_chain/multi/gui_zcash.cInspect captured patch +2036 / −270
diff --git a/rust/apps/zcash/src/errors.rs b/rust/apps/zcash/src/errors.rs
index 7e3c5f2..cc24297 100644
--- a/rust/apps/zcash/src/errors.rs
+++ b/rust/apps/zcash/src/errors.rs
@@ -29,6 +29,13 @@ impl From<orchard::pczt::ParseError> for ZcashError {
}
}
+#[cfg(feature = "cypherpunk")]
+impl From<zcash_vendor::pczt::orchard::BundleParseError> for ZcashError {
+ fn from(e: zcash_vendor::pczt::orchard::BundleParseError) -> Self {
+ Self::InvalidPczt(alloc::format!("Invalid Orchard bundle: {e:?}"))
+ }
+}
+
impl From<transparent::pczt::ParseError> for ZcashError {
fn from(e: transparent::pczt::ParseError) -> Self {
Self::InvalidPczt(alloc::format!("Invalid transparent bundle: {e:?}"))
diff --git a/rust/apps/zcash/src/lib.rs b/rust/apps/zcash/src/lib.rs
index d7473bb..6b04acc 100644
--- a/rust/apps/zcash/src/lib.rs
+++ b/rust/apps/zcash/src/lib.rs
@@ -95,6 +95,7 @@ pub fn check_pczt_multi_coins<P: consensus::Parameters>(
account_index: u32,
) -> Result<()> {
let pczt = pczt::parse_pczt(pczt)?;
+ reject_legacy_check_unsupported_pczt(&pczt)?;
let account_pubkey = transparent_account_pubkey_from_xpub(xpub)?;
let account_index = zip32::AccountId::try_from(account_index)
.map_err(|_e| ZcashError::InvalidDataError("invalid account index".to_string()))?;
@@ -134,6 +135,21 @@ fn transparent_account_pubkey_from_xpub(
.map_err(|e| ZcashError::InvalidDataError(e.to_string()))
}
+#[cfg(feature = "multi_coins")]
+fn reject_legacy_check_unsupported_pczt(pczt: &Pczt) -> Result<()> {
+ #[cfg(zcash_unstable = "nu6.3")]
+ {
+ // The legacy multi-coins check path only verifies transparent data.
+ // Reject V6/Ironwood PCZTs so check, parse, and sign enforce the same boundary.
+ if pczt::pczt_requires_cypherpunk_support(pczt) {
+ return Err(ZcashError::InvalidPczt(
+ "V6 or Ironwood PCZTs require cypherpunk checking support".to_string(),
+ ));
+ }
+ }
+ Ok(())
+}
+
/// Parses a Partially Created Zcash Transaction (PCZT) and extracts its details.
///
/// This function takes a binary PCZT and a Unified Full Viewing Key (UFVK), parses the transaction,
@@ -218,3 +234,594 @@ pub fn sign_pczt(pczt: &[u8], seed: &[u8]) -> Result<Vec<u8>> {
let pczt = pczt::parse_pczt(pczt)?;
pczt::sign::sign_pczt(pczt, seed)
}
+
+#[cfg(all(test, feature = "multi_coins", not(feature = "cypherpunk")))]
+mod legacy_tests {
+ use super::*;
+ use zcash_vendor::{
+ pczt::roles::creator::Creator,
+ zcash_protocol::consensus::{BranchId, MainNetwork, NetworkConstants},
+ };
+
+ fn assert_invalid_pczt_message<T: core::fmt::Debug>(result: Result<T>, expected: &str) {
+ match result {
+ Err(ZcashError::InvalidPczt(message)) if message == expected => {}
+ other => panic!("unexpected InvalidPczt result: {other:?}"),
+ }
+ }
+
+ #[test]
+ fn legacy_parse_uses_seed_fingerprint_and_check_validates_transparent_account() {
+ let sample = pczt::legacy_test_support::legacy_transparent_sample();
+
+ let parsed = parse_pczt_multi_coins(&MainNetwork, &sample.bytes, &sample.seed_fingerprint)
+ .expect("selected account PCZT should parse");
+ assert!(parsed
+ .get_transparent()
+ .unwrap()
+ .get_from()
+ .first()
+ .unwrap()
+ .get_is_mine());
+ check_pczt_multi_coins(
+ &MainNetwork,
+ &sample.bytes,
+ &sample.xpub,
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .expect("selected account PCZT should check");
+
+ let account_one_pczt =
+ pczt::legacy_test_support::legacy_transparent_pczt_with_input_derivation(
+ &sample.bytes,
+ sample.seed_fingerprint,
+ sample.input_pubkey,
+ pczt::legacy_test_support::legacy_transparent_path_for_account(1),
+ );
+
+ parse_pczt_multi_coins(&MainNetwork, &account_one_pczt, &sample.seed_fingerprint)
+ .expect("parse uses seed fingerprint ownership only");
+ assert_invalid_pczt_message(
+ check_pczt_multi_coins(
+ &MainNetwork,
+ &account_one_pczt,
+ &sample.xpub,
+ &sample.seed_fingerprint,
+ 0,
+ ),
+ "transparent input bip32 derivation path invalid",
+ );
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn legacy_check_rejects_v6_pczt() {
+ let pczt = Creator::new_v6(
+ BranchId::Nu6_3.into(),
+ 10,
+ MainNetwork.coin_type(),
+ [0; 32],
+ [0; 32],
+ [1; 32],
+ )
+ .build();
+
+ let result = check_pczt_multi_coins(
+ &MainNetwork,
+ &pczt.serialize(),
+ "not-an-xpub",
+ &[7u8; 32],
+ 0,
+ );
+
+ assert!(matches!(
+ result,
+ Err(ZcashError::InvalidPczt(msg))
+ if msg == "V6 or Ironwood PCZTs require cypherpunk checking support"
+ ));
+ }
+}
+
+#[cfg(feature = "cypherpunk")]
+#[cfg(test)]
+mod tests {
+ use alloc::{collections::BTreeMap, string::String, vec::Vec};
+
+ use consensus::MainNetwork;
+ use keystore::algorithms::zcash::{calculate_seed_fingerprint, derive_ufvk};
+ use serde::{Deserialize, Serialize};
+ use zcash_vendor::zcash_protocol::constants;
+
+ use super::*;
+ extern crate std;
+
+ #[derive(Serialize, Deserialize)]
+ struct PcztMirror {
+ global: GlobalMirror,
+ transparent: ::pczt::transparent::Bundle,
+ sapling: SaplingBundleMirror,
+ orchard: ::pczt::orchard::Bundle,
+ #[cfg(zcash_unstable = "nu6.3")]
+ ironwood: ::pczt::orchard::Bundle,
+ }
+
+ #[derive(Serialize, Deserialize)]
+ struct GlobalMirror {
+ tx_version: u32,
+ version_group_id: u32,
+ consensus_branch_id: u32,
+ fallback_lock_time: Option<u32>,
+ expiry_height: u32,
+ coin_type: u32,
+ tx_modifiable: u8,
+ proprietary: BTreeMap<String, Vec<u8>>,
+ }
+
+ #[derive(Serialize, Deserialize)]
+ struct SaplingBundleMirror {
+ spends: Vec<SaplingSpendMirror>,
+ outputs: Vec<SaplingOutputMirror>,
+ value_sum: i128,
+ anchor: [u8; 32],
+ bsk: Option<[u8; 32]>,
+ }
+
+ #[derive(Serialize, Deserialize)]
+ struct SaplingSpendMirror;
+
+ #[serde_with::serde_as]
+ #[derive(Serialize, Deserialize)]
+ struct SaplingOutputMirror {
+ cv: [u8; 32],
+ cmu: [u8; 32],
+ ephemeral_key: [u8; 32],
+ enc_ciphertext: Vec<u8>,
+ out_ciphertext: Vec<u8>,
+ #[serde_as(as = "Option<[_; 144]>")]
+ zkproof: Option<[u8; 144]>,
+ #[serde_as(as = "Option<[_; 43]>")]
+ recipient: Option<[u8; 43]>,
+ value: Option<u64>,
+ rseed: Option<[u8; 32]>,
+ rcv: Option<[u8; 32]>,
+ ock: Option<[u8; 32]>,
+ zip32_derivation: Option<Zip32DerivationMirror>,
+ user_address: Option<String>,
+ proprietary: BTreeMap<String, Vec<u8>>,
+ }
+
+ #[derive(Serialize, Deserialize)]
+ struct Zip32DerivationMirror {
+ seed_fingerprint: [u8; 32],
+ derivation_path: Vec<u32>,
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ fn v5_pczt_with_ironwood_actions() -> Vec<u8> {
+ let sample = pczt::test_support::sample_ironwood_pczt();
+ let mut bytes = sample.bytes;
+ let mut pczt: PcztMirror = postcard::from_bytes(&bytes[8..]).unwrap();
+ assert!(!pczt.ironwood.actions().is_empty());
+
+ pczt.global.tx_version = constants::V5_TX_VERSION;
+ pczt.global.version_group_id = constants::V5_VERSION_GROUP_ID;
+
+ bytes.truncate(8);
+ postcard::to_extend(&pczt, bytes).unwrap()
+ }
+
+ fn assert_invalid_pczt_message<T: core::fmt::Debug>(result: Result<T>, expected: &str) {
+ assert_eq!(
+ result.unwrap_err(),
+ ZcashError::InvalidPczt(expected.to_string())
+ );
+ }
+
+ fn malformed_pczt_with_empty_sapling_bundle_and_nonzero_value_sum() -> Vec<u8> {
+ use ::pczt::roles::creator::Creator;
+ use zcash_vendor::zcash_protocol::consensus::{BranchId, NetworkConstants};
+
+ let mut bytes = Creator::new(
+ BranchId::Nu6.into(),
+ 10,
+ MainNetwork.coin_type(),
+ [0; 32],
+ [0; 32],
+ )
+ .build()
+ .serialize();
+ let mut pczt: PcztMirror = postcard::from_bytes(&bytes[8..]).unwrap();
+ assert!(pczt.sapling.spends.is_empty());
+ assert!(pczt.sapling.outputs.is_empty());
+
+ pczt.sapling.value_sum = 1;
+
+ bytes.truncate(8);
+ postcard::to_extend(&pczt, bytes).unwrap()
+ }
+
+ /// A PCZT whose Sapling bundle is empty but declares a non-zero value sum is malformed
+ /// and must be rejected before signing.
+ #[test]
+ fn test_check_pczt_rejects_empty_sapling_bundle_with_nonzero_value_sum() {
+ let seed = [9u8; 32];
+ let malformed_pczt = malformed_pczt_with_empty_sapling_bundle_and_nonzero_value_sum();
+ let ufvk = derive_ufvk(&MainNetwork, &seed, "m/32'/133'/0'").unwrap();
+ let seed_fingerprint = calculate_seed_fingerprint(&seed).unwrap();
+
+ let result = check_pczt_cypherpunk(&MainNetwork, &malformed_pczt, &ufvk, &seed_fingerprint, 0);
+
+ assert_invalid_pczt_message(
+ result,
+ "sapling value_sum must be zero when Sapling bundle is empty",
+ );
+ }
+
+ /// Regression test for internal-OVK change spoofing.
+ ///
+ /// An Orchard output paid to a non-wallet recipient but encrypted with the wallet's
+ /// *internal* OVK must be rejected: otherwise funds leaving the wallet could be displayed
+ /// (and signed) as if they were the user's own change. Both the parse path (what the user
+ /// sees) and the check path (pre-sign validation) must reject it.
+ #[test]
+ fn test_parse_pczt_rejects_orchard_internal_ovk_change_spoofing() {
+ use ::pczt::roles::creator::Creator;
+ use bitcoin::secp256k1::Secp256k1;
+ use rand_core::OsRng;
+ use zcash_primitives::transaction::{
+ builder::{BuildConfig, Builder, PcztResult},
+ fees::zip317,
+ };
+ use zcash_vendor::{
+ orchard,
+ transparent::{
+ bundle as transparent,
+ keys::{AccountPrivKey, IncomingViewingKey},
+ },
+ zcash_protocol::{memo::MemoBytes, value::Zatoshis},
+ zip32,
+ };
+
+ let params = MainNetwork;
+
+ let victim_seed = [7u8; 32];
+ let ufvk_text = derive_ufvk(¶ms, &victim_seed, "m/32'/133'/0'").unwrap();
+ let ufvk = UnifiedFullViewingKey::decode(¶ms, &ufvk_text).unwrap();
+ let victim_fvk = ufvk.orchard().unwrap().clone();
+
+ let victim_account =
+ AccountPrivKey::from_seed(¶ms, &victim_seed, zip32::AccountId::ZERO).unwrap();
+ let (victim_addr, address_index) = victim_account
+ .to_account_pubkey()
+ .derive_external_ivk()
+ .unwrap()
+ .default_address();
+ let victim_sk = victim_account
+ .derive_external_secret_key(address_index)
+ .unwrap();
+ let secp = Secp256k1::signing_only();
+ let victim_pubkey = victim_sk.public_key(&secp);
+
+ // Attacker-controlled Orchard recipient that does NOT belong to the victim wallet.
+ let attacker_sk = orchard::keys::SpendingKey::from_bytes([2; 32]).unwrap();
+ let attacker_fvk = orchard::keys::FullViewingKey::from(&attacker_sk);
+ let attacker_recipient = attacker_fvk.address_at(0u32, orchard::keys::Scope::External);
+ let victim_change = victim_fvk.address_at(0u32, orchard::keys::Scope::Internal);
+
+ let coin = transparent::TxOut::new(
+ Zatoshis::const_from_u64(1_000_000),
+ victim_addr.script().into(),
+ );
+ let mut builder = Builder::new(
+ ¶ms,
+ 10_000_000.into(),
+ BuildConfig::Standard {
+ sapling_anchor: None,
+ orchard_anchor: Some(orchard::Anchor::empty_tree()),
+ ironwood_anchor: None,
+ },
+ );
+ builder
+ .add_transparent_p2pkh_input(
+ victim_pubkey,
+ transparent::OutPoint::new([1u8; 32], 0),
+ coin,
+ )
+ .unwrap();
+ // Pay the attacker, but encrypt the output with the victim's INTERNAL ovk (the spoof).
+ builder
+ .add_orchard_output::<zip317::FeeRule>(
+ Some(victim_fvk.to_ovk(orchard::keys::Scope::Internal)),
+ attacker_recipient,
+ Zatoshis::const_from_u64(100_000),
+ MemoBytes::empty(),
+ )
+ .unwrap();
+ // A genuine internal-ovk change output back to the victim.
+ builder
+ .add_orchard_output::<zip317::FeeRule>(
+ Some(victim_fvk.to_ovk(orchard::keys::Scope::Internal)),
+ victim_change,
+ Zatoshis::const_from_u64(885_000),
+ MemoBytes::empty(),
+ )
+ .unwrap();
+
+ let PcztResult { pczt_parts, .. } = builder
+ .build_for_pczt(OsRng, &zip317::FeeRule::standard())
+ .unwrap();
+ let pczt_bytes = Creator::build_from_parts(pczt_parts).unwrap().serialize();
+ let seed_fingerprint = calculate_seed_fingerprint(&victim_seed).unwrap();
+
+ let expected =
+ "output was recoverable with an internal OVK but does not belong to this wallet";
+
+ match parse_pczt_cypherpunk(¶ms, &pczt_bytes, &ufvk_text, &seed_fingerprint) {
+ Err(ZcashError::InvalidPczt(msg)) if msg.contains(expected) => {}
+ other => panic!("parse must reject internal-OVK change spoofing, got: {other:?}"),
+ }
+
+ match check_pczt_cypherpunk(¶ms, &pczt_bytes, &ufvk_text, &seed_fingerprint, 0) {
+ Err(ZcashError::InvalidPczt(msg)) if msg.contains(expected) => {}
+ other => panic!("check must reject internal-OVK change spoofing, got: {other:?}"),
+ }
+ }
+
+ #[test]
+ fn test_get_address() {
+ let address = get_address(&MainNetwork, "uview1s2e0495jzhdarezq4h4xsunfk4jrq7gzg22tjjmkzpd28wgse4ejm6k7yfg8weanaghmwsvc69clwxz9f9z2hwaz4gegmna0plqrf05zkeue0nevnxzm557rwdkjzl4pl4hp4q9ywyszyjca8jl54730aymaprt8t0kxj8ays4fs682kf7prj9p24dnlcgqtnd2vnskkm7u8cwz8n0ce7yrwx967cyp6dhkc2wqprt84q0jmwzwnufyxe3j0758a9zgk9ssrrnywzkwfhu6ap6cgx3jkxs3un53n75s3");
+ assert_eq!(address.unwrap(), "u1tqdskj32l9udfp0rysmca6gpz73fdqc2rmeenyhh0nfrq4vgak284ehkxefw5cf9495rdur0tparuntevp6nnetzjkyzv08m524e4swwk94asas7hm2ad5w5c64zz00hmr7nux0yhaz");
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_pczt_ironwood_to_ironwood() {
+ let sample = pczt::test_support::sample_ironwood_pczt();
+ let seed_fingerprint = sample.seed_fingerprint;
+ let parsed_pczt = parse_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.ufvk_text,
+ &seed_fingerprint,
+ )
+ .unwrap();
+
+ assert!(parsed_pczt.get_ironwood().is_some());
+ assert!(parsed_pczt.get_orchard().is_none());
+ assert_eq!(parsed_pczt.get_fee_value(), "0.0001 ZEC");
+
+ check_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.ufvk_text,
+ &seed_fingerprint,
+ 0,
+ )
+ .unwrap();
+
+ let signed = sign_pczt(&sample.bytes, &sample.seed).expect("Ironwood PCZT should sign");
+ let signed_pczt = Pczt::parse(&signed).expect("signed PCZT must parse");
+ assert!(
+ signed_pczt
+ .ironwood()
+ .actions()
+ .iter()
+ .any(|action| action.spend().spend_auth_sig().is_some()),
+ "Ironwood spend authorization signature must be present",
+ );
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_parse_pczt_orchard_decodes_spend_and_change() {
+ let sample = pczt::test_support::sample_orchard_change_pczt();
+ let parsed = parse_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ )
+ .unwrap();
+
+ // Decodes as an Orchard (not Ironwood) bundle.
+ assert!(parsed.get_ironwood().is_none());
+ let orchard = parsed.get_orchard().expect("orchard bundle should decode");
+
+ // The wallet's own spend is recognized, with its value.
+ let from = orchard.get_from();
+ assert_eq!(from.len(), 1);
+ assert!(from[0].get_is_mine());
+ assert!(from[0].get_address().is_none());
+ assert_eq!(from[0].get_value(), "0.01 ZEC");
+
+ // The output value and recipient are decoded (a wallet output, not change).
+ let to = orchard.get_to();
+ assert_eq!(to.len(), 1);
+ assert_eq!(to[0].get_value(), "0.0099 ZEC");
+ assert!(to[0].get_address().starts_with("u1"));
+ assert!(!to[0].get_is_change());
+
+ assert_eq!(parsed.get_fee_value(), "0.0001 ZEC");
+
+ // The same PCZT also passes the pre-sign checks.
+ check_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .unwrap();
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_parse_and_check_ignore_unsupported_ironwood_spend_zip32_path() {
+ let sample = pczt::test_support::sample_ironwood_pczt();
+ let parsed_pczt = parse_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ )
+ .unwrap();
+ assert!(parsed_pczt
+ .get_ironwood()
+ .unwrap()
+ .get_from()
+ .first()
+ .unwrap()
+ .get_is_mine());
+
+ for path in pczt::test_support::unsupported_orchard_spend_paths() {
+ let pczt = pczt::test_support::ironwood_pczt_with_spend_derivation(
+ &sample.bytes,
+ sample.seed_fingerprint,
+ path,
+ );
+
+ parse_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &pczt,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ )
+ .expect("parse uses seed fingerprint ownership only");
+ check_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &pczt,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .expect("check ignores non-selected shielded spend paths");
+ }
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_parse_and_check_ignore_dummy_ironwood_spend_zip32_metadata() {
+ let sample = pczt::test_support::sample_ironwood_pczt();
+ let mut paths = pczt::test_support::unsupported_orchard_spend_paths();
+ paths.push(pczt::test_support::orchard_spend_path_for_account(1));
+
+ for path in paths {
+ let pczt = pczt::test_support::ironwood_pczt_with_dummy_spend_derivation(
+ &sample.bytes,
+ sample.seed_fingerprint,
+ path,
+ );
+
+ let parsed_pczt = parse_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &pczt,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ )
+ .unwrap();
+ assert!(parsed_pczt
+ .get_ironwood()
+ .unwrap()
+ .get_from()
+ .first()
+ .unwrap()
+ .get_is_mine());
+ check_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &pczt,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .unwrap();
+ }
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_parse_check_and_sign_reject_v5_pczt_with_ironwood_actions() {
+ let sample = pczt::test_support::sample_ironwood_pczt();
+ let malformed_pczt = v5_pczt_with_ironwood_actions();
+
+ assert_invalid_pczt_message(
+ parse_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &malformed_pczt,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ ),
+ "Ironwood actions require a v6 PCZT",
+ );
+ assert_invalid_pczt_message(
+ check_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &malformed_pczt,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ 0,
+ ),
+ "Ironwood actions require a v6 PCZT",
+ );
+ assert_invalid_pczt_message(
+ sign_pczt(&malformed_pczt, &sample.seed),
+ "Ironwood actions require a v6 PCZT",
+ );
+ }
+
+ #[test]
+ fn test_get_address_invalid_ufvk() {
+ let invalid_ufvk = "invalid_ufvk_string";
+ let result = get_address(&MainNetwork, invalid_ufvk);
+ assert!(result.is_err());
+ assert!(matches!(
+ result.unwrap_err(),
+ ZcashError::GenerateAddressError(_)
+ ));
+ }
+
+ #[test]
+ fn test_check_pczt_invalid_data() {
+ let invalid_pczt = b"invalid_pczt_data";
+ let seed = hex::decode("d561f5aba9db8b100a9a84197322e522f952171a388ad74eaab1ab9db815be3335c3099a0a2bb0fee57e630db5ed7251412b6bd4b905cf518627411fee3f32dd").unwrap();
+ let ufvk = derive_ufvk(&MainNetwork, &seed, "m/32'/133'/0'").unwrap();
+ let seed_fingerprint = calculate_seed_fingerprint(&seed).unwrap();
+
+ let result = check_pczt_cypherpunk(
+ &MainNetwork,
+ invalid_pczt,
+ &ufvk.to_string(),
+ &seed_fingerprint,
+ 0,
+ );
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), ZcashError::InvalidPczt(_)));
+ }
+
+ #[test]
+ fn test_parse_pczt_invalid_data() {
+ let invalid_pczt = b"invalid_pczt_data";
+ let seed = hex::decode("d561f5aba9db8b100a9a84197322e522f952171a388ad74eaab1ab9db815be3335c3099a0a2bb0fee57e630db5ed7251412b6bd4b905cf518627411fee3f32dd").unwrap();
+ let ufvk = derive_ufvk(&MainNetwork, &seed, "m/32'/133'/0'").unwrap();
+ let seed_fingerprint = calculate_seed_fingerprint(&seed).unwrap();
+
+ let result = parse_pczt_cypherpunk(
+ &MainNetwork,
+ invalid_pczt,
+ &ufvk.to_string(),
+ &seed_fingerprint,
+ );
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), ZcashError::InvalidPczt(_)));
+ }
+
+ #[test]
+ fn test_sign_pczt_invalid_data() {
+ let invalid_pczt = b"invalid_pczt_data";
+ let seed = hex::decode("d561f5aba9db8b100a9a84197322e522f952171a388ad74eaab1ab9db815be3335c3099a0a2bb0fee57e630db5ed7251412b6bd4b905cf518627411fee3f32dd").unwrap();
+
+ let result = sign_pczt(invalid_pczt, &seed);
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), ZcashError::InvalidPczt(_)));
+ }
+}
diff --git a/rust/apps/zcash/src/pczt/check.rs b/rust/apps/zcash/src/pczt/check.rs
index 20f6656..c722867 100644
--- a/rust/apps/zcash/src/pczt/check.rs
+++ b/rust/apps/zcash/src/pczt/check.rs
@@ -42,7 +42,9 @@ pub fn check_pczt_orchard<P: consensus::Parameters>(
pczt: &Pczt,
) -> Result<(), ZcashError> {
super::validate_supported_pczt(pczt)?;
- Verifier::new(pczt.clone())
+ #[cfg(zcash_unstable = "nu6.3")]
+ let should_process_ironwood = super::pczt_should_process_ironwood(pczt);
+ let verifier = Verifier::new(pczt.clone())
.with_orchard(|bundle| {
check_orchard(
params,
@@ -56,6 +58,23 @@ pub fn check_pczt_orchard<P: consensus::Parameters>(
Ok(())
})
.map_err(map_orchard_verifier_error)?;
+ #[cfg(zcash_unstable = "nu6.3")]
+ if should_process_ironwood {
+ verifier
+ .with_ironwood(|bundle| {
+ check_orchard(
+ params,
+ seed_fingerprint,
+ account_index,
+ ufvk,
+ bundle,
+ "Ironwood",
+ )
+ .map_err(pczt::roles::verifier::OrchardError::Custom)?;
+ Ok(())
+ })
+ .map_err(map_orchard_verifier_error)?;
+ }
Ok(())
}
@@ -358,9 +377,13 @@ fn check_action_spend<P: consensus::Parameters>(
) -> Result<(), ZcashError> {
// We can only verify the `nullifier` and `rk` fields of a spend if we know its FVK.
let can_verify_nf_rk = match (spend.value(), spend.fvk(), spend.zip32_derivation()) {
- // If the spend is marked as matching the accounts's FVK, verify with it.
- (_, _, Some(zip32_derivation))
- if zip32_derivation.seed_fingerprint() == seed_fingerprint
+ // Dummy notes use randomly-generated FVKs, so if one is already present then
+ // don't validate using the account's FVK.
+ (Some(value), Some(_), _) if value.inner() == 0 => Some(None),
+ // If the spend is marked as matching the selected account's FVK, verify with it.
+ (Some(value), _, Some(zip32_derivation))
+ if value.inner() != 0
+ && zip32_derivation.seed_fingerprint() == seed_fingerprint
&& zip32_derivation.derivation_path()
== &[
zip32::ChildIndex::hardened(32),
@@ -370,10 +393,7 @@ fn check_action_spend<P: consensus::Parameters>(
{
Some(Some(fvk))
}
- // Dummy notes use randomly-generated FVKs, so if one is already present then
- // don't validate using the account's FVK.
- (Some(value), Some(_), _) if value.inner() == 0 => Some(None),
- // Don't verify `nullifier` or `rk` for any other spends.
+ // Don't verify `nullifier` or `rk` for spends that lack value data.
_ => None,
};
@@ -433,6 +453,9 @@ fn check_action_output<P: consensus::Parameters>(
if let Some((_, address, _)) =
super::parse::decode_output_enc_ciphertext(action, vk.as_ref())?
{
+ if let Some(user_address) = action.output().user_address() {
+ super::parse::validate_orchard_user_address(params, user_address, &address)?;
+ }
if is_internal_ovk && !is_wallet_orchard_address(fvk, &address) {
return Err(ZcashError::InvalidPczt(alloc::format!(
"{pool_label} output was recoverable with an internal OVK but does not belong to this wallet"
@@ -442,5 +465,11 @@ fn check_action_output<P: consensus::Parameters>(
}
}
+ if let (Some(user_address), Some(recipient)) =
+ (action.output().user_address(), action.output().recipient())
+ {
+ super::parse::validate_orchard_user_address(params, user_address, recipient)?;
+ }
+
Ok(())
}
diff --git a/rust/apps/zcash/src/pczt/mod.rs b/rust/apps/zcash/src/pczt/mod.rs
index 0784e3c..31304fa 100644
--- a/rust/apps/zcash/src/pczt/mod.rs
+++ b/rust/apps/zcash/src/pczt/mod.rs
@@ -7,7 +7,7 @@ use alloc::{format, string::ToString};
use zcash_vendor::{
pczt::Pczt,
transparent,
- zcash_protocol::value::ZatBalance,
+ zcash_protocol::{constants, value::ZatBalance},
zip32,
};
@@ -20,9 +20,34 @@ pub(crate) fn parse_pczt(bytes: &[u8]) -> Result<Pczt, ZcashError> {
pub(crate) fn validate_supported_pczt(pczt: &Pczt) -> Result<(), ZcashError> {
validate_sapling_bundle_consistency(pczt)?;
+ #[cfg(zcash_unstable = "nu6.3")]
+ {
+ if pczt_has_ironwood_actions(pczt) && !pczt_is_v6(pczt) {
+ return Err(ZcashError::InvalidPczt(
+ "Ironwood actions require a v6 PCZT".to_string(),
+ ));
+ }
+ }
+
Ok(())
}
+#[cfg(zcash_unstable = "nu6.3")]
+pub(crate) fn pczt_has_ironwood_actions(pczt: &Pczt) -> bool {
+ !pczt.ironwood().actions().is_empty()
+}
+
+#[cfg(zcash_unstable = "nu6.3")]
+pub(crate) fn pczt_is_v6(pczt: &Pczt) -> bool {
+ *pczt.global().tx_version() == constants::V6_TX_VERSION
+ && *pczt.global().version_group_id() == constants::V6_VERSION_GROUP_ID
+}
+
+#[cfg(zcash_unstable = "nu6.3")]
+pub(crate) fn pczt_should_process_ironwood(pczt: &Pczt) -> bool {
+ pczt_is_v6(pczt) || pczt_has_ironwood_actions(pczt)
+}
+
fn validate_sapling_bundle_consistency(pczt: &Pczt) -> Result<(), ZcashError> {
let value_balance = (*pczt.sapling().value_sum())
.try_into()
@@ -74,3 +99,577 @@ pub(crate) fn transparent_derivation_matches_selected_account<
Ok(true)
}
+
+/// Returns the supported account declared by a shielded spend derivation that
+/// belongs to this seed. Missing or different seed fingerprints are not ours
+/// and return `None`; matching fingerprints with paths outside
+/// `m/32'/coin_type'/account'` are invalid.
+#[cfg(feature = "cypherpunk")]
+pub(crate) fn matching_seed_supported_orchard_account(
+ seed_fingerprint: &[u8; 32],
+ derivation: Option<&zcash_vendor::orchard::pczt::Zip32Derivation>,
+ coin_type: u32,
+ pool_label: &str,
+) -> Result<Option<zcash_vendor::zip32::AccountId>, crate::errors::ZcashError> {
+ let Some(derivation) = derivation else {
+ return Ok(None);
+ };
+ if derivation.seed_fingerprint() != seed_fingerprint {
+ return Ok(None);
+ }
+
+ let unsupported_path = || {
+ crate::errors::ZcashError::InvalidPczt(alloc::format!(
+ "unsupported {pool_label} spend ZIP 32 derivation path"
+ ))
+ };
+
+ let [purpose, path_coin_type, account_index] = &derivation.derivation_path()[..] else {
+ return Err(unsupported_path());
+ };
+
+ if purpose != &zcash_vendor::zip32::ChildIndex::hardened(32)
+ || path_coin_type != &zcash_vendor::zip32::ChildIndex::hardened(coin_type)
+ {
+ return Err(unsupported_path());
+ }
+
+ let account_index = account_index
+ .index()
+ .checked_sub(1 << 31)
+ .ok_or_else(unsupported_path)?;
+ zcash_vendor::zip32::AccountId::try_from(account_index)
+ .map(Some)
+ .map_err(|_| unsupported_path())
+}
+
+#[cfg(all(
+ zcash_unstable = "nu6.3",
+ any(feature = "multi_coins", not(feature = "cypherpunk"))
+))]
+pub(crate) fn pczt_requires_cypherpunk_support(pczt: &zcash_vendor::pczt::Pczt) -> bool {
+ *pczt.global().tx_version() >= 6 || !pczt.ironwood().actions().is_empty()
+}
+
+#[cfg(all(test, feature = "cypherpunk"))]
+pub(crate) mod test_support {
+ use alloc::{string::String, vec, vec::Vec};
+
+ use ::pczt::roles::{creator::Creator, updater::Updater};
+ #[cfg(zcash_unstable = "nu6.3")]
+ use incrementalmerkletree::Retention;
+ use keystore::algorithms::zcash::{calculate_seed_fingerprint, derive_ufvk};
+ use rand_core::OsRng;
+ #[cfg(zcash_unstable = "nu6.3")]
+ use shardtree::{store::memory::MemoryShardStore, ShardTree};
+ #[cfg(zcash_unstable = "nu6.3")]
+ use zcash_note_encryption::try_note_decryption;
+ use zcash_primitives::transaction::{
+ builder::{BuildConfig, Builder, PcztParts, PcztResult},
+ fees::zip317,
+ TxVersion,
+ };
+ #[cfg(zcash_unstable = "nu6.3")]
+ use zcash_vendor::zcash_protocol::consensus::{BlockHeight, NetworkType, NetworkUpgrade};
+ use zcash_vendor::{
+ orchard,
+ pczt::Pczt,
+ zcash_keys::keys::UnifiedFullViewingKey,
+ zcash_protocol::{
+ consensus::{BranchId, MainNetwork, Parameters},
+ memo::{Memo, MemoBytes},
+ value::Zatoshis,
+ },
+ zip32,
+ };
+
+ pub(crate) struct SamplePczt {
+ pub(crate) bytes: Vec<u8>,
+ pub(crate) seed: Vec<u8>,
+ pub(crate) ufvk_text: String,
+ pub(crate) seed_fingerprint: [u8; 32],
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[derive(Clone, Copy, Debug)]
+ pub(crate) struct Nu6_3Network;
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ impl Parameters for Nu6_3Network {
+ fn network_type(&self) -> NetworkType {
+ NetworkType::Main
+ }
+
+ fn activation_height(&self, nu: NetworkUpgrade) -> Option<BlockHeight> {
+ match nu {
+ NetworkUpgrade::Nu6_3 => Some(BlockHeight::from_u32(10)),
+ _ => MainNetwork.activation_height(nu),
+ }
+ }
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ pub(crate) fn unsupported_orchard_spend_paths() -> Vec<Vec<u32>> {
+ vec![
+ vec![
+ zip32::ChildIndex::hardened(32).index(),
+ zip32::ChildIndex::hardened(1).index(),
+ zip32::ChildIndex::hardened(0).index(),
+ ],
+ vec![
+ zip32::ChildIndex::hardened(32).index(),
+ zip32::ChildIndex::hardened(133).index(),
+ zip32::ChildIndex::hardened(0).index(),
+ zip32::ChildIndex::hardened(0).index(),
+ ],
+ ]
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ pub(crate) fn orchard_spend_path_for_account(account_index: u32) -> Vec<u32> {
+ vec![
+ zip32::ChildIndex::hardened(32).index(),
+ zip32::ChildIndex::hardened(133).index(),
+ zip32::ChildIndex::hardened(account_index).index(),
+ ]
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ pub(crate) fn ironwood_pczt_with_spend_derivation(
+ bytes: &[u8],
+ seed_fingerprint: [u8; 32],
+ path: Vec<u32>,
+ ) -> Vec<u8> {
+ Updater::new(Pczt::parse(bytes).unwrap())
+ .update_ironwood_with(|mut bundle| {
+ for action_index in 0..bundle.bundle().actions().len() {
+ let derivation =
+ orchard::pczt::Zip32Derivation::parse(seed_fingerprint, path.clone())
+ .unwrap();
+ bundle.update_action_with(action_index, |mut action| {
+ action.set_spend_zip32_derivation(derivation);
+ Ok(())
+ })?;
+ }
+ Ok(())
+ })
+ .unwrap()
+ .finish()
+ .serialize()
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ pub(crate) fn ironwood_pczt_with_dummy_spend_derivation(
+ bytes: &[u8],
+ seed_fingerprint: [u8; 32],
+ path: Vec<u32>,
+ ) -> Vec<u8> {
+ Updater::new(Pczt::parse(bytes).unwrap())
+ .update_ironwood_with(|mut bundle| {
+ let dummy_action_indices = bundle
+ .bundle()
+ .actions()
+ .iter()
+ .enumerate()
+ .filter_map(|(index, action)| {
+ matches!(action.spend().value().map(|value| value.inner()), Some(0))
+ .then_some(index)
+ })
+ .collect::<Vec<_>>();
+ assert!(!dummy_action_indices.is_empty());
+
+ for action_index in dummy_action_indices {
+ let derivation =
+ orchard::pczt::Zip32Derivation::parse(seed_fingerprint, path.clone())
+ .unwrap();
+ bundle.update_action_with(action_index, |mut action| {
+ action.set_spend_zip32_derivation(derivation);
+ Ok(())
+ })?;
+ }
+ Ok(())
+ })
+ .unwrap()
+ .finish()
+ .serialize()
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ pub(crate) fn sample_ironwood_pczt() -> SamplePczt {
+ let params = Nu6_3Network;
+ let seed = [7u8; 32];
+ let ufvk_text = derive_ufvk(¶ms, &seed, "m/32'/133'/0'").unwrap();
+ let ufvk = UnifiedFullViewingKey::decode(¶ms, &ufvk_text).unwrap();
+ let orchard_fvk = ufvk.orchard().unwrap().clone();
+ let orchard_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::External);
+ let orchard_ovk = orchard_fvk.to_ovk(orchard::keys::Scope::External);
+ let recipient = orchard_fvk.address_at(0u32, orchard::keys::Scope::External);
+
+ let value = orchard::value::NoteValue::from_raw(1_000_000);
+ let note = {
+ let mut orchard_builder = orchard::builder::Builder::new(
+ orchard::BundleProtocol::IronwoodPostNu6_3,
+ orchard::builder::BundleType::DEFAULT,
+ orchard::Anchor::empty_tree(),
+ );
+ orchard_builder
+ .add_output(None, recipient, value, Memo::Empty.encode().into_bytes())
+ .unwrap();
+ let (bundle, meta) = orchard_builder.build::<i64>(&mut OsRng).unwrap().unwrap();
+ let action = bundle
+ .actions()
+ .get(meta.output_action_index(0).unwrap())
+ .unwrap();
+ let domain = orchard::note_encryption::OrchardDomain::for_action(action);
+ let (note, _, _) =
+ try_note_decryption(&domain, &orchard_ivk.prepare(), action).unwrap();
+ note
+ };
+
+ let (anchor, merkle_path) = {
+ let cmx: orchard::note::ExtractedNoteCommitment = note.commitment().into();
+ let leaf = orchard::tree::MerkleHashOrchard::from_cmx(&cmx);
+ let mut tree = ShardTree::<_, 32, 16>::new(
+ MemoryShardStore::<orchard::tree::MerkleHashOrchard, u32>::empty(),
+ 100,
+ );
+ tree.append(leaf, Retention::Marked).unwrap();
+ tree.checkpoint(9_999_999).unwrap();
+ let merkle_path = tree
+ .witness_at_checkpoint_depth(0.into(), 0)
+ .unwrap()
+ .unwrap();
+ let anchor = merkle_path.root(leaf);
+ (anchor.into(), merkle_path.into())
+ };
+
+ let mut builder = Builder::new(
+ ¶ms,
+ 10_000_000.into(),
+ BuildConfig::Standard {
+ sapling_anchor: None,
+ orchard_anchor: None,
+ ironwood_anchor: Some(anchor),
+ },
+ );
+ builder
+ .add_ironwood_spend::<zip317::FeeRule>(orchard_fvk.clone(), note, merkle_path)
+ .unwrap();
+ builder
+ .add_ironwood_output::<zip317::FeeRule>(
+ Some(orchard_ovk),
+ recipient,
+ Zatoshis::const_from_u64(990_000),
+ MemoBytes::empty(),
+ )
+ .unwrap();
+ let PcztResult {
+ pczt_parts,
+ ironwood_meta,
+ ..
+ } = builder
+ .build_for_pczt(OsRng, &zip317::FeeRule::standard())
+ .unwrap();
+ let spend_action_index = ironwood_meta.spend_action_index(0).unwrap();
+ let seed_fingerprint = calculate_seed_fingerprint(&seed).unwrap();
+ let derivation = orchard::pczt::Zip32Derivation::parse(
+ seed_fingerprint,
+ vec![
+ zip32::ChildIndex::hardened(32).index(),
+ zip32::ChildIndex::hardened(133).index(),
+ zip32::ChildIndex::hardened(0).index(),
+ ],
+ )
+ .unwrap();
+ let pczt = Updater::new(Creator::build_from_parts(pczt_parts).unwrap())
+ .update_ironwood_with(|mut bundle| {
+ bundle.update_action_with(spend_action_index, |mut action| {
+ action.set_spend_zip32_derivation(derivation);
+ Ok(())
+ })
+ })
+ .unwrap()
+ .finish();
+
+ SamplePczt {
+ bytes: pczt.serialize(),
+ seed: seed.to_vec(),
+ ufvk_text,
+ seed_fingerprint,
+ }
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ pub(crate) fn sample_orchard_change_pczt() -> SamplePczt {
+ let params = MainNetwork;
+ let seed = [7u8; 32];
+ let ufvk_text = derive_ufvk(¶ms, &seed, "m/32'/133'/0'").unwrap();
+ let ufvk = UnifiedFullViewingKey::decode(¶ms, &ufvk_text).unwrap();
+ let orchard_fvk = ufvk.orchard().unwrap().clone();
+ let orchard_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::External);
+ let recipient_scope = orchard::keys::Scope::External;
+ let recipient = orchard_fvk.address_at(0u32, recipient_scope);
+ let orchard_ovk = orchard_fvk.to_ovk(recipient_scope);
+
+ let value = orchard::value::NoteValue::from_raw(1_000_000);
+ let note = {
+ let mut orchard_builder = orchard::builder::Builder::new(
+ orchard::BundleProtocol::OrchardPostNu6_3,
+ orchard::builder::BundleType::Coinbase,
+ orchard::Anchor::empty_tree(),
+ );
+ orchard_builder
+ .add_output(None, recipient, value, Memo::Empty.encode().into_bytes())
+ .unwrap();
+ let (bundle, meta) = orchard_builder.build::<i64>(&mut OsRng).unwrap().unwrap();
+ let action = bundle
+ .actions()
+ .get(meta.output_action_index(0).unwrap())
+ .unwrap();
+ let domain = orchard::note_encryption::OrchardDomain::for_action(action);
+ let (note, _, _) =
+ try_note_decryption(&domain, &orchard_ivk.prepare(), action).unwrap();
+ note
+ };
+
+ let (anchor, merkle_path) = {
+ let cmx: orchard::note::ExtractedNoteCommitment = note.commitment().into();
+ let leaf = orchard::tree::MerkleHashOrchard::from_cmx(&cmx);
+ let mut tree = ShardTree::<_, 32, 16>::new(
+ MemoryShardStore::<orchard::tree::MerkleHashOrchard, u32>::empty(),
+ 100,
+ );
+ tree.append(leaf, Retention::Marked).unwrap();
+ tree.checkpoint(9_999_999).unwrap();
+ let merkle_path = tree
+ .witness_at_checkpoint_depth(0.into(), 0)
+ .unwrap()
+ .unwrap();
+ let anchor = merkle_path.root(leaf);
+ (anchor.into(), merkle_path.into())
+ };
+
+ let mut builder = orchard::builder::Builder::new(
+ orchard::BundleProtocol::OrchardPostNu6_3,
+ orchard::builder::BundleType::DEFAULT,
+ anchor,
+ );
+ builder
+ .add_spend(orchard_fvk.clone(), note, merkle_path)
+ .unwrap();
+ builder
+ .add_change_output(
+ orchard_fvk,
+ Some(orchard_ovk),
+ recipient,
+ orchard::value::NoteValue::from_raw(990_000),
+ Memo::Empty.encode().into_bytes(),
+ )
+ .unwrap();
+ let (orchard_bundle, _) = builder.build_for_pczt(&mut OsRng).unwrap();
+ let seed_fingerprint = calculate_seed_fingerprint(&seed).unwrap();
+ let pczt = Creator::build_from_parts(PcztParts {
+ params,
+ version: TxVersion::V6,
+ consensus_branch_id: BranchId::Nu6_3,
+ lock_time: 0,
+ expiry_height: BlockHeight::from_u32(10_000_000),
+ transparent: None,
+ sapling: None,
+ orchard: Some(orchard_bundle),
+ ironwood: None,
+ })
+ .unwrap();
+ let pczt = Updater::new(pczt)
+ .update_orchard_with(|mut bundle| {
+ let signing_action_indices = bundle
+ .bundle()
+ .actions()
+ .iter()
+ .enumerate()
+ .filter_map(|(index, action)| {
+ action.spend().dummy_sk().is_none().then_some(index)
+ })
+ .collect::<Vec<_>>();
+ assert_eq!(signing_action_indices.len(), 2);
+
+ for action_index in signing_action_indices {
+ let derivation = orchard::pczt::Zip32Derivation::parse(
+ seed_fingerprint,
+ orchard_spend_path_for_account(0),
+ )
+ .unwrap();
+ bundle.update_action_with(action_index, |mut action| {
+ action.set_spend_zip32_derivation(derivation);
+ Ok(())
+ })?;
+ }
+ Ok(())
+ })
+ .unwrap()
+ .finish();
+
+ SamplePczt {
+ bytes: pczt.serialize(),
+ seed: seed.to_vec(),
+ ufvk_text,
+ seed_fingerprint,
+ }
+ }
+}
+
+#[cfg(all(test, feature = "multi_coins", not(feature = "cypherpunk")))]
+pub(crate) mod legacy_test_support {
+ use alloc::{
+ string::{String, ToString},
+ vec,
+ vec::Vec,
+ };
+
+ use ::pczt::roles::{creator::Creator, updater::Updater};
+ use bitcoin::secp256k1::Secp256k1;
+ use keystore::algorithms::{
+ secp256k1::get_extended_public_key_by_seed, zcash::calculate_seed_fingerprint,
+ };
+ use rand_core::OsRng;
+ use zcash_primitives::transaction::{
+ builder::{BuildConfig, Builder, PcztResult},
+ fees::zip317,
+ };
+ use zcash_vendor::{
+ pczt::Pczt,
+ transparent::{
+ bundle as transparent,
+ keys::{AccountPrivKey, IncomingViewingKey},
+ },
+ zcash_protocol::{
+ consensus::{MainNetwork, Parameters},
+ value::Zatoshis,
+ },
+ zip32,
+ };
+
+ pub(crate) struct LegacyTransparentSample {
+ pub(crate) bytes: Vec<u8>,
+ pub(crate) seed: Vec<u8>,
+ pub(crate) seed_fingerprint: [u8; 32],
+ pub(crate) xpub: String,
+ pub(crate) input_pubkey: [u8; 33],
+ }
+
+ pub(crate) fn legacy_transparent_path_for_account(account_index: u32) -> Vec<u32> {
+ vec![
+ 44 | zcash_vendor::bip32::ChildNumber::HARDENED_FLAG,
+ 133 | zcash_vendor::bip32::ChildNumber::HARDENED_FLAG,
+ account_index | zcash_vendor::bip32::ChildNumber::HARDENED_FLAG,
+ 0,
+ 0,
+ ]
+ }
+
+ pub(crate) fn legacy_transparent_pczt_with_input_derivation(
+ bytes: &[u8],
+ seed_fingerprint: [u8; 32],
+ input_pubkey: [u8; 33],
+ path: Vec<u32>,
+ ) -> Vec<u8> {
+ let derivation =
+ zcash_vendor::transparent::pczt::Bip32Derivation::parse(seed_fingerprint, path)
+ .unwrap();
+ Updater::new(Pczt::parse(bytes).unwrap())
+ .update_transparent_with(|mut bundle| {
+ bundle.update_input_with(0, |mut input| {
+ input.set_bip32_derivation(input_pubkey, derivation);
+ Ok(())
+ })
+ })
+ .unwrap()
+ .finish()
+ .serialize()
+ }
+
+ pub(crate) fn legacy_transparent_sample() -> LegacyTransparentSample {
+ let params = MainNetwork;
+ let seed = [7u8; 32];
+ let account = AccountPrivKey::from_seed(¶ms, &seed, zip32::AccountId::ZERO).unwrap();
+ let (input_addr, address_index) = account
+ .to_account_pubkey()
+ .derive_external_ivk()
+ .unwrap()
+ .default_address();
+ let input_sk = account.derive_external_secret_key(address_index).unwrap();
+ let secp = Secp256k1::signing_only();
+ let input_pubkey = input_sk.public_key(&secp);
+
+ let recipient_account =
+ AccountPrivKey::from_seed(¶ms, &[8u8; 32], zip32::AccountId::ZERO).unwrap();
+ let (recipient, _) = recipient_account
+ .to_account_pubkey()
+ .derive_external_ivk()
+ .unwrap()
+ .default_address();
+ let transparent_recipient = recipient
+ .to_zcash_address(MainNetwork.network_type())
+ .encode();
+
+ let coin = transparent::TxOut::new(
+ Zatoshis::const_from_u64(1_000_000),
+ input_addr.script().into(),
+ );
+ let mut builder = Builder::new(
+ ¶ms,
+ 10_000_000.into(),
+ BuildConfig::Standard {
+ sapling_anchor: None,
+ orchard_anchor: None,
+ ironwood_anchor: None,
+ },
+ );
+ builder
+ .add_transparent_p2pkh_input(
+ input_pubkey,
+ transparent::OutPoint::new([1u8; 32], 1),
+ coin,
+ )
+ .unwrap();
+ builder
+ .add_transparent_output(&recipient, Zatoshis::const_from_u64(990_000))
+ .unwrap();
+
+ let PcztResult { pczt_parts, .. } = builder
+ .build_for_pczt(OsRng, &zip317::FeeRule::standard())
+ .unwrap();
+ let seed_fingerprint = calculate_seed_fingerprint(&seed).unwrap();
+ let input_pubkey = input_pubkey.serialize();
+ let pczt = Updater::new(Creator::build_from_parts(pczt_parts).unwrap())
+ .update_transparent_with(|mut bundle| {
+ let derivation = zcash_vendor::transparent::pczt::Bip32Derivation::parse(
+ seed_fingerprint,
+ legacy_transparent_path_for_account(0),
+ )
+ .unwrap();
+ bundle.update_input_with(0, |mut input| {
+ input.set_bip32_derivation(input_pubkey, derivation);
+ Ok(())
+ })?;
+ bundle.update_output_with(0, |mut output| {
+ output.set_user_address(transparent_recipient.clone());
+ Ok(())
+ })
+ })
+ .unwrap()
+ .finish();
+
+ let xpub = get_extended_public_key_by_seed(&seed, &"M/44'/133'/0'".into())
+ .unwrap()
+ .to_string();
+
+ LegacyTransparentSample {
+ bytes: pczt.serialize(),
+ seed: seed.to_vec(),
+ seed_fingerprint,
+ xpub,
+ input_pubkey,
+ }
+ }
+}
diff --git a/rust/apps/zcash/src/pczt/parse.rs b/rust/apps/zcash/src/pczt/parse.rs
index cb7fed5..567b58e 100644
--- a/rust/apps/zcash/src/pczt/parse.rs
+++ b/rust/apps/zcash/src/pczt/parse.rs
@@ -3,17 +3,14 @@ use alloc::{
string::{String, ToString},
vec,
};
+#[cfg(feature = "cypherpunk")]
use zcash_note_encryption::{try_output_recovery_with_ovk, try_output_recovery_with_pkd_esk};
use zcash_vendor::{
pczt::{self, roles::verifier::Verifier, Pczt},
ripemd::{Digest, Ripemd160},
sha2::Sha256,
transparent::{self, address::TransparentAddress},
- zcash_address::{
- unified::{self, Encoding, Receiver},
- ToAddress, ZcashAddress,
- },
- zcash_keys::keys::UnifiedFullViewingKey,
+ zcash_address::{ToAddress, ZcashAddress},
zcash_protocol::{
consensus::{self},
value::ZatBalance,
@@ -26,12 +23,57 @@ use zcash_note_encryption::Domain;
use zcash_vendor::orchard::{
self, keys::OutgoingViewingKey, note::Note, note_encryption::OrchardDomain, Address,
};
+#[cfg(feature = "cypherpunk")]
+use zcash_vendor::{
+ zcash_address::{
+ unified::{self, Encoding, Receiver},
+ ConversionError, TryFromAddress,
+ },
+ zcash_keys::keys::UnifiedFullViewingKey,
+};
+#[cfg(feature = "cypherpunk")]
+use super::structs::ParsedOrchard;
+use super::structs::{ParsedFrom, ParsedPczt, ParsedTo, ParsedTransparent};
use crate::errors::ZcashError;
-use super::structs::{ParsedFrom, ParsedOrchard, ParsedPczt, ParsedTo, ParsedTransparent};
const ZEC_DIVIDER: u32 = 100_000_000;
+#[cfg(feature = "cypherpunk")]
+struct NetworkCheckedUnifiedAddress;
+
+#[cfg(feature = "cypherpunk")]
+impl TryFromAddress for NetworkCheckedUnifiedAddress {
+ type Error = core::convert::Infallible;
+
+ fn try_from_unified(
+ net: consensus::NetworkType,
+ data: unified::Address,
+ ) -> Result<Self, ConversionError<Self::Error>> {
+ let _ = (net, data);
+ Ok(Self)
+ }
+}
+
+#[cfg(feature = "cypherpunk")]
+fn map_orchard_verifier_error(
+ error: pczt::roles::verifier::OrchardError<ZcashError>,
+) -> ZcashError {
+ match error {
+ pczt::roles::verifier::OrchardError::Custom(error) => error,
+ error => ZcashError::InvalidDataError(format!("{error:?}")),
+ }
+}
+
+fn map_transparent_verifier_error(
+ error: pczt::roles::verifier::TransparentError<ZcashError>,
+) -> ZcashError {
+ match error {
+ pczt::roles::verifier::TransparentError::Custom(error) => error,
+ error => ZcashError::InvalidDataError(format!("{error:?}")),
+ }
+}
+
fn format_zec_value(value: f64) -> String {
let zec_value = format!("{:.8}", value / ZEC_DIVIDER as f64);
let zec_value = zec_value
@@ -82,11 +124,15 @@ pub fn decode_output_enc_ciphertext(
ZcashError::InvalidPczt("Missing rseed field for Orchard action".into())
})?;
- let note = orchard::Note::from_parts(recipient, value, rho, rseed)
- .into_option()
- .ok_or_else(|| {
- ZcashError::InvalidPczt("Orchard action contains invalid note".into())
- })?;
+ let note = orchard::Note::from_parts(
+ recipient,
+ value,
+ rho,
+ rseed,
+ (*action.output().note_version()).into(),
+ )
+ .into_option()
+ .ok_or_else(|| ZcashError::InvalidPczt("Orchard action contains invalid note".into()))?;
let pk_d = OrchardDomain::get_pk_d(¬e);
let esk = OrchardDomain::derive_esk(¬e).expect("Orchard notes are post-ZIP 212");
@@ -98,7 +144,7 @@ pub fn decode_output_enc_ciphertext(
/// Parses a PCZT (Partially Created Zcash Transaction) into a structured format
///
/// This function analyzes the transaction and extracts information about inputs, outputs,
-/// values, and fees across different Zcash pools (transparent and Orchard).
+/// values, and fees across supported Zcash pools.
///
/// # Parameters
/// * `params` - Network consensus parameters
@@ -123,22 +169,40 @@ pub fn parse_pczt_cypherpunk<P: consensus::Parameters>(
ufvk: &UnifiedFullViewingKey,
pczt: &Pczt,
) -> Result<ParsedPczt, ZcashError> {
+ super::validate_supported_pczt(pczt)?;
let mut parsed_orchard = None;
+ #[cfg(zcash_unstable = "nu6.3")]
+ let mut parsed_ironwood = None;
+ #[cfg(zcash_unstable = "nu6.3")]
+ let should_process_ironwood = super::pczt_should_process_ironwood(pczt);
let mut parsed_transparent = None;
- Verifier::new(pczt.clone())
+ let verifier = Verifier::new(pczt.clone())
.with_orchard(|bundle| {
- parsed_orchard = parse_orchard(params, seed_fingerprint, ufvk, bundle)
+ parsed_orchard = parse_orchard(params, seed_fingerprint, ufvk, bundle, "Orchard")
.map_err(pczt::roles::verifier::OrchardError::Custom)?;
Ok(())
})
- .map_err(|e| ZcashError::InvalidDataError(alloc::format!("{e:?}")))?
+ .map_err(map_orchard_verifier_error)?;
+ #[cfg(zcash_unstable = "nu6.3")]
+ let verifier = if should_process_ironwood {
+ verifier
+ .with_ironwood(|bundle| {
+ parsed_ironwood = parse_orchard(params, seed_fingerprint, ufvk, bundle, "Ironwood")
+ .map_err(pczt::roles::verifier::OrchardError::Custom)?;
+ Ok(())
+ })
+ .map_err(map_orchard_verifier_error)?
+ } else {
+ verifier
+ };
+ verifier
.with_transparent(|bundle| {
parsed_transparent = parse_transparent(params, seed_fingerprint, bundle)
.map_err(pczt::roles::verifier::TransparentError::Custom)?;
Ok(())
})
- .map_err(|e| ZcashError::InvalidDataError(alloc::format!("{e:?}")))?;
+ .map_err(map_transparent_verifier_error)?;
let mut total_input_value = 0;
let mut total_output_value = 0;
@@ -161,6 +225,22 @@ pub fn parse_pczt_cypherpunk<P: consensus::Parameters>(
.iter()
.fold(0, |acc, to| acc + to.get_amount());
}
+ #[cfg(zcash_unstable = "nu6.3")]
+ if let Some(ironwood) = &parsed_ironwood {
+ total_change_value += ironwood
+ .get_to()
+ .iter()
+ .filter(|v| v.get_is_change())
+ .fold(0, |acc, to| acc + to.get_amount());
+ total_input_value += ironwood
+ .get_from()
+ .iter()
+ .fold(0, |acc, from| acc + from.get_amount());
+ total_output_value += ironwood
+ .get_to()
+ .iter()
+ .fold(0, |acc, to| acc + to.get_amount());
+ }
if let Some(transparent) = &parsed_transparent {
total_change_value += transparent
@@ -203,9 +283,15 @@ pub fn parse_pczt_cypherpunk<P: consensus::Parameters>(
let has_sapling = !pczt.sapling().spends().is_empty() || !pczt.sapling().outputs().is_empty();
+ #[cfg(zcash_unstable = "nu6.3")]
+ let parsed_ironwood = parsed_ironwood;
+ #[cfg(not(zcash_unstable = "nu6.3"))]
+ let parsed_ironwood = None;
+
Ok(ParsedPczt::new(
parsed_transparent,
parsed_orchard,
+ parsed_ironwood,
total_transfer_value,
fee_value,
has_sapling,
@@ -217,6 +303,9 @@ pub fn parse_pczt_multi_coins<P: consensus::Parameters>(
seed_fingerprint: &[u8; 32],
pczt: &Pczt,
) -> Result<ParsedPczt, ZcashError> {
+ super::validate_supported_pczt(pczt)?;
+ reject_legacy_parse_unsupported_pczt(pczt)?;
+
let mut parsed_transparent = None;
Verifier::new(pczt.clone())
@@ -225,7 +314,7 @@ pub fn parse_pczt_multi_coins<P: consensus::Parameters>(
.map_err(pczt::roles::verifier::TransparentError::Custom)?;
Ok(())
})
- .map_err(|e| ZcashError::InvalidDataError(alloc::format!("{e:?}")))?;
+ .map_err(map_transparent_verifier_error)?;
let mut total_input_value = 0;
let mut total_output_value = 0;
@@ -277,12 +366,28 @@ pub fn parse_pczt_multi_coins<P: consensus::Parameters>(
Ok(ParsedPczt::new(
parsed_transparent,
None,
+ None,
total_transfer_value,
fee_value,
has_sapling,
))
}
+#[cfg(feature = "multi_coins")]
+fn reject_legacy_parse_unsupported_pczt(pczt: &Pczt) -> Result<(), ZcashError> {
+ #[cfg(zcash_unstable = "nu6.3")]
+ {
+ // The legacy multi-coins parser only displays transparent data. Reject
+ // V6/Ironwood PCZTs instead of showing an incomplete transaction review.
+ if super::pczt_requires_cypherpunk_support(pczt) {
+ return Err(ZcashError::InvalidPczt(
+ "V6 or Ironwood PCZTs require cypherpunk parsing support".to_string(),
+ ));
+ }
+ }
+ Ok(())
+}
+
fn parse_transparent<P: consensus::Parameters>(
params: &P,
seed_fingerprint: &[u8; 32],
@@ -295,7 +400,7 @@ fn parse_transparent<P: consensus::Parameters>(
Ok::<_, ZcashError>(())
})?;
transparent.outputs().iter().try_for_each(|output| {
- let parsed_to = parse_transparent_output(seed_fingerprint, output)?;
+ let parsed_to = parse_transparent_output(params, seed_fingerprint, output)?;
parsed_transparent.add_to(parsed_to);
Ok::<_, ZcashError>(())
})?;
@@ -313,7 +418,7 @@ fn parse_transparent_input<P: consensus::Parameters>(
) -> Result<ParsedFrom, ZcashError> {
let script = input.script_pubkey().clone();
//P2SH address is not supported by Zashi yet, we only consider P2PKH address at the moment.
- match script.address() {
+ match TransparentAddress::from_script_from_chain(&script) {
Some(TransparentAddress::PublicKeyHash(hash)) => {
//find the pubkey in the derivation path
let pubkey = input
@@ -326,7 +431,6 @@ fn parse_transparent_input<P: consensus::Parameters>(
let is_mine = match pubkey {
Some(pubkey) => match input.bip32_derivation().get(pubkey) {
- //pubkey validation is checked on transaction checking part
Some(bip32_derivation) => {
seed_fingerprint == bip32_derivation.seed_fingerprint()
}
@@ -347,12 +451,13 @@ fn parse_transparent_input<P: consensus::Parameters>(
}
}
-fn parse_transparent_output(
+fn parse_transparent_output<P: consensus::Parameters>(
+ _params: &P,
seed_fingerprint: &[u8; 32],
output: &transparent::pczt::Output,
) -> Result<ParsedTo, ZcashError> {
let script = output.script_pubkey().clone();
- match script.address() {
+ match TransparentAddress::from_script_pubkey(&script) {
Some(TransparentAddress::PublicKeyHash(hash)) => {
let pubkey = output
.bip32_derivation()
@@ -409,6 +514,7 @@ fn parse_orchard<P: consensus::Parameters>(
seed_fingerprint: &[u8; 32],
ufvk: &UnifiedFullViewingKey,
orchard: &orchard::pczt::Bundle,
+ pool_label: &str,
) -> Result<Option<ParsedOrchard>, ZcashError> {
let mut parsed_orchard = ParsedOrchard::new(vec![], vec![]);
orchard.actions().iter().try_for_each(|action| {
@@ -421,7 +527,7 @@ fn parse_orchard<P: consensus::Parameters>(
parsed_orchard.add_from(parsed_from);
}
}
- let parsed_to = parse_orchard_output(params, ufvk, action)?;
+ let parsed_to = parse_orchard_output(params, ufvk, action, pool_label)?;
if !parsed_to.get_is_dummy() {
parsed_orchard.add_to(parsed_to);
}
@@ -485,11 +591,32 @@ fn is_internal_orchard_address(
Ok(internal_ivk.diversifier_index(address).is_some())
}
+#[cfg(feature = "cypherpunk")]
+pub(crate) fn validate_orchard_user_address<P: consensus::Parameters>(
+ params: &P,
+ user_address: &str,
+ address: &Address,
+) -> Result<(), ZcashError> {
+ let za = ZcashAddress::try_from_encoded(user_address)
+ .map_err(|e| ZcashError::InvalidPczt(format!("user address is invalid: {e:?}")))?;
+ za.clone()
+ .convert_if_network::<NetworkCheckedUnifiedAddress>(params.network_type())
+ .map_err(|e| ZcashError::InvalidPczt(format!("user address network mismatch: {e:?}")))?;
+ let receiver = Receiver::Orchard(address.to_raw_address_bytes());
+ if !za.matches_receiver(&receiver) {
+ return Err(ZcashError::InvalidPczt(
+ "user address is not match with address in decoded note".to_string(),
+ ));
+ }
+ Ok(())
+}
+
#[cfg(feature = "cypherpunk")]
fn parse_orchard_output<P: consensus::Parameters>(
params: &P,
ufvk: &UnifiedFullViewingKey,
action: &orchard::pczt::Action,
+ pool_label: &str,
) -> Result<ParsedTo, ZcashError> {
let output = action.output();
let fvk = ufvk.orchard().ok_or(ZcashError::InvalidDataError(
@@ -509,10 +636,8 @@ fn parse_orchard_output<P: consensus::Parameters>(
.ok_or(ZcashError::InvalidPczt("value is not present".to_string()))?
.inner();
- let decode_output = |vk: Option<OutgoingViewingKey>, is_internal_ovk: bool| match decode_output_enc_ciphertext(
- action,
- vk.as_ref(),
- )? {
+ let decode_output = |vk: Option<OutgoingViewingKey>, is_internal_ovk: bool| {
+ match decode_output_enc_ciphertext(action, vk.as_ref())? {
Some((note, address, memo)) => {
let zec_value = format_zec_value(note.value().inner() as f64);
let memo = decode_memo(memo);
@@ -534,21 +659,15 @@ fn parse_orchard_output<P: consensus::Parameters>(
.encode(¶ms.network_type());
let user_address = action.output().user_address();
if let Some(user_address) = user_address {
- let za = ZcashAddress::try_from_encoded(user_address).unwrap();
- let receiver = Receiver::Orchard(address.to_raw_address_bytes());
- if !za.matches_receiver(&receiver) {
- return Err(ZcashError::InvalidPczt(
- "user address is not match with address in decoded note".to_string(),
- ));
- }
+ validate_orchard_user_address(params, user_address, &address)?;
}
let belongs_to_wallet = is_wallet_orchard_address(ufvk, &address)?;
let is_internal = is_internal_orchard_address(ufvk, &address)?;
if is_internal_ovk && !belongs_to_wallet {
- return Err(ZcashError::InvalidPczt(
- "Orchard output was recoverable with an internal OVK but does not belong to this wallet".into(),
- ));
+ return Err(ZcashError::InvalidPczt(alloc::format!(
+ "{pool_label} output was recoverable with an internal OVK but does not belong to this wallet"
+ )));
}
let is_dummy = match vk {
Some(_) => false,
@@ -576,7 +695,7 @@ fn parse_orchard_output<P: consensus::Parameters>(
// `vk.is_none()` as a fallback. We require that non-trivial outputs are
// visible to the Keystone device.
(None, Some(value)) if value.inner() != 0 => Err(ZcashError::InvalidPczt(
- "enc_ciphertext field for Orchard action is undecryptable".into(),
+ alloc::format!("enc_ciphertext field for {pool_label} action is undecryptable"),
)),
// We couldn't directly decrypt a zero-valued note. This is okay because
// it is checked elsewhere that the direct details in the PCZT are valid,
@@ -585,7 +704,8 @@ fn parse_orchard_output<P: consensus::Parameters>(
// contains no in-band data (as is the case for e.g. dummy outputs).
(None, _) => Ok(None),
},
- };
+ }
+ };
let mut keys = vec![(Some(external_ovk), false), (Some(internal_ovk), true)];
@@ -619,11 +739,16 @@ fn parse_orchard_output<P: consensus::Parameters>(
match parsed_to {
None => {
let (address, is_dummy) = match (output.user_address(), value) {
- (Some(addr), _) => Ok((addr.clone(), false)),
+ (Some(addr), _) => {
+ if let Some(recipient) = output.recipient() {
+ validate_orchard_user_address(params, addr, recipient)?;
+ }
+ Ok((addr.clone(), false))
+ }
(None, 0) => Ok(("Dummy output".into(), true)),
- (None, _) => Err(ZcashError::InvalidPczt(
- "missing user address for Orchard output".into(),
- )),
+ (None, _) => Err(ZcashError::InvalidPczt(alloc::format!(
+ "missing user address for {pool_label} output"
+ ))),
}?;
Ok(ParsedTo::new(
address,
@@ -638,6 +763,38 @@ fn parse_orchard_output<P: consensus::Parameters>(
}
}
+#[cfg(all(test, feature = "multi_coins", not(feature = "cypherpunk")))]
+mod legacy_tests {
+ use super::*;
+ use zcash_vendor::{
+ pczt::roles::creator::Creator,
+ zcash_protocol::consensus::{BranchId, MainNetwork, NetworkConstants},
+ };
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn legacy_parse_rejects_v6_pczt() {
+ let pczt = Creator::new_v6(
+ BranchId::Nu6_3.into(),
+ 10,
+ MainNetwork.coin_type(),
+ [0; 32],
+ [0; 32],
+ [1; 32],
+ )
+ .build();
+
+ let result = parse_pczt_multi_coins(&MainNetwork, &[7u8; 32], &pczt);
+
+ assert!(matches!(
+ result,
+ Err(ZcashError::InvalidPczt(msg))
+ if msg == "V6 or Ironwood PCZTs require cypherpunk parsing support"
+ ));
+ }
+}
+
+#[cfg(feature = "cypherpunk")]
fn decode_memo(memo_bytes: [u8; 512]) -> Option<String> {
let first = memo_bytes[0];
@@ -677,8 +834,8 @@ fn decode_memo(memo_bytes: [u8; 512]) -> Option<String> {
#[cfg(feature = "cypherpunk")]
#[cfg(test)]
mod tests {
- use alloc::collections::BTreeMap;
use super::*;
+ use alloc::collections::BTreeMap;
use zcash_vendor::{
transparent::pczt,
zcash_address::ZcashAddress,
@@ -740,71 +897,17 @@ mod tests {
}
#[test]
- fn test_decode_pczt_to_p2pkh_transparent_output() {
- let hex_str = "50435a5401000000058ace9cb502d5a09cc70c0100d989a80185010001227a636173685f636c69656e745f6261636b656e643a70726f706f73616c5f696e666f144951eeff9ccf4eb390ff94a60aa5673db189a8010001a08d061976a9149517c77b7fcc08e66122dccb6ee6713eb7712d2b88ac00000123743158547742385031783459697042744c6850575331334a50445135524d6b4d41364d01207a636173685f636c69656e745f6261636b656e643a6f75747075745f696e666f0100000000fbc2f4300c01f0b7820d00e3347c8da4ee614674376cbc45359daa54f9b5493e010000000000000000000000000000000000000000000000000000000000000000023ad18a78e48f81fe95b3569486ee1db9eed90a319fac6faea1eed4e35b936717236c4a092e80c35e67e0b51b7a41de4013eaffed855b138934b9dcf28ad51f1ea51f60210a8d7f4f6ffb848bbafd4cdfd09df400e53c595861cc0dd8afc32dae010fba942a1bc32f2cc78408be742cd13846bbd50f3869c09d59bffc4e758949b56f422def9eb9d491721b9fc198edf183e9c32920eb30c476f236009a16355d1401b169abb973b87b0fd009693c349a5fdccaa2f1a266ea99f4e7f1fa13d2a8ec96023d73d0573b6fcdbe1b95010001986da568298f3de75fc0bf29de32aec6f2dada30c4e20286eb23667048d21d29017dd8c0825f1f2bc7aac48163e06f44024a657495e7c75fd1d2192336eef159640181679891b0cc1eaf42df83756d69c6833d552e0faa6d3c6af1405f2fcd4fbe0c360dff7f03b6da416da80b533cfb442be30185c9a7f4cba15f7fc836ccbd5d3e88daccb4be13ee8b9f850e542106ecb8107538ea23f4e21882e0c3fa46c0a228018daff38304e9759e20f4bb6f424a1040b554548feaf87e285a0a2ac9de930047e4bdc7042e08965e83fee3c20b18e3a7119457853e42b704f3df3449da1c365804abe0dd16ea51dbe4502e425ef8bb0103a9c314b041a9bf531facdc6ccab8404459261003b77426a46dff084bc2ff8f9a3da8c250bdd0ba8419d086b88fb25285d4fe8d047041cd2be273e8ccfabe154a818f491ea3fb9b9d854f4935480aa253fe35882ddfcc90b2b71eaa44f5299a34a946993fef33bc55c87a5f3427ac646f42eabe190c6856f4c8c1d1f505262b85353de115b061e2a1df24e69ec911e8f1c78b5c0716b8f01d16c560e3b81a07b3030c3beb15613b23e925c9456a9bfee1a25ef51f1ce9bf853c1264f6dd6fa2ea7087a8a78051626518afc8e88a1ed226f0cec1159a4a5ec4131041b3d8cc58ba10c7f676f52805be117a5689fa1a474e3f5bce38349f10e63ba7721a019ced0b63be870cef6f9ef2d228d8a58e695e871f7c4b0e94445d5255708c50298b13c55044f8f5995a258826291eda6f1671681e8f101aad68a17330a3faf4017f87b315aa53bb57fb785953dd9a480c85dd1182fcb235df28c7d3f3d8b79fe72f3b42d63c7ae563f0a4df22de593a488ee514dbda39291a69a0fd09a17ca6786cba733fdadb0f785147676378e7fbdd2a00a45b14460f415578abf268306554e68261022eb4c1e60df1566ac1853d01b2685924329e17963014ba9cd39f6c1c7f7e169574bab623fc15066223196ce0b1ef2251e5dc1f56707910d825c1451bf98115afd2dff4800fcfefc74c237b7bfec5cc2bf20e158f09cbd482b2f17cb0dcef82660ff0f697459e9cb1fdc77a10cffa9b91d88c3791b4a47e11781e0a7e1215315900a40bad41570d70ed6e7598d48eb8956ef90afda546e823446a3cb65a80f1407ea065d196f07524561be836cb5de0b9d5ba3665d2abf29c30bcef800dbeee9e293c59e4c5cee22984e205ebd67147ed3bf8234e654e32053e16bfe2cdeb70f83e1288793284056aac734eb8593feae99c48036b3d852f228febddd3c402bc5ae92d1e1adc313a53eae14213d67afb688efe0c2cdbff9ff826ad0b6cb2d8907949fbacc71fa539a9c60e6c3e2b79ba68b4b73b5b7d6f6e8f08ab80eb53099df60f6bd0c06d9b1e701c7471b9e9453bc78fa00ecaa28a1dbc9b56d2b0750eb5d038d58257a94fc57072f676555b9120536834309db16e168d8dc3f2b8680435853f216b576e39e9ea212f3fb2804c85065d801cf77c2586a76c404023554755ee41cffc358a30d67a26f758979f7f52233a680f0fa64ecdce560dfb206f232ec18450806a8fd8eccf23ea0cbd2dbe3374ccce23449d58aef4512452a329c037f96294f7a97a99fd7721e7bf07e32940b96dbb29e91a6bf267dfecf3e7bccaa657d405d2e7fd76ff8d51203b532a54868089c81d010b3b1b123a1303ec7643dbd09c1f396bc6915f6c4a02cdeba9becd724bb1f42e000000e0ebfc0224e6821685772ac6261127fa8dc097c4ce073ae9e468dbf8edb67a0131399b289425cf7741a1e779f1a089700276df5b193e1822bbf4416c89d4bc94c404b2ca1419e8f7cd98ed4f692efa5f01cc21558245469cfb558da73c13b42074ac966cc41733ffdfcade01c68062a0e0b4a74a3ab15123d62a6e270ea03c2ce88d337034e9537d7f871e8b1363b14096fd4ceb6ba46b2f7308ee1e0824eadd073dbdad58da08aa58b87fa7328710759597fe5a70516299c4a4302888b974da65c259cf8b3b4a1b7207e4d30f4b0f97e48702a25e17d51fa7ea2889d5926d9c66a151d8c713f267f95e0e730d89dbf2140cddcdc2d3508fa7902b1c360244ec407723929891f3990577a7478d4e0e3e374e4fc59e2ee704fca7a51196170e517eb74c339b016eeb3ff49ce10c13b217bbea0f4c234e4fe2766a788cd23000fe8920f973227ef987104d4d458f7754ed14fa2c798d7081d9311e0109c27ad89f362b24818cd803a600b2f21cf36e321c89ac785268e566e850d4c328eefce8062bbad81a854e08fa9a48f110f00072523cb6a670c52891af6e893ac010d4cf540a7ef598437803488d203de92966eb8ca9594d36ccb023d229ac8c391f4b9d50a569890052f27d9271f122c10f78b4c94c256f1e67a9e9c20779d34a0898daedb972ccee455ebb0e48b39d405f74f9074b943c5b5a38612e94bc8c82ec6176b90156289a99f816f64a5cd8cdfd8a9d1de4158e144869e3c535d1928f14d76d6a806b8730b92ada3353ededeb8c2151e876c566121ea2a6cfc93e3da2b1780b827ed7e5420c716b52b7811e650befc972685d153ac68ec9bf7d8b0a09659c5b36bd8226796770ebc05a6b9057a2c82feaae40a281edec65993cd792c980a845d2fbff8189d288500875f768205053e84b399b1a6141f380b7cfc235f26d3ce316cf2afae0404be86ea803a2733c9d32478c822dfa905a4e360c5fef6e05419088fe8921392c4366f976ecfd33332c0273e6ad6770a7a993010bf78b0565a3aa4fbb531de5fbd44a6caa636b2c03f0a38250bf599389154c4a55e4599a13b719551486bf010001587617b07bb6120ac6886c2a4641b8a0b9180ebd3375cf55e1f4066464ad43450000000001e902c227e9c4f4206e1f917c596fe9f4e6d2dca81063093d7065a70ed79674052df49c10eb57f2dda99dee1c1c01e9ed7efd1c9f6f971bf62906a97830b7ad2beec2cfcb30451b6c157fd144041e0429fa1aa2f9f7d0e84e74014205f0a9bd08171a83f92b3493fe29fa0802eefab10ecef3904aa569d82f6e95fdf2dc9cdd28000114b0cac3e14c4e1a4a4fd22c98563b9cbe859c67768a39f67abd5dbe55d3d16ff58e5c45b21d8c573f54b901a8ce920701de6d3d52208c36d6a557c3ed5843cf85a03b029ec491b8ad2541db678ab25a3d0125056d422f2e1393ba7e5295422e9ff38ce8276124ae0fec4bb06e147be83a8f017d6d7535f6e3eade13042662d05eefa91d1f4757fcd0b4b844554e77366f4b36a7068e15c2f06aaa11f6bcfd2e9e36231530069f7fb99a2771050689f24f332b427fdd05d1d5f3219230a1406f0955e3ddc0ef5ce2c7f160d797f3e54ef5782001cfdcac17ee2155c2c8b84c2ddc976891132206e97f7296033a7ef7eaa12ef214c4abf71d1439d50cefd21bedf293df7e11e187c64e13dd9a7905a1e2a26d2647cc1004394bba40a982173a00559da43ccad644d32eb4c4f785264fe3ebd9b12a1d7a6c31aab9e988f717752dbb6d8f555dcf360e25f71a7b1d95ab6b54761c3774882f16455a42d4ee6692b641bd0ccf14d81b56f0503932377bf5fae7d5a9c103c10e2ccd48e8778e0b217e8255746c998108508a6e81e1f1c1992ccd65ee9bfb196e0f7257c5dd5392cc0947e0b77bd30e18165c8a3541e62949e40c88c90fdf827133cbd17ab082f151d390180ca92cf0d8c4bd08e01b2f4f50b18bd589171d96723d00a3716420df0a98200f20a5136f36e955b51946a471eb029951e42268c00c34c53b660d2c636bf39e6f67e8933002c7b64fb410cd5e07cdac9e47d9d21c150a6fac33462496e7df033277558b54ac238f073776cb533551ced42caeba694f31fd8d40ccb9fb2e104aa0357a122141052c9a741c4c2c736b6dd0363ddbf27e2922ae2800cb93abe63b70c172de70362d9830e53800398884a7a64ff68ed99e0b9d2e26bdef115ced7bd36305a54386996133c4e65759f3731637a40eba67da103f98adbe364f148b0cc2042cafc6be1166fae39090ab4b354bfb6217b964453b63f8dbd10df936f1734973e0b3bd25f4ed440566c923085903f696bc6347ec0f6f3f63aab58e63b6449583df5658a91972a20291c6311b5b3e5240aff8d7d00212278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522ca2ced953b7fb95e3ba986333da9e69cd355223c929731094b6c2174c7638d2e60040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066177097b04c2aa045a0deffcaca41c5ac92e694466578f5909e72bb78d33310f705cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d354708102dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402cdaf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234151f91982912012669f74d0cfa1030ff37b152324e5b8346b3335a0aaeb63a0a2de2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1be8ae2ad91d463bab75ee941d33cc5817b613c63cda943a4c07f600591b088a25d53fdee371cef596766823f4a518a583b1158243afe89700f0da76da46d0060f15d2444cefe7914c9a61e829c730eceb216288fee825f6b3b6298f6f6b6bd62e4c57a617a0aa10ea7a83aa6b6b0ed685b6a3d9e5b8fd14f56cdc18021b12253f3fd4915c19bd831a7920be55d969b2ac23359e2559da77de2373f06ca014ba2787d063cd07ee4944222b7762840eb94c688bec743fa8bdf7715c8fe29f104c2a014c18207b76f3808351694eae9a99f8d7786e4c3e6b0c3452a518b0375deb0829012fac20755b7bb7c99d302b782ea36f62a1b0cfe8d7d4d09a58e8ba5da26f457803a080808008858180800880808080080000c86d0beb146429ab2ddc5e2b67b68cd0fa540c8a2c1637cde3220874577fd72337afa5c4823cffe1c5c57ba90eb737f081827bbf51437a2c420afa809bb04f3cc4046b05a8223b1b1114958bc0e10ecb6ae0b383ebd22f686f57d2f905acca999ae1e85f85acc5cb5b517b4233d3db94dc05259c76e8a04ae5d84f4331348388387edd327e40ae6b542f5b92cfa0a55f01ba9ba3f0035d64311f55042c1b86a8178f3ce47592cc1cdc3d4dfcbe66b267906a2c38313651863037d5fb3aeb4fcb85cb06e489536fe35784e5a1c0bc9a8083fd43ca2aeb18881caa02e9bde0a29ebb0ed1687299d97ce49bb6545050756fda15ee31c9cd947bf9019d90db96e89e3ee3e63717c34b485530590387b8bd2f57adc2c5b2fea35209ea22b4e2cb5e2d65e1f56cd1f16e5954bfb8425826cd87b75e57262d710bd1d5c9bd3b4a2c99a89926cc32c59e16ceb64698e1bcd82ae21d02ee4cb67e814861cd22810a0adaff558df41125e37179d16adc7cd4e1296bd31f44290e8c218664074158e724aee81a5ee5fb7f16852263b6902521c90dc4380b54aaf700a1ca6bd93a22ec1fd062f14b32f6d2d6ff51e151bfda4ccd569bfb966d294be0ee61dae648877e25b0841a27d5c224d4fd949926d4dfde6d28b7d14e16ae60d2112a79da714bb454a9f6a034a191c659fcd0c20a35d85f18b8700a29c5cb9c386f2afb10e8fafa892c3a1c5fbfee08cd58610339b7222f5945e775cfbe87089f48081b38775541cadeebbd5b51ee981b9558a0d4e01a0fba29d0b50fa9b843db2dbcc25071352041a199d7a85d5bd956d7f61db4a95cc26b1709fa48c0eba34676ee7f855b70ea4f8657f6f00180b43be23c6edd3259a84b873d560f60f5a7d7fd54b0330f835398c4ef2bb3a61d2fae5088b03c542ac58f663ad15cb471e39f6f06d2a47cd696bda59923f64718e81a5438f1711d43e284b9c566e596dc77f1e0809f96d40f76804c265ab9654c1ff8c18a1e8410164d09ae5bc1dd982eceb57c0114b0cac3e14c4e1a4a4fd22c98563b9cbe859c67768a39f67abd5dbe55d3d16ff58e5c45b21d8c573f54b901f0cb8b0701cdf9bded0827a82dc56ad98807f9c96ca814b2651a6b82d22a5c10d5fc80cdbd00000001207a636173685f636c69656e745f6261636b656e643a6f75747075745f696e666f11024951eeff9ccf4eb390ff94a60aa5673d015249a562d4ea0bc08e26f627c4a418d274e930230cab2139e2766d2654f0ed3903b882070039fb66568096852da4cb54410485be43a51a0269351ed32433ed7ddd1b12d43b00013b4c678abdaf00e1fc4587a41d1402c75bbc0dcc1c0e2b7652dc14352b87623f";
- let pczt_hex = hex::decode(hex_str).unwrap();
- let pczt = Pczt::parse(&pczt_hex).unwrap();
- let fingerprint =
- hex::decode("2fac20755b7bb7c99d302b782ea36f62a1b0cfe8d7d4d09a58e8ba5da26f4578")
- .unwrap();
- let ufvk = "uview10zf3gnxd08cne6g7ryh6lln79duzsayg0qxktvyc3l6uutfk0agmyclm5g82h5z0lqv4c2gzp0eu0qc0nxzurxhj4ympwn3gj5c3dc9g7ca4eh3q09fw9kka7qplzq0wnauekf45w9vs4g22khtq57sc8k6j6s70kz0rtqlyat6zsjkcqfrlm9quje8vzszs8y9mjvduf7j2vx329hk2v956g6svnhqswxfp3n760mw233w7ffgsja2szdhy5954hsfldalf28wvav0tctxwkmkgrk43tq2p7sqchzc6";
- let fingerprint = fingerprint.try_into().unwrap();
- let unified_fvk = UnifiedFullViewingKey::decode(&MAIN_NETWORK, ufvk).unwrap();
-
- let result = parse_pczt_cypherpunk(&MAIN_NETWORK, &fingerprint, &unified_fvk, &pczt);
- assert!(result.is_ok());
- let result = result.unwrap();
- assert!(!result.get_has_sapling());
- assert_eq!(result.get_total_transfer_value(), "0.001 ZEC");
- assert_eq!(result.get_fee_value(), "0.00015 ZEC");
- let transparent = result.get_transparent();
- assert!(transparent.is_some());
- let transparent = transparent.unwrap();
- assert_eq!(transparent.get_from().len(), 0);
- assert_eq!(transparent.get_to().len(), 1);
- assert_eq!(
- transparent.get_to()[0].get_address(),
- "t1XTwB8P1x4YipBtLhPWS13JPDQ5RMkMA6M"
- );
- assert_eq!(transparent.get_to()[0].get_value(), "0.001 ZEC");
- assert!(!transparent.get_to()[0].get_is_change());
- assert!(!transparent.get_to()[0].get_is_dummy());
- assert_eq!(transparent.get_to()[0].get_amount(), 100_000);
- let orchard = result.get_orchard();
- assert!(orchard.is_some());
- let orchard = orchard.unwrap();
- assert_eq!(orchard.get_from().len(), 1);
- assert_eq!(orchard.get_from()[0].get_address(), None);
- assert_eq!(orchard.get_from()[0].get_value(), "0.14985 ZEC");
- assert!(orchard.get_from()[0].get_is_mine());
- assert_eq!(orchard.get_from()[0].get_amount(), 14985000);
- assert_eq!(orchard.get_to().len(), 1);
- assert_eq!(orchard.get_to()[0].get_address(), "<internal-address>");
- assert_eq!(orchard.get_to()[0].get_value(), "0.1487 ZEC");
- assert_eq!(orchard.get_to()[0].get_memo(), None);
- assert!(orchard.get_to()[0].get_is_change());
- assert!(!orchard.get_to()[0].get_is_dummy());
- assert_eq!(orchard.get_to()[0].get_amount(), 14870000);
- }
+ fn test_validate_orchard_user_address_rejects_invalid_address() {
+ let sk = orchard::keys::SpendingKey::from_bytes([2; 32]).unwrap();
+ let fvk = orchard::keys::FullViewingKey::from(&sk);
+ let address = fvk.address_at(0u32, orchard::keys::Scope::External);
- #[test]
- fn test_decode_pczt_to_sapling() {
- let hex_str = "50435a5401000000058ace9cb502d5a09cc70c0100ec8ba80185010001227a636173685f636c69656e745f6261636b656e643a70726f706f73616c5f696e666f144951eeff9ccf4eb390ff94a60aa5673dc48ba80100000002ac338dcff9cf137ba61ae13c992b53109861de0e794f41ba2399af570c768ae76f5c33dcb56261068f5413a002d03086a41d2dd40d6bc41ad2b8eb1831c3cc65ea183dd3a9cf918cc71b00d97a83263c9a42d277c154450976b9fc4404ff8322c404b2f3c896db40d43126376480272a057830c30855b16f7a2e8a84160a92d10c893f6bad7cf2c4f8f13e01a109c8b62b0f01f416b29e01d08e4053b596c6a2d9ca56b51c92a644a25449848f8c82945ed7ccf67045a25c5180b2f13a10b7f0137d543cabd753aa3f09a41f1b0318bbe608a50158d74dfe702f418e1ca2c57809de09dc51a255d7a83a45ce3dcaec15adbfdd5d221e1a3e43fd740c05ae24f41ebd7df84732602276708710969a1c4589fbafda4a2e8be96657a27c4dd33da64f583b755ab9f8b9cc3ac4898d175160cae430f697ddd2c04ff7103e45897d8caf52a106c807e9e639b7307776713266e448a826a5027d12c7b1b96a5b5057b7a66c10b2eaa366524db0221e025c984f72b5652bbb3e887e47bee059f8791184806ffd3ddeb167d41d16b6d748781d7cf7c8474ec406cc6925f9fc1f2a25f327147831b0432d930ab177678cbb5cd9ba646812c42a39891331858ed1877ae6fddf67183cbf3331c1ee0da60689d7019f714033742d5e036fcc6fdd298023a5450f59d57755f3d7684bb929f6a146bba970f3787aaf302ef8270fdd4670d80148d3240c1c00ac6e6a36eade8a6cad9576678ddd9f316653a541030e6131577caab1cf1837473de30552b18ca47cca65a79a6e077a1df3f8241ea3dc169df1f1f5fdb671be38a9a963e9d6197a28f30d94c99f2d2712a1fa090d29a2c091f11e58378331c3460beb25d76bdb6a050efc03474fc223f1f90a491096935ff8579890b4151dc824e214ee45506ffaee400b6ba7865704d5a8ad8bd9d6545e9eead4b0be8a0994d47750d8b5a130806ae84dbcf552763954c3313000df63a0b9b0186f4756dd86c02ac54da43648e502f99e0704a66b8ff3037b6ce3c531a81fbbf83cf526e5130666000cdff65015dfdeb5fa7a6c80e4d4477c00016eda1757b0b4c2f1851ef53408cdd09b6965ad895fd9d4b1fd0617a0b0ac88659182f086a85963f8cc976001a08d060184f4595f43b2229d250ccf8941e7972b36ae3f3c7b2492c5a452a6d60da5fb97012b5c6bb24cc8d10f1d8f093ea3a78279d2e6914a1cba7eceff5a0b97c8b7bc040000014e7a7331646d6470773461736b6e70307270673737353671336e77736e64356b74747666746c766166763061716374367076397633706a6572716873733635396a636c63656a746b7175716674723401207a636173685f636c69656e745f6261636b656e643a6f75747075745f696e666f01007e67b65e0e9bca4567ccb50d8978da4eac6f2632fae722539d8bac8b3705cae2e35265414af4c9aec1f314decff9de81c795d1efbf62f9fbc4835062cdf48f5b1821183721a30bdc256c8817c1bab47765ac92104f8575134b9dbfd8f626150dc404c79c1e4914b8e1583f30dddd622fd852075e31b3435f7674ee199a3e439340ed647d6feaaa3d61b56cf4a9871c1d2dd16b156e9d623245ce794d30f06fbb118858cf2d80e924f4953c47c8938969f5d1b97371a4089972166a338efd78bb650b7868f0f9c011be13403e4995a0e454f8b379b684553bb34b2dff48d5475c4a7a97a13c3100ad75a1296b066cd6a2db0a6053c7ad6f81a717f2f51dae0b96695e2936fc7c979e524eeab8ebcbb6638f3d68c480d3fba0fecd1bf0b2efd5254102c8d97db89d67eaf49e5be32549c3626c3a1874c9f695f080bafd54ea4d80fec72bdec44c7648e27b2e1087cf1ee81c38ec01757157a1fe9ec8ee81901187c5bcabe3a3d1b7c2a05a63372aab06a21552fd7bc7431ae601ab307ed3451d111cb86a261291fefed4978558bbf62099d3e766b611cde5f207d95bc8ca9d50b460b14c8cb2dcde6b4a926537c6255e028a9423d98b76581cc9e99a5b0df20a43678b6ce4927e433c25cf00a3a4f97b8237cec8a18948791f4bb0b4362908ce8bcefd94eecc1e1c4319c47be8f234196a974bfbbbd76b8ddae7026526f27d5e5699e3aa534a042daaf9ecab6303d167a4cb9a3bd5703f3e135e6c45fba5fbe4f4889707206e5086b47af7e7dba7ac10b0b32db1a9caaccb2526500b9480ac9332bd4a4d496bc7eed630edf6a01de960fbabe766192de0d07b29b9ad9c029bf872914ab142a795991b7dffe8322de6b51cff4b4362070e1b2a753a1815702ea5035812e10ade2e3a0d310a8a25aac537251a2fa2a7d40c8168407ed6c1b3212eb0cdc47f0377dc50d795019499a348931c9d84a8dab8e720b2c8d42182809c234322f63d064eef4f80ce75f845b1347796b36c0cbaa99c0015b4dc30ae777e4b294ec862c07bace978564f4d7aa79ebc0268385205c4cc0f00016340cd133ad2995f980c9ebb6a14d1b8891aecadd897fb89ea4842b36d3bbf71d4594704ec94bc7673ed62010001772d3a1d8c04cf4051ee4e079ee662ea6e4ca8368247df928887b11a59c8d61701a1e5f40d9da687a1e0254d62cd004d56b512de67e4135610268c47a1e556fb0300000000bf9a0c47417e4bfba4d866f5b2762b342b430857c17a29138f4f59739390f61670590401ebea9616759f3d1f855b712c237898d67841c44e006d922783c8e02c3ca6c505029a5e3c582173eebd763c0c32746a46011381f223701ace23bcc5617b7405268550afd777a0138ca528fb44720600f6b81fb0e23b2ba4e1a21882e889c8ebf82f31166f087d042ae49cea162e499e9d0883b610108fb7f1397b3b0b4ee351290a0123f48d843320d31777010f37759faf689b1cc4882a5ebcce2e7bd3ce71805a33b87989c47ff7ab5707fc86452e2a49e778cace05026ed1acd2f1eca9b78a1f3501dc81e7da6e61186045a442a269bca8960b10df99233b1fca2262c5a6cd010fbcade4772250c8c99a620d1b0100013c72f350a5fb02a34d7aa9ba6580db86c5fb296c86adea6033cb95649a46d33a018cfa3b58f3d598ac9f509eaf9351c44a83211a79a04f17e5f154eb86addab01a01f08e7fbb25b69a456e293e9720fd9703b60c479f5e9468cc991fd7b4ac43700592ff2d75cec6cb7e9425226a8bb144e54e86c445c345fdc1fe1e3a0269d14c29e1f3b49fc0b4c5c95046eb46bdd6a6e8f42a48577733a3f10956a0091161db00018391cab603411efcfdf27b04a7db524e81dde69ddda3f5e6af2702d58ae82b3cb173c114059cf95e884d22eeac429f976f158ed027839eb5fbc8885b73c8f64ab89c618f13e9955cfd994cb1bb75bef1249df49815e0f13e351e64cded9e2bbfb11ef1bd261d5a7ba637c5eb8dd035e3e5c2292311f8c7a6fa92a846211becee977d8d232ee62ceb5ebeba78f05da856cd0be335770a87cb91b83b58843a3e311b4df3af1aca4d4be4a1bbdb2480d4efff6bc98af1f95b4d4c6a58fbd3742fa61db2a49933138b3786e9b5775ebdde58de68739c4ffd62d4e86759106f891eea8e2100792c45cb3b68ad07332100bb291ca1201c79ce0e04a76cf4814d62209004e70e500c34a70c9158d6015deb4475ebd593058a6bf82c31e13bd02e93ca51042e08492ce67e2a7df01eac78e7fb9eaca726dac17a27a993844da3f7b722350d4f62523d964ffdd390220ba1e99e5615405e7e97b6733734ade4ef4441c190be855a913c04ce0c2d537ee5b505764821e0bc667038bc938473b58b53eb37903fa4a32719df1fbf380d1b6d525da31982fd3cd03a0eef388a65b0a1649e4adf5c92144e0e65d4c3192f67d3b9909240f335d19d13a57c579f04e31c60d6b0ba362753be38fc0c5d40b6af321d5d6a6c1abbcec982af39d42394c34b61b808ed0bc36f9003ba5d9a122d53cc242c14bcedd30d6b5b303d24a859ef2d559378a916ac5e5d24fa17787d7c171c881514738abed0b757b67f297a1f167b1a3f43a552f4e37e1678caf66057dad1f384b918d8ad36d72ea1633aaa0248000370105e9766d7b300c4c303e72d0e83c3d52059df29ccdc62e6974ff4c6a3ee0a7dd03d7f8d48cb3e9620e83fa02a6ecbf95a3af9765baab0176aae8d507384abd291aba006e7de3c802e78d3d8db3dc718cea36159aca411d17aaf101bc55cbb7a681833e05bfb0f23d584f0f8db1b333445ea47be715be1fccff9e9337f9e740fd737b22a2afe2935134e67f989748cd5470a92ae12e7ece501c68900717b3a3aba6c513e2c4514d854d63fad26d3770485aeb5407a83fd249744a5995501a95123b33e4e80442c1db0b050714dc06491579ff7a8286a6c6d2c27edf8f6aa70667e2cc9af63f208535fb5ae610315797071dfced79f4a4ba943dc8ae9b4008d21de1c46abe2ba296d133d1387a3225425050c920165d85913e754c7430b07b5b6df842010ddd1306fc74f14de29640d8e41741d7791aa128085dd4421424e24a407c62976718d0c5846790cc3948dba055d0258a53b0d6617b72f0aa112f6f654f7a2f219f4481263f21a7d4132facab430b03b1f575494db945cca62d38cfdd00e86b2f20526171f6533b3f13400946c9902ee348a41b925afad8a9f695845277e394ad8ee3f26205b7a25825e3ceefd8d629e87a1af0ce6c45fcccac6ee32d2b64dd75a3c2c0601819cf447911878cb2b2d16cbca1bb7192652db8a549c1612838387954895572b000000b03dbb44ce10a905f792050b1aa8e285ef9ab1bc860db688afe9aa76554022165d4b0fec38ad2427118ecfb2d80b43f2e2f1d0bf58eea3b95d4735522d6f3228c4048a920344ceea4a719fdb41a052e537b76cc1303776255389fa92c2d481120dad733dfba897dff10a066697d41aef022577446ccea0634004af98ba8e7e4d832c69aa7596f706c1b3d0f1e7887836892ca862cbc45b891d302a2301b98ed4f8d7e148869d30104ed8f1467cd9b2217331d7e3874a25688edf9421172f7bb0cd56af6e890b633b849e6a7df2017dcd116707bf194c77529eccfc4e4de8cf81d2b94f54fb3ffe6dd87447abb0c926dc1fb6ce2e2fd684a1fc1b4dcf1c1245d93f7f79ed738c0a8d80ea6194039a388926f555b088d166f8f005c8eea2d120230951d0f0528c981babecc3be4f52f9cf932df91ee41335de3941db27e5a7a117ed2d16c69f2be3dbdc4df453894efc86d161a364b0f8443e829e57aabfa3eb3ea85ba58c924a186d326c3987eb535ad4176223621e6d339352fbff0dabc8aa18486433dd0b99fa5236f4e6bf353335ec1de5cfb8860fdc8ab0ec02ebf4689f5d51e9d7cb497ffd5bfcb93a42e97c12a518d79d5da9d11aed018e908be3c1ec5d1cb12067a33cf4dc710ad6186e577ec11dc8df077056bc7473909cfdd87b60e20bc010893d893f5a82e3941da1e39b731d79fda52b4022e79884a255d96312eef5bbe5fefa0999e63f543996c176a4ae9773c2306b349afc6148594052bb15dc5c006691442813d3369e52aeda9c305236959dc096aa544aa364b8a24d08d8a895ad7e7361ef7eb1c36516f4a13e1b7740b44f805e5ac744180d39129c8e8665799b2195ad2d96c55145c4af04af8bd76ed54e5452e5ca253fccaddb3829b26c1055c6d517f950741035b384e264bd677b5265079d0610de5e54a1d92e5d48fd705f38ac14c7e87c6281899b2f81ea4c8e4f62bf91e50e769f3cc7e2faa0c16aa666c9a942759ddbb94c4d07fc1b6859b15c3d11f9e8b7015fd4f8520a3d8fa00ccdeede53f443672815ae36466b733a006887ab60540ccc59fd50d4a9f1a759a63f8b010001e3996dce9674d98958bfd1e46cf28a54df3c0de71122cbb93288648bf75bb7320000000001cd4bffb62423806a435a31a774d639275e7f3be1ac7703091d745b8ce8a8292ee3a722e3194f6fe7bc2df7c88d94b5b98442b0368eeea9bb298ae35bfe05e186c2147137cbc46f2902e7cb51161120829bf48d59f274e6808ffb1a260de3b008d31ba22f275af783e39f511ecdc10d621e04a40cec76437cf97bec8728a0dc19000114b0cac3e14c4e1a4a4fd22c98563b9cbe859c67768a39f67abd5dbe55d3d16ff58e5c45b21d8c573f54b901c0b49807014bffc5036be15238fea26adbfca534330c943fad580fdc3f2b487ee5510aa91801846a55accbc6d7cf8a388ed5ca3ce07dd77ff88000259da019ba8daa61b15347017d6d7535f6e3eade13042662d05eefa91d1f4757fcd0b4b844554e77366f4b36a7068e15c2f06aaa11f6bcfd2e9e36231530069f7fb99a2771050689f24f332b427fdd05d1d5f3219230a1406f0955e3ddc0ef5ce2c7f160d797f3e54ef5782001a1e0ac172341eb105c1c4cd0b214c393110edf135827ffa908522c0adbe047f04121402edab64ddf965b98a2325fb204da47aa4ba418120c2077e0a0f083cc53a27273257f9ac2783b4b6a06b835e6110de28bb42059261263682dd2224accfbc1339e3c137904b7789ec0393f7e3fc510c4c3edf0120f6a176aeb721ef944c237368b2d5d3fab8c4f5841174817748561262841a0f7b5347e83f0ab07a54798ac548000f1c70354010ed9de251594dda8495ed7abd55babc4d6b56493562f0e37d3293c8a882758980efc55e338e8a004ad39dd72289d00423e8a4459eb74401cd84e0e4e14563df191a2a65b4b37113b5230680555051b22d74a8e1f1d706f90f3133bb3bbe4f993d18a0f4eb7f4174b1d8555ce3396855d04676f1ce4f06dda07371f4ef5bde9c6f0d76aeb9e27e93fba28c679dfcb991cbcb8395a2b57924cbd170ea3c02568acebf5ca1ec30d6a7d7cd217a47d6a1b8311bf9462a5f939c6b743073ef9b30bae6122da1605bad6ec5d49b41d4d40caa96c1cf6302b66c5d2d10d396e0183683f64ec039e3f3ecfd4753b55e83d3d70f635dc360a342d69256bfa349d2e26bdef115ced7bd36305a54386996133c4e65759f3731637a40eba67da103f98adbe364f148b0cc2042cafc6be1166fae39090ab4b354bfb6217b964453b63f8dbd10df936f1734973e0b3bd25f4ed440566c923085903f696bc6347ec0f6f3f63aab58e63b6449583df5658a91972a20291c6311b5b3e5240aff8d7d00212278dfeae9949f887b70ae81e084f8897a5054627acef3efd01c8b29793d522ca2ced953b7fb95e3ba986333da9e69cd355223c929731094b6c2174c7638d2e60040850b766b126a2b4843fcdfdffa5d5cab3f53bc860a3bef68958b5f066177097b04c2aa045a0deffcaca41c5ac92e694466578f5909e72bb78d33310f705cc2dcaa338b312112db04b435a706d63244dd435238f0aa1e9e1598d354708102dcc4273c8a0ed2337ecf7879380a07e7d427c7f9d82e538002bd1442978402cdaf63debf5b40df902dae98dadc029f281474d190cddecef1b10653248a234151f91982912012669f74d0cfa1030ff37b152324e5b8346b3335a0aaeb63a0a2de2bca6a8d987d668defba89dc082196a922634ed88e065c669e526bb8815ee1be8ae2ad91d463bab75ee941d33cc5817b613c63cda943a4c07f600591b088a25d53fdee371cef596766823f4a518a583b1158243afe89700f0da76da46d0060f15d2444cefe7914c9a61e829c730eceb216288fee825f6b3b6298f6f6b6bd62e4c57a617a0aa10ea7a83aa6b6b0ed685b6a3d9e5b8fd14f56cdc18021b12253f3fd4915c19bd831a7920be55d969b2ac23359e2559da77de2373f06ca014ba2787d063cd07ee4944222b7762840eb94c688bec743fa8bdf7715c8fe29f104c2a01fb1dfcae4233ee8eece0af6dfae15925a261b8814a22830570fcbcad85384c1b012fac20755b7bb7c99d302b782ea36f62a1b0cfe8d7d4d09a58e8ba5da26f457803a0808080088581808008808080800800004220db463466091bc1a7dde8bcc53ea875bcabe9cdef45bdda6ea4dce9830c398ad3a6cf27673cf9bc86ce3a502bea62fcf197599542a312aad33cdbf0976d06c40437e76543ef96f903074d8e97c0b551c7fb94249d358dd04d8337e00f0742e9e57eb3008d3e814188a9d2444c4e6094d3fc779af7a3a11b297b4af3064b2a3195daaf3d1e8d682f12824f799e6c46db73b5b7676ee9ceeae026cdf610bfd7a7b96cab6e27f1642cce2e5360b1fd0ee4b1b4a87ceb5d8a0b9e6e433ab94b16920d6b5c792614084879e7a0b7c47bc9ad0e637359a9651661e6ba963b5c311977f0276f5c2f791217b7ea0691bfd20271cf9492f2dcc2dd0951aa015d8c21062549d8adcba1b59ff9d9ef29932694a22e5d26542963efa5724f730e93f700b2a22910d49fe03386695997b86bc1fa44bf6532b8360e3eed30445aada9f1d8e9a0fa51c7d1717a60f7c70721f4fbf4335cb1182f22a1792d7d4c5e9ff2f449f9373d0acd3e413777da3489a43ef944765817a37c16b11967abb16dc13027c312c02b49d78bc30fed0efc54af79d0b9dc2dac1b062190890346f738dcbc562e8dc20e57dedfd0d8752e9651bee550c34900381528061d0d0f69348ff0c9a368bcefbfd307d59b01a9d91a533b8d309de686dc60fa6e31872ad25348f653a00619d035ae000a6fdb92d78067739058e272d60900cc0653440e2c907157df8b1681e254c94225b2c6b8fba7bb24cdfba0ebafe1697babc8f544e8d7312764fce4a485149f89c92112f63a2cf537f3c207fb50078fa20d0d00b8c093357f32ef5544ec7a8da37ee868b5c58a427913f11eea557e5830eb6a2882ec791dd5bb0aa4046ba716b6e03588d60bc39b7ad560c4642be0a8201665a78396735072c8ddb35781224cf9be6050fab075f24061973bdd2f2587503aef1a8ececd60cd58be6ee7807b7744c4a798326365d3cb561ac3b909984d1bb9fa5175f706dc145006612fb42f7e51ab7ddb02393d68a0f93c30c007d8a1082fcb480114b0cac3e14c4e1a4a4fd22c98563b9cbe859c67768a39f67abd5dbe55d3d16ff58e5c45b21d8c573f54b901808b910701f35323d2510d77264cce5f5fac679fccdcbd877939a2ab3006910a92a45295fc00000001207a636173685f636c69656e745f6261636b656e643a6f75747075745f696e666f11024951eeff9ccf4eb390ff94a60aa5673d01bdcb2a53666e628f2fd162b9c7db0fcdc2b668f1db89b62cf99455a34514742903c0a9070006f01fcf2202c1550a6bd0770a536233035d23e02b38ecb0bf94d02525708e08000189172a0a6aa69b6d9582ff56401903d22036a4d28801ba351609b12f2ebd9d17";
- let pczt_hex = hex::decode(hex_str).unwrap();
- let pczt = Pczt::parse(&pczt_hex).unwrap();
- let fingerprint =
- hex::decode("2fac20755b7bb7c99d302b782ea36f62a1b0cfe8d7d4d09a58e8ba5da26f4578")
- .unwrap();
- let ufvk = "uview10zf3gnxd08cne6g7ryh6lln79duzsayg0qxktvyc3l6uutfk0agmyclm5g82h5z0lqv4c2gzp0eu0qc0nxzurxhj4ympwn3gj5c3dc9g7ca4eh3q09fw9kka7qplzq0wnauekf45w9vs4g22khtq57sc8k6j6s70kz0rtqlyat6zsjkcqfrlm9quje8vzszs8y9mjvduf7j2vx329hk2v956g6svnhqswxfp3n760mw233w7ffgsja2szdhy5954hsfldalf28wvav0tctxwkmkgrk43tq2p7sqchzc6";
- let fingerprint = fingerprint.try_into().unwrap();
- let unified_fvk = UnifiedFullViewingKey::decode(&MAIN_NETWORK, ufvk).unwrap();
-
- let result = parse_pczt_cypherpunk(&MAIN_NETWORK, &fingerprint, &unified_fvk, &pczt);
- assert!(result.is_ok());
- let result = result.unwrap();
- assert!(result.get_has_sapling());
- assert_eq!(result.get_total_transfer_value(), "0.001 ZEC");
- assert_eq!(result.get_fee_value(), "0.0002 ZEC");
+ let result = validate_orchard_user_address(&MAIN_NETWORK, "not-a-zcash-address", &address);
+
+ assert!(matches!(
+ result,
+ Err(ZcashError::InvalidPczt(msg)) if msg.contains("user address is invalid")
+ ));
}
#[test]
@@ -812,7 +915,7 @@ mod tests {
let seed_fingerprint = [0x22; 32];
let output = p2sh_output_with_matching_seed_fingerprint(seed_fingerprint);
- let parsed = parse_transparent_output(&seed_fingerprint, &output).unwrap();
+ let parsed = parse_transparent_output(&MAIN_NETWORK, &seed_fingerprint, &output).unwrap();
assert!(!parsed.get_is_change());
}
diff --git a/rust/apps/zcash/src/pczt/sign.rs b/rust/apps/zcash/src/pczt/sign.rs
index ffa5d34..565a63a 100644
--- a/rust/apps/zcash/src/pczt/sign.rs
+++ b/rust/apps/zcash/src/pczt/sign.rs
@@ -1,29 +1,108 @@
-use super::*;
-use crate::version::KEYSTONE_FW_VERSION;
+use alloc::{
+ format,
+ string::{String, ToString},
+ vec::Vec,
+};
-/// `global.proprietary` key stamped into every signed PCZT response.
-/// Value is 3 bytes `[major, minor, build]`. Wallets read this to check
-/// whether the device meets their minimum version requirements.
-const PROP_KEY_FW_VERSION: &str = "keystone:fw_version";
use bitcoin::secp256k1;
-use blake2b_simd::Hash;
-use keystore::algorithms::secp256k1::get_private_key_by_seed;
-use rand_core::OsRng;
+use keystore::algorithms::{
+ secp256k1::{get_private_key_by_seed, get_public_key_by_seed},
+ zcash::calculate_seed_fingerprint,
+};
use zcash_vendor::{
pczt::{
- roles::{low_level_signer, redactor::Redactor, updater::Updater},
+ roles::{
+ low_level_signer,
+ redactor::{orchard::OrchardRedactor, Redactor},
+ updater::Updater,
+ },
Pczt,
},
- pczt_ext::{self, PcztSigner},
- transparent::{self, sighash::SignableInput},
+ transparent,
+};
+
+#[cfg(feature = "cypherpunk")]
+use zcash_vendor::{orchard, pczt::roles::signer::Signer as RoleSigner};
+
+#[cfg(all(feature = "multi_coins", not(feature = "cypherpunk")))]
+use zcash_vendor::{
+ pczt_ext::{self, PcztSigner as LegacyPcztSigner},
+ transparent::sighash::SignableInput,
};
+use crate::{errors::ZcashError, version::KEYSTONE_FW_VERSION};
+
+/// `global.proprietary` key stamped into every signed PCZT response.
+/// Value is 3 bytes `[major, minor, build]`. Wallets read this to check
+/// whether the device meets their minimum version requirements.
+const PROP_KEY_FW_VERSION: &str = "keystone:fw_version";
+
+#[derive(Debug)]
+#[cfg(feature = "cypherpunk")]
+enum SigningKeyCollectionError {
+ Zcash(ZcashError),
+ TransparentParse(transparent::pczt::ParseError),
+ #[cfg(feature = "cypherpunk")]
+ OrchardParse(orchard::pczt::ParseError),
+ OrchardBundleParse(zcash_vendor::pczt::orchard::BundleParseError),
+}
+
+#[cfg(feature = "cypherpunk")]
+impl SigningKeyCollectionError {
+ fn into_zcash(self) -> ZcashError {
+ match self {
+ SigningKeyCollectionError::Zcash(e) => e,
+ SigningKeyCollectionError::TransparentParse(e) => {
+ ZcashError::SigningError(format!("failed to parse transparent bundle: {e:?}"))
+ }
+ #[cfg(feature = "cypherpunk")]
+ SigningKeyCollectionError::OrchardParse(e) => {
+ ZcashError::SigningError(format!("failed to parse shielded bundle: {e:?}"))
+ }
+ SigningKeyCollectionError::OrchardBundleParse(e) => {
+ ZcashError::SigningError(format!("failed to parse shielded bundle: {e:?}"))
+ }
+ }
+ }
+}
+
+#[cfg(feature = "cypherpunk")]
+impl From<ZcashError> for SigningKeyCollectionError {
+ fn from(e: ZcashError) -> Self {
+ SigningKeyCollectionError::Zcash(e)
+ }
+}
+
+#[cfg(feature = "cypherpunk")]
+impl From<transparent::pczt::ParseError> for SigningKeyCollectionError {
+ fn from(e: transparent::pczt::ParseError) -> Self {
+ SigningKeyCollectionError::TransparentParse(e)
+ }
+}
+
+#[cfg(feature = "cypherpunk")]
+impl From<orchard::pczt::ParseError> for SigningKeyCollectionError {
+ fn from(e: orchard::pczt::ParseError) -> Self {
+ SigningKeyCollectionError::OrchardParse(e)
+ }
+}
+
+#[cfg(feature = "cypherpunk")]
+impl From<zcash_vendor::pczt::orchard::BundleParseError> for SigningKeyCollectionError {
+ fn from(e: zcash_vendor::pczt::orchard::BundleParseError) -> Self {
+ SigningKeyCollectionError::OrchardBundleParse(e)
+ }
+}
+
+#[cfg(all(feature = "multi_coins", not(feature = "cypherpunk")))]
struct SeedSigner<'a> {
seed: &'a [u8],
}
-impl PcztSigner for SeedSigner<'_> {
+#[cfg(all(feature = "multi_coins", not(feature = "cypherpunk")))]
+impl LegacyPcztSigner for SeedSigner<'_> {
type Error = ZcashError;
+
fn sign_transparent<F>(
&self,
index: usize,
@@ -33,96 +112,99 @@ impl PcztSigner for SeedSigner<'_> {
where
F: FnOnce(SignableInput) -> [u8; 32],
{
- let fingerprint = calculate_seed_fingerprint(self.seed)
- .map_err(|e| ZcashError::SigningError(e.to_string()))?;
-
- let key_path = input.bip32_derivation();
-
- let path = key_path
- .iter()
- .find_map(|(pubkey, path)| {
- let path_fingerprint = *path.seed_fingerprint();
- if fingerprint == path_fingerprint {
- let path = {
- let mut ret = "m".to_string();
- for i in path.derivation_path().iter() {
- if i.is_hardened() {
- ret.push_str(&alloc::format!("/{}'", i.index()));
- } else {
- ret.push_str(&alloc::format!("/{}", i.index()));
- }
- }
- ret
- };
- match get_public_key_by_seed(self.seed, &path) {
- Ok(my_pubkey) if my_pubkey.serialize().to_vec().eq(pubkey) => {
- Some(Ok(path))
- }
- Err(e) => Some(Err(e)),
- _ => None,
- }
- } else {
- None
- }
- })
- .transpose()
- .map_err(|e| ZcashError::SigningError(e.to_string()))?;
-
- if let Some(path) = path {
+ if let Some(path) = transparent_key_path_for_input(self.seed, input)? {
let sk = get_private_key_by_seed(self.seed, &path).map_err(|e| {
- ZcashError::SigningError(alloc::format!("failed to get private key: {e:?}"))
+ ZcashError::SigningError(format!("failed to get private key: {e:?}"))
})?;
let secp = secp256k1::Secp256k1::new();
- input.sign(index, hash, &sk, &secp).map_err(|e| {
- ZcashError::SigningError(alloc::format!("failed to sign input: {e:?}"))
- })?;
+ input
+ .sign(index, hash, &sk, &secp)
+ .map_err(|e| ZcashError::SigningError(format!("failed to sign input: {e:?}")))?;
}
Ok(())
}
+}
- #[cfg(feature = "cypherpunk")]
- fn sign_orchard(
- &self,
- action: &mut orchard::pczt::Action,
- hash: Hash,
- ) -> Result<(), Self::Error> {
- let fingerprint = calculate_seed_fingerprint(self.seed)
- .map_err(|e| ZcashError::SigningError(e.to_string()))?;
-
- let derivation = action.spend().zip32_derivation().as_ref().ok_or_else(|| {
- ZcashError::SigningError("missing ZIP 32 derivation for Orchard action".into())
- })?;
+#[cfg(not(feature = "cypherpunk"))]
+pub fn sign_pczt(pczt: Pczt, seed: &[u8]) -> crate::Result<Vec<u8>> {
+ super::validate_supported_pczt(&pczt)?;
+ reject_legacy_unsupported_pczt(&pczt)?;
+
+ let signer = low_level_signer::Signer::new(pczt);
+
+ #[cfg(feature = "multi_coins")]
+ let signer = pczt_ext::sign_transparent(signer, &SeedSigner { seed })
+ .map_err(|e| ZcashError::SigningError(e.to_string()))?;
+
+ Ok(stamp_and_redact(signer.finish()).serialize())
+}
- if &fingerprint == derivation.seed_fingerprint() {
- sign_message_orchard(
- action,
- self.seed,
- hash.as_bytes().try_into().expect("correct length"),
- &derivation.derivation_path().clone(),
- OsRng,
- )
- .map_err(|e| ZcashError::SigningError(e.to_string()))
- } else {
- Ok(())
+#[cfg(not(feature = "cypherpunk"))]
+fn reject_legacy_unsupported_pczt(pczt: &Pczt) -> Result<(), ZcashError> {
+ #[cfg(zcash_unstable = "nu6.3")]
+ {
+ // The legacy helper below carries the pre-NU6.3 transparent sighash implementation.
+ // It must not be used for V6/Ironwood PCZTs.
+ if super::pczt_requires_cypherpunk_support(pczt) {
+ return Err(ZcashError::SigningError(
+ "V6 or Ironwood PCZTs require cypherpunk signing support".to_string(),
+ ));
}
}
+ Ok(())
}
+
+#[cfg(feature = "cypherpunk")]
pub fn sign_pczt(pczt: Pczt, seed: &[u8]) -> crate::Result<Vec<u8>> {
- let signer = low_level_signer::Signer::new(pczt);
+ super::validate_supported_pczt(&pczt)?;
+ let transparent_keys = collect_transparent_signing_keys(&pczt, seed)?;
+ let orchard_keys = collect_orchard_signing_keys(&pczt, seed, ShieldedPool::Orchard)?;
+ #[cfg(zcash_unstable = "nu6.3")]
+ let ironwood_keys = if super::pczt_should_process_ironwood(&pczt) {
+ collect_orchard_signing_keys(&pczt, seed, ShieldedPool::Ironwood)?
+ } else {
+ Vec::new()
+ };
- #[cfg(any(feature = "multi_coins", feature = "cypherpunk"))]
- let signer = pczt_ext::sign_transparent(signer, &SeedSigner { seed })
- .map_err(|e| ZcashError::SigningError(e.to_string()))?;
- #[cfg(feature = "cypherpunk")]
- let signer = pczt_ext::sign_orchard(signer, &SeedSigner { seed })
- .map_err(|e| ZcashError::SigningError(e.to_string()))?;
+ let signature_count = transparent_keys.len() + orchard_keys.len();
+ #[cfg(zcash_unstable = "nu6.3")]
+ let signature_count = signature_count + ironwood_keys.len();
+ if signature_count == 0 {
+ return Err(ZcashError::PcztNoMyInputs);
+ }
+
+ let mut signer = RoleSigner::new(pczt)
+ .map_err(|e| ZcashError::SigningError(format!("failed to prepare PCZT signer: {e:?}")))?;
+
+ for (index, sk) in transparent_keys {
+ signer
+ .sign_transparent(index, &sk)
+ .map_err(|e| ZcashError::SigningError(format!("failed to sign input: {e:?}")))?;
+ }
+
+ for (index, ask) in orchard_keys {
+ signer.sign_orchard(index, &ask).map_err(|e| {
+ ZcashError::SigningError(format!("failed to sign Orchard action: {e:?}"))
+ })?;
+ }
+ #[cfg(zcash_unstable = "nu6.3")]
+ for (index, ask) in ironwood_keys {
+ signer.sign_ironwood(index, &ask).map_err(|e| {
+ ZcashError::SigningError(format!("failed to sign Ironwood action: {e:?}"))
+ })?;
+ }
+
+ Ok(stamp_and_redact(signer.finish()).serialize())
+}
+
+fn stamp_and_redact(pczt: Pczt) -> Pczt {
// Stamp the firmware version into `global.proprietary` so the wallet can
// tell exactly which version of Keystone firmware produced this signature.
// The Redactor below intentionally does not touch `global`, so this value
// survives the redaction pass into the returned bytes.
- let stamped_pczt = Updater::new(signer.finish())
+ let stamped_pczt = Updater::new(pczt)
.update_global_with(|mut g| {
g.set_proprietary(
PROP_KEY_FW_VERSION.into(),
@@ -131,33 +213,14 @@ pub fn sign_pczt(pczt: Pczt, seed: &[u8]) -> crate::Result<Vec<u8>> {
})
.finish();
- // Now that we've created the signature, remove the other optional fields from the
- // PCZT, to reduce its size for the return trip and make the QR code scanning more
- // reliable. The wallet that provided the unsigned PCZT can retain it for combining if
- // these fields are needed.
- let signed_pczt = Redactor::new(stamped_pczt)
- .redact_orchard_with(|mut r| {
- r.redact_actions(|mut ar| {
- ar.clear_spend_recipient();
- ar.clear_spend_value();
- ar.clear_spend_rho();
- ar.clear_spend_rseed();
- ar.clear_spend_fvk();
- ar.clear_spend_witness();
- ar.clear_spend_alpha();
- ar.clear_spend_zip32_derivation();
- ar.clear_spend_dummy_sk();
- ar.clear_output_recipient();
- ar.clear_output_value();
- ar.clear_output_rseed();
- ar.clear_output_ock();
- ar.clear_output_zip32_derivation();
- ar.clear_output_user_address();
- ar.clear_rcv();
- });
- r.clear_zkproof();
- r.clear_bsk();
- })
+ // Now that we've created the signature, remove optional fields that the
+ // signing response does not need. This keeps the QR round trip small while
+ // preserving signatures and global proprietary fields for the wallet.
+ let redactor = Redactor::new(stamped_pczt).redact_orchard_with(redact_orchard_bundle);
+ #[cfg(zcash_unstable = "nu6.3")]
+ let redactor = redactor.redact_ironwood_with(redact_orchard_bundle);
+
+ redactor
.redact_sapling_with(|mut r| {
r.redact_spends(|mut sr| {
sr.clear_zkproof();
@@ -199,55 +262,358 @@ pub fn sign_pczt(pczt: Pczt, seed: &[u8]) -> crate::Result<Vec<u8>> {
or.clear_user_address();
});
})
- .finish();
+ .finish()
+}
+
+fn redact_orchard_bundle(mut r: OrchardRedactor<'_>) {
+ r.redact_actions(|mut ar| {
+ ar.clear_spend_recipient();
+ ar.clear_spend_value();
+ ar.clear_spend_rho();
+ ar.clear_spend_rseed();
+ ar.clear_spend_fvk();
+ ar.clear_spend_witness();
+ ar.clear_spend_alpha();
+ ar.clear_spend_zip32_derivation();
+ ar.clear_spend_dummy_sk();
+ ar.clear_output_recipient();
+ ar.clear_output_value();
+ ar.clear_output_rseed();
+ ar.clear_output_ock();
+ ar.clear_output_zip32_derivation();
+ ar.clear_output_user_address();
+ ar.clear_rcv();
+ });
+ r.clear_zkproof();
+ r.clear_bsk();
+}
+
+#[cfg(any(
+ feature = "cypherpunk",
+ all(feature = "multi_coins", not(feature = "cypherpunk"))
+))]
+fn transparent_key_path_for_input(
+ seed: &[u8],
+ input: &transparent::pczt::Input,
+) -> Result<Option<String>, ZcashError> {
+ let fingerprint =
+ calculate_seed_fingerprint(seed).map_err(|e| ZcashError::SigningError(e.to_string()))?;
+
+ for (pubkey, path) in input.bip32_derivation().iter() {
+ let path_fingerprint = *path.seed_fingerprint();
+ if fingerprint != path_fingerprint {
+ continue;
+ }
+
+ let path = {
+ let mut ret = "m".to_string();
+ for i in path.derivation_path().iter() {
+ if i.is_hardened() {
+ ret.push_str(&format!("/{}'", i.index()));
+ } else {
+ ret.push_str(&format!("/{}", i.index()));
+ }
+ }
+ ret
+ };
+
+ match get_public_key_by_seed(seed, &path) {
+ Ok(my_pubkey) if my_pubkey.serialize().to_vec().eq(pubkey) => return Ok(Some(path)),
+ Err(e) => return Err(ZcashError::SigningError(e.to_string())),
+ _ => {}
+ }
+ }
+
+ Ok(None)
+}
+
+#[cfg(feature = "cypherpunk")]
+fn collect_transparent_signing_keys(
+ pczt: &Pczt,
+ seed: &[u8],
+) -> Result<Vec<(usize, secp256k1::SecretKey)>, ZcashError> {
+ let mut keys = Vec::new();
+ low_level_signer::Signer::new(pczt.clone())
+ .sign_transparent_with(|_pczt, bundle, _tx_modifiable| {
+ for (index, input) in bundle.inputs_mut().iter().enumerate() {
+ if let Some(path) = transparent_key_path_for_input(seed, input)? {
+ let sk = get_private_key_by_seed(seed, &path).map_err(|e| {
+ ZcashError::SigningError(format!("failed to get private key: {e:?}"))
+ })?;
+ keys.push((index, sk));
+ }
+ }
+ Ok::<_, SigningKeyCollectionError>(())
+ })
+ .map_err(SigningKeyCollectionError::into_zcash)?;
+ Ok(keys)
+}
+
+#[cfg(feature = "cypherpunk")]
+#[derive(Clone, Copy)]
+enum ShieldedPool {
+ Orchard,
+ #[cfg(zcash_unstable = "nu6.3")]
+ Ironwood,
+}
+
+#[cfg(feature = "cypherpunk")]
+impl ShieldedPool {
+ fn label(self) -> &'static str {
+ match self {
+ ShieldedPool::Orchard => "Orchard",
+ #[cfg(zcash_unstable = "nu6.3")]
+ ShieldedPool::Ironwood => "Ironwood",
+ }
+ }
+}
+
+#[cfg(feature = "cypherpunk")]
+fn collect_orchard_signing_keys(
+ pczt: &Pczt,
+ seed: &[u8],
+ pool: ShieldedPool,
+) -> Result<Vec<(usize, orchard::keys::SpendAuthorizingKey)>, ZcashError> {
+ let mut keys = Vec::new();
+
+ match pool {
+ ShieldedPool::Orchard => {
+ low_level_signer::Signer::new(pczt.clone())
+ .sign_orchard_with(|_pczt, bundle, _tx_modifiable| {
+ collect_orchard_bundle_signing_keys(&mut keys, seed, pool, bundle)
+ })
+ .map_err(SigningKeyCollectionError::into_zcash)?;
+ }
+ #[cfg(zcash_unstable = "nu6.3")]
+ ShieldedPool::Ironwood => {
+ if !super::pczt_should_process_ironwood(pczt) {
+ return Ok(keys);
+ }
+ low_level_signer::Signer::new(pczt.clone())
+ .sign_ironwood_with(|_pczt, bundle, _tx_modifiable| {
+ collect_orchard_bundle_signing_keys(&mut keys, seed, pool, bundle)
+ })
+ .map_err(SigningKeyCollectionError::into_zcash)?;
+ }
+ }
- Ok(signed_pczt.serialize())
+ Ok(keys)
}
-#[cfg(test)]
+#[cfg(feature = "cypherpunk")]
+fn collect_orchard_bundle_signing_keys(
+ keys: &mut Vec<(usize, orchard::keys::SpendAuthorizingKey)>,
+ seed: &[u8],
+ pool: ShieldedPool,
+ bundle: &mut orchard::pczt::Bundle,
+) -> Result<(), SigningKeyCollectionError> {
+ for (index, action) in bundle.actions().iter().enumerate() {
+ let pool_label = pool.label();
+ if action.spend().spend_auth_sig().is_some() {
+ continue;
+ }
+ if action.spend().dummy_sk().is_some() {
+ match action.spend().value().map(|value| value.inner()) {
+ Some(0) | None => continue,
+ Some(_) => {
+ return Err(ZcashError::InvalidPczt(format!(
+ "{pool_label} spend dummy_sk is only valid for dummy spends"
+ ))
+ .into());
+ }
+ }
+ }
+ if action.spend().value().is_none() {
+ continue;
+ }
+ if let Some(ask) = spend_authorizing_key_for_action(seed, action, pool_label)? {
+ keys.push((index, ask));
+ }
+ }
+ Ok(())
+}
+
+#[cfg(feature = "cypherpunk")]
+fn spend_authorizing_key_for_action(
+ seed: &[u8],
+ action: &orchard::pczt::Action,
+ pool_label: &str,
+) -> Result<Option<orchard::keys::SpendAuthorizingKey>, ZcashError> {
+ let fingerprint =
+ calculate_seed_fingerprint(seed).map_err(|e| ZcashError::SigningError(e.to_string()))?;
+ let Some(account_index) = super::matching_seed_supported_orchard_account(
+ &fingerprint,
+ action.spend().zip32_derivation().as_ref(),
+ 133,
+ pool_label,
+ )?
+ else {
+ return Ok(None);
+ };
+
+ let osk =
+ orchard::keys::SpendingKey::from_zip32_seed(seed, 133, account_index).map_err(|e| {
+ ZcashError::SigningError(format!("failed to derive {pool_label} spending key: {e:?}"))
+ })?;
+ Ok(Some(orchard::keys::SpendAuthorizingKey::from(&osk)))
+}
+
+#[cfg(all(test, feature = "cypherpunk"))]
mod tests {
use super::*;
+ fn assert_invalid_pczt_message<T: core::fmt::Debug>(result: crate::Result<T>, expected: &str) {
+ match result {
+ Err(ZcashError::InvalidPczt(message)) if message == expected => {}
+ other => panic!("unexpected InvalidPczt result: {other:?}"),
+ }
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ fn signable_sample_pczt() -> crate::pczt::test_support::SamplePczt {
+ crate::pczt::test_support::sample_ironwood_pczt()
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
#[test]
fn test_sign_pczt_invalid_seed_fingerprint() {
- // A valid PCZT hex string found in the codebase
- let pczt_hex = "50435a5401000000058ace9cb502d5a09cc70c0100f083ae0185010000000180ade2041976a91467f7aa14f177a7e0058c66c7242e086488bd3d1088ac000001237431544d4c4a376b324e344e6172716b3546643575556f38324e58534d624b5267436300000000fbc2f4300c01f0b7820d00e3347c8da4ee614674376cbc45359daa54f9b5493e010000000000000000000000000000000000000000000000000000000000000000024d2eeb083d7c168f64239c3186d53c72e2b1a3a5140f5250f0963689c08cd61c0999baea13f0be05dc6a2554bb2f8f093f4d20911202567a5ab9fd17bce5142b3f79838a71d14757fcff03ba16486a3efb26c9773ec9596821d1e5f32039fe220001d5d3506f152f62c45198446223abf29e06da700990a779fb60a460712fb666a0ff1fab61e2b2b3566b263d0180b6dc05014b2225d5521d6dbb55ae03d22567ce98b242ba5520bc4e2493ec36fb9211c6350194215c2aa089dfa317c61bab4b9747f4e45abca855e45e00710a3dc5caa40a570186f6f9e818f6674c2df92918a55d20f340944de5c67c1c4a9ee347c2c2d6d71d4753d765f2859a3157f7b05cc3bc7089e3f2c9d5abb3fcb1708e74c790985d3dd90cfe2ed03276dfda527c6e8c08d9a1fdeedcb6aef59d9e5bf0ae5d9477ed030001872727f23f40a96896b66d04de905791bae2bc7ee9dc1f4e4ec5ae493dc2fc1001afb475105f1f5b477c52aa3c32ccf131b0c556b80f55ac555460e6b5148bf85303a0808080088581808008808080800800002585b32c42aa5a12b2763953f09aafed13450eda0c416e32d0978260c4171c375413b91e25fa826399623b6716ae8bbb0b4a1099de22478944627af7e5969aa0c404ffab4d35664c1dafd2d2c0cecf4fb3c8b054179f84b2d35d207077b3d256b429acdee34963c573b55ae20fffce73e0e3e575c8fde9d115e7ffab50b3bee60d2436b72c17677e1d7db141fafa72c7f89002908a7a8de3320e5ad3d1ed0bb545235e136904c5c5e4adfa5a100420ceb2196e5e197e919aeaeefa7cb2a1d98e011539af52d618bfb3ba1dfc2d2c01e9bd67523bb6787eb5a0d28e30ad483c6303efd4796795082cc67ea94ba8548a33da1a5ec7c56174bd6b260f548e83a924b7cdd32980ca489b44e981aa1d81cefe2581eebf3a585fb80542aea4a27862f593203b560a412ba4e737c8f678f239f3d1d07c5a82367435f0a0921c46600eb4f6f7387b3cb5984af98b1337f5148ad6388b62dab7cdc48c66ff81685894c2d1d0fe41716b7cb457fb5bd6ff13e321d2f91c15d431f942d7869955dfeadfff61638266ba38d7ba4db7ffe5ee03550d345715cebd9b378181b5769c22e1b20328165da02eeb5d246c70c008ac0c7f7b1bba2cf8270f013eb99cbc5d534270180f34892fdf08d8c16c518d8b7f62d832d676c65fcae34c640ff30d5bd9d65afeab509117a98374b4b9b016228a65bdd803d6c601d2ad6a654c2fe4487d9c7b088d886c36a6afe63d33f8c474f096500acabbb63968e7408c620cc8139331cf7227e9bdbf4b7bae292e15d310e66186b730f28d0515ac5bb71fcc5de09995fe89d005cc2c7afd0fb8f01b315815d38366ebeb6de9ed565b5d1f2ce14b7795b9ad784851f357beacc454be41aaec506f0148461ba5907043ab8618114bbbede979d7f0e0e0af914750df648079e3625e4f309d13ff74d4ada783203bb3652137abd8327cdd06b9332591c9abdcc0cc16f7fec2e0afd849bef8927b3b0ceeca2b90af7611875b78cf525852ee83e10c8f4cb2c80045cbf33c0801a55eeb15c9dca6e53b3dde8a12daf820f1f76624ee48e3128aaa0ef6f6fb32a0303d89e88be288be1b92a301e893790179ec07711e275f48de2f5f8e0ee7b000091c9d96159746d46f353e67463d7052000000000118c5796d39cd2bc56b0a062c20ebd32feb0b57cc231c262d6703520f8de603211edcf51f6084e3288cbdb02957a02cd68fb84973a6a98260fb60f30951dedb2e1240275687c0bd82a2653a2c212bd3c0ea75cd294f5a4d31dcf507c15461402760282899f6b560858c0b6bd95c708f62d1e856480a52401d0d7d6a642fa1c2a10176072c6147735b785ea4ad9276378885704a44c6246f4630ef1df59438562e055bba6c1411a790727ab27421e6c418df8b65cb636d6786ce9e5b632659f5d32401caffe6271e2d77d8634e67a116926d7566b5eb2f2aadba6498d7a1e120f27f52379bb3f8781090ae47e30b0100011a78b2abbab21b29d79141fdff8a389c2eacde5be75c69ae4c4fabc175aec10a0142b202630def2df1f7cd23fcf362c68194829282c57b0c4d5f0ca023b51a571f01bd466676b53cfc27ba4a94bb4ab3ed19d8db336042e09e1e756b560b5ce7fc05d5dc3269236828f541662db5bfd4ab6e07c4dac2682906ee85eca2d12b6522013dd286fc499141cfebfb53175ea4321e08e8a504604bbc2e9d3e59706a1fa439000130febcd5d0c57c6e3780d6fe1f6c07f01a9d5d7a053ac5562f29304418d33a20000000f7fa16a612e422c34d61c44ae692b255c921239547172fcd26519928a3abb10d22548d840b466f1fed5ccb4c442d97b4b59d1a728455ee1598bae8e316f819bac404c9112693c57e0733d550ddc984d82ecc9047721e7e7bc6f283ba00852e49a4d3cda4dad343a366650b1d75b26025eadc5200113ebcc2a4a7db9ac2291083d76e7a8c04831764caf35e4c18bfc58e58699b4a651ca3686a95a6db7133611b5ce80a14225cdac643311869ea0c4a6d760379f285fa9c396c435361044da7e077f236d589a3eb962129988ea6ccde694cb72fa986748fc106981320f478a1c5402fe75a26dee31ec9fad4240aa19932fa8361c43798aa381c63b0c0b17657ccf37792a28456cfe6562e15d9e4aa26ed2660b6c8fc8a92cd352a6025dabcbed5eba82d88b9df3ba73270ff2f9c44fca8b0c1df8ed4cbfa2a4ebe7d0bcc6e5ce73e43b51e054860d7939ca13d77813b372070fd24cdd9c0e2fad7567471c0279bba19a76f0cdbd3107220821dd676c1df6524c15b87c1318eda418d65f8c66d2a77a65f6894199d44611e60c0291c330d1692bd521aef0e316e2b3f8c377b0d6873b3b645196ba74a79c6e0509869ac66276c3e2dfefd54a12365b5945406e7b673321ed36e89a14a194ae8b864e9ac4684655bae7fcd3123a226f282ac6ac82ca88d6a383d8be90f87f4cb85225f697932abfb4c05cda3b6dadb003621fee663f3fcb8f1c96320a3f148bc106ec231961a8f5142dd614317eef16b81492668a8b8795b85d7b0f737fa8d79e9dc3d78840d158a73dc6d1700ce3a8de2a9f93ff1bc8108703b94fd5bd230a19dd0fd821b832d3508b335e07bac28e95c3ab0eb637334bf166fa2a440ea35c0372bb5a745ee86c727a80f0d0d080fef6642ae7aae1407d6a25c3050c498a52ae300105bded1f19829b10df00e7ba301a9aef2c99ad7c5338b0e259ab97ea852630606b8d59709ca067d32698c8761e0f7d5b76ac07d4860b0fe2992010ba88827bb37cf4e3436488580e79101b366d454f29aa2bdf76725130baa08b38af3a71c251521809c84fe3d086943f39f01d760884b6342fac60c010001c54930d4f4f9946dfe91ac3e94cf5b513871c4a5c0c21137959482da796d2d280000000001c4666732084baff2e402ed7d3e457303c73b77dbd4aa5bc943ac7ca96f3779070398a2e304004aed48232c44dbd0b0b5404063ecc4679436f28c6251cbba91e29388fcd98d0e0001dc2be19f4118dbb7500df3a95e304733b247cea7f8c681f6aaafceb8fc1d7d28";
- let pczt_bytes = hex::decode(pczt_hex).unwrap();
- let pczt = Pczt::parse(&pczt_bytes).unwrap();
+ let sample = signable_sample_pczt();
+ let pczt = Pczt::parse(&sample.bytes).unwrap();
+ let mismatched_seed = [9u8; 32];
- // Random seed, should mismatch the seed fingerprint in PCZT
- let seed = hex::decode("d561f5aba9db8b100a9a84197322e522f952171a388ad74eaab1ab9db815be3335c3099a0a2bb0fee57e630db5ed7251412b6bd4b905cf518627411fee3f32dd").unwrap();
+ let result = sign_pczt(pczt, &mismatched_seed);
+ assert!(matches!(result, Err(ZcashError::PcztNoMyInputs)));
+ }
- // Should return a successfull result but with redacted information
- // In the current logic, if keys don't match, it returns Ok and does not sign, but redactor still runs.
- let result = sign_pczt(pczt, &seed);
- assert!(result.is_ok());
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_sign_pczt_ironwood_spend() {
+ let sample = crate::pczt::test_support::sample_ironwood_pczt();
+ let pczt = Pczt::parse(&sample.bytes).unwrap();
- let signed_pczt_bytes = result.unwrap();
- // Verify result is a valid PCZT
- let parsed = Pczt::parse(&signed_pczt_bytes).expect("signed PCZT must parse");
+ let base_sighash = RoleSigner::new(pczt.clone())
+ .expect("Ironwood PCZT signer should initialize")
+ .shielded_sighash();
+ let updated_anchor = orchard::Anchor::from_bytes([6u8; 32]).unwrap();
+ let updated_anchor_pczt = Updater::new(pczt.clone())
+ .set_v6_ironwood_anchor(updated_anchor)
+ .expect("v6 Ironwood anchor should be replaceable before proving")
+ .finish();
+ assert_ne!(
+ pczt.ironwood().anchor(),
+ updated_anchor_pczt.ironwood().anchor()
+ );
+ assert_eq!(
+ base_sighash,
+ RoleSigner::new(updated_anchor_pczt)
+ .expect("anchor-updated Ironwood PCZT signer should initialize")
+ .shielded_sighash(),
+ "v6 Ironwood spend signatures must not commit to the anchor"
+ );
- // Verify redaction occurred (size should be smaller as witness data is removed)
- // (The updater stamp adds only a few dozen bytes; redaction strips kilobytes.)
- assert!(signed_pczt_bytes.len() < pczt_bytes.len());
+ let signed_pczt_bytes = sign_pczt(pczt, &sample.seed).expect("Ironwood PCZT should sign");
+ let parsed = Pczt::parse(&signed_pczt_bytes).expect("signed PCZT must parse");
- // The firmware version stamp must be present in every signed response,
- // even when the request carried no explicit minimum version.
let stamp = parsed
.global()
.proprietary()
.get(PROP_KEY_FW_VERSION)
.expect("firmware version stamp must be present");
assert_eq!(stamp, &KEYSTONE_FW_VERSION.encode().to_vec());
+ assert!(
+ parsed
+ .ironwood()
+ .actions()
+ .iter()
+ .any(|action| action.spend().spend_auth_sig().is_some()),
+ "Ironwood spend authorization signature must be present",
+ );
+ for action in parsed.ironwood().actions().iter() {
+ if let Some(sig) = action.spend().spend_auth_sig() {
+ let rk = orchard::primitives::redpallas::VerificationKey::<
+ orchard::primitives::redpallas::SpendAuth,
+ >::try_from(*action.spend().rk())
+ .expect("Ironwood randomized validating key must parse");
+ let sig: orchard::primitives::redpallas::Signature<
+ orchard::primitives::redpallas::SpendAuth,
+ > = (*sig).into();
+
+ rk.verify(&base_sighash, &sig)
+ .expect("Ironwood spend authorization signature must match v6 sighash");
+ }
+ }
+ assert!(
+ signed_pczt_bytes.len() < sample.bytes.len(),
+ "signed response should be redacted",
+ );
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_sign_pczt_ironwood_spend_rejects_unsupported_zip32_path() {
+ let sample = crate::pczt::test_support::sample_ironwood_pczt();
+ for path in crate::pczt::test_support::unsupported_orchard_spend_paths() {
+ let pczt = crate::pczt::test_support::ironwood_pczt_with_spend_derivation(
+ &sample.bytes,
+ sample.seed_fingerprint,
+ path,
+ );
+
+ assert_invalid_pczt_message(
+ sign_pczt(Pczt::parse(&pczt).unwrap(), &sample.seed),
+ "unsupported Ironwood spend ZIP 32 derivation path",
+ );
+ }
}
- /// Reusable helper: parse the bundled test PCZT and inject a
- /// test proprietary entry at the global level to verify round-trip.
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_sign_pczt_ironwood_spend_ignores_dummy_zip32_metadata() {
+ let sample = crate::pczt::test_support::sample_ironwood_pczt();
+ let pczt = crate::pczt::test_support::ironwood_pczt_with_dummy_spend_derivation(
+ &sample.bytes,
+ sample.seed_fingerprint,
+ crate::pczt::test_support::orchard_spend_path_for_account(1),
+ );
+
+ let signed = sign_pczt(Pczt::parse(&pczt).unwrap(), &sample.seed)
+ .expect("dummy spend ZIP 32 metadata must not block signing real spends");
+ let parsed = Pczt::parse(&signed).expect("signed PCZT must parse");
+ assert!(
+ parsed
+ .ironwood()
+ .actions()
+ .iter()
+ .any(|action| action.spend().spend_auth_sig().is_some()),
+ "Ironwood spend authorization signature must be present",
+ );
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn test_sign_pczt_orchard_change_output_spend() {
+ let sample = crate::pczt::test_support::sample_orchard_change_pczt();
+ let pczt = Pczt::parse(&sample.bytes).unwrap();
+
+ let signed =
+ sign_pczt(pczt, &sample.seed).expect("Orchard change output spend should sign");
+ let parsed = Pczt::parse(&signed).expect("signed PCZT must parse");
+ let signed_actions = parsed
+ .orchard()
+ .actions()
+ .iter()
+ .filter(|action| action.spend().spend_auth_sig().is_some())
+ .count();
+ assert_eq!(
+ signed_actions, 2,
+ "real spend and wallet controlled zero value spend must be signed",
+ );
+ }
+
+ #[cfg(zcash_unstable = "nu6.3")]
fn pczt_with_min_version(min_version: &[u8]) -> Pczt {
- // Same real Orchard→transparent PCZT used by `test_sign_pczt_invalid_seed_fingerprint`.
- let pczt_hex = "50435a5401000000058ace9cb502d5a09cc70c0100f083ae0185010000000180ade2041976a91467f7aa14f177a7e0058c66c7242e086488bd3d1088ac000001237431544d4c4a376b324e344e6172716b3546643575556f38324e58534d624b5267436300000000fbc2f4300c01f0b7820d00e3347c8da4ee614674376cbc45359daa54f9b5493e010000000000000000000000000000000000000000000000000000000000000000024d2eeb083d7c168f64239c3186d53c72e2b1a3a5140f5250f0963689c08cd61c0999baea13f0be05dc6a2554bb2f8f093f4d20911202567a5ab9fd17bce5142b3f79838a71d14757fcff03ba16486a3efb26c9773ec9596821d1e5f32039fe220001d5d3506f152f62c45198446223abf29e06da700990a779fb60a460712fb666a0ff1fab61e2b2b3566b263d0180b6dc05014b2225d5521d6dbb55ae03d22567ce98b242ba5520bc4e2493ec36fb9211c6350194215c2aa089dfa317c61bab4b9747f4e45abca855e45e00710a3dc5caa40a570186f6f9e818f6674c2df92918a55d20f340944de5c67c1c4a9ee347c2c2d6d71d4753d765f2859a3157f7b05cc3bc7089e3f2c9d5abb3fcb1708e74c790985d3dd90cfe2ed03276dfda527c6e8c08d9a1fdeedcb6aef59d9e5bf0ae5d9477ed030001872727f23f40a96896b66d04de905791bae2bc7ee9dc1f4e4ec5ae493dc2fc1001afb475105f1f5b477c52aa3c32ccf131b0c556b80f55ac555460e6b5148bf85303a0808080088581808008808080800800002585b32c42aa5a12b2763953f09aafed13450eda0c416e32d0978260c4171c375413b91e25fa826399623b6716ae8bbb0b4a1099de22478944627af7e5969aa0c404ffab4d35664c1dafd2d2c0cecf4fb3c8b054179f84b2d35d207077b3d256b429acdee34963c573b55ae20fffce73e0e3e575c8fde9d115e7ffab50b3bee60d2436b72c17677e1d7db141fafa72c7f89002908a7a8de3320e5ad3d1ed0bb545235e136904c5c5e4adfa5a100420ceb2196e5e197e919aeaeefa7cb2a1d98e011539af52d618bfb3ba1dfc2d2c01e9bd67523bb6787eb5a0d28e30ad483c6303efd4796795082cc67ea94ba8548a33da1a5ec7c56174bd6b260f548e83a924b7cdd32980ca489b44e981aa1d81cefe2581eebf3a585fb80542aea4a27862f593203b560a412ba4e737c8f678f239f3d1d07c5a82367435f0a0921c46600eb4f6f7387b3cb5984af98b1337f5148ad6388b62dab7cdc48c66ff81685894c2d1d0fe41716b7cb457fb5bd6ff13e321d2f91c15d431f942d7869955dfeadfff61638266ba38d7ba4db7ffe5ee03550d345715cebd9b378181b5769c22e1b20328165da02eeb5d246c70c008ac0c7f7b1bba2cf8270f013eb99cbc5d534270180f34892fdf08d8c16c518d8b7f62d832d676c65fcae34c640ff30d5bd9d65afeab509117a98374b4b9b016228a65bdd803d6c601d2ad6a654c2fe4487d9c7b088d886c36a6afe63d33f8c474f096500acabbb63968e7408c620cc8139331cf7227e9bdbf4b7bae292e15d310e66186b730f28d0515ac5bb71fcc5de09995fe89d005cc2c7afd0fb8f01b315815d38366ebeb6de9ed565b5d1f2ce14b7795b9ad784851f357beacc454be41aaec506f0148461ba5907043ab8618114bbbede979d7f0e0e0af914750df648079e3625e4f309d13ff74d4ada783203bb3652137abd8327cdd06b9332591c9abdcc0cc16f7fec2e0afd849bef8927b3b0ceeca2b90af7611875b78cf525852ee83e10c8f4cb2c80045cbf33c0801a55eeb15c9dca6e53b3dde8a12daf820f1f76624ee48e3128aaa0ef6f6fb32a0303d89e88be288be1b92a301e893790179ec07711e275f48de2f5f8e0ee7b000091c9d96159746d46f353e67463d7052000000000118c5796d39cd2bc56b0a062c20ebd32feb0b57cc231c262d6703520f8de603211edcf51f6084e3288cbdb02957a02cd68fb84973a6a98260fb60f30951dedb2e1240275687c0bd82a2653a2c212bd3c0ea75cd294f5a4d31dcf507c15461402760282899f6b560858c0b6bd95c708f62d1e856480a52401d0d7d6a642fa1c2a10176072c6147735b785ea4ad9276378885704a44c6246f4630ef1df59438562e055bba6c1411a790727ab27421e6c418df8b65cb636d6786ce9e5b632659f5d32401caffe6271e2d77d8634e67a116926d7566b5eb2f2aadba6498d7a1e120f27f52379bb3f8781090ae47e30b0100011a78b2abbab21b29d79141fdff8a389c2eacde5be75c69ae4c4fabc175aec10a0142b202630def2df1f7cd23fcf362c68194829282c57b0c4d5f0ca023b51a571f01bd466676b53cfc27ba4a94bb4ab3ed19d8db336042e09e1e756b560b5ce7fc05d5dc3269236828f541662db5bfd4ab6e07c4dac2682906ee85eca2d12b6522013dd286fc499141cfebfb53175ea4321e08e8a504604bbc2e9d3e59706a1fa439000130febcd5d0c57c6e3780d6fe1f6c07f01a9d5d7a053ac5562f29304418d33a20000000f7fa16a612e422c34d61c44ae692b255c921239547172fcd26519928a3abb10d22548d840b466f1fed5ccb4c442d97b4b59d1a728455ee1598bae8e316f819bac404c9112693c57e0733d550ddc984d82ecc9047721e7e7bc6f283ba00852e49a4d3cda4dad343a366650b1d75b26025eadc5200113ebcc2a4a7db9ac2291083d76e7a8c04831764caf35e4c18bfc58e58699b4a651ca3686a95a6db7133611b5ce80a14225cdac643311869ea0c4a6d760379f285fa9c396c435361044da7e077f236d589a3eb962129988ea6ccde694cb72fa986748fc106981320f478a1c5402fe75a26dee31ec9fad4240aa19932fa8361c43798aa381c63b0c0b17657ccf37792a28456cfe6562e15d9e4aa26ed2660b6c8fc8a92cd352a6025dabcbed5eba82d88b9df3ba73270ff2f9c44fca8b0c1df8ed4cbfa2a4ebe7d0bcc6e5ce73e43b51e054860d7939ca13d77813b372070fd24cdd9c0e2fad7567471c0279bba19a76f0cdbd3107220821dd676c1df6524c15b87c1318eda418d65f8c66d2a77a65f6894199d44611e60c0291c330d1692bd521aef0e316e2b3f8c377b0d6873b3b645196ba74a79c6e0509869ac66276c3e2dfefd54a12365b5945406e7b673321ed36e89a14a194ae8b864e9ac4684655bae7fcd3123a226f282ac6ac82ca88d6a383d8be90f87f4cb85225f697932abfb4c05cda3b6dadb003621fee663f3fcb8f1c96320a3f148bc106ec231961a8f5142dd614317eef16b81492668a8b8795b85d7b0f737fa8d79e9dc3d78840d158a73dc6d1700ce3a8de2a9f93ff1bc8108703b94fd5bd230a19dd0fd821b832d3508b335e07bac28e95c3ab0eb637334bf166fa2a440ea35c0372bb5a745ee86c727a80f0d0d080fef6642ae7aae1407d6a25c3050c498a52ae300105bded1f19829b10df00e7ba301a9aef2c99ad7c5338b0e259ab97ea852630606b8d59709ca067d32698c8761e0f7d5b76ac07d4860b0fe2992010ba88827bb37cf4e3436488580e79101b366d454f29aa2bdf76725130baa08b38af3a71c251521809c84fe3d086943f39f01d760884b6342fac60c010001c54930d4f4f9946dfe91ac3e94cf5b513871c4a5c0c21137959482da796d2d280000000001c4666732084baff2e402ed7d3e457303c73b77dbd4aa5bc943ac7ca96f3779070398a2e304004aed48232c44dbd0b0b5404063ecc4679436f28c6251cbba91e29388fcd98d0e0001dc2be19f4118dbb7500df3a95e304733b247cea7f8c681f6aaafceb8fc1d7d28";
- let pczt_bytes = hex::decode(pczt_hex).unwrap();
- let base = Pczt::parse(&pczt_bytes).unwrap();
+ let sample = signable_sample_pczt();
+ let base = Pczt::parse(&sample.bytes).unwrap();
let min_version = min_version.to_vec();
Updater::new(base)
.update_global_with(|mut g| {
@@ -256,13 +622,14 @@ mod tests {
.finish()
}
+ #[cfg(zcash_unstable = "nu6.3")]
fn test_seed() -> Vec<u8> {
- hex::decode("d561f5aba9db8b100a9a84197322e522f952171a388ad74eaab1ab9db815be3335c3099a0a2bb0fee57e630db5ed7251412b6bd4b905cf518627411fee3f32dd").unwrap()
+ [7u8; 32].to_vec()
}
+ #[cfg(zcash_unstable = "nu6.3")]
#[test]
fn firmware_equal_version_stamps_response() {
- // Demand the exact running version. Must pass and stamp the response.
let pczt = pczt_with_min_version(&KEYSTONE_FW_VERSION.encode());
let signed = sign_pczt(pczt, &test_seed()).expect("equal-version PCZT should sign");
let parsed = Pczt::parse(&signed).expect("signed PCZT must parse");
@@ -274,8 +641,6 @@ mod tests {
.expect("firmware version stamp must be present");
assert_eq!(stamp, &KEYSTONE_FW_VERSION.encode().to_vec());
- // The min-version request key the wallet set should survive unchanged
- // so the wallet can correlate response to request if it wants to.
let request_min = parsed
.global()
.proprietary()
@@ -284,9 +649,9 @@ mod tests {
assert_eq!(request_min, &KEYSTONE_FW_VERSION.encode().to_vec());
}
+ #[cfg(zcash_unstable = "nu6.3")]
#[test]
fn firmware_older_min_version_still_stamps_response() {
- // Wallet demands 1.0.0 — older than firmware. Must pass and stamp.
let pczt = pczt_with_min_version(&[1, 0, 0]);
let signed = sign_pczt(pczt, &test_seed()).expect("older-min PCZT should sign");
let parsed = Pczt::parse(&signed).expect("signed PCZT must parse");
@@ -299,14 +664,12 @@ mod tests {
assert_eq!(stamp, &KEYSTONE_FW_VERSION.encode().to_vec());
}
+ #[cfg(zcash_unstable = "nu6.3")]
#[test]
fn malformed_min_version_round_trips_and_stamps() {
- // The wallet is the authority on min-version; firmware does not
- // validate the shape of wallet-set proprietary keys on the way in.
- // A short/malformed value from the wallet should still round-trip,
- // and the response must still carry the firmware stamp.
let pczt = pczt_with_min_version(&[1, 2]);
- let signed = sign_pczt(pczt, &test_seed()).expect("malformed min bytes must not block signing");
+ let signed =
+ sign_pczt(pczt, &test_seed()).expect("malformed min bytes must not block signing");
let parsed = Pczt::parse(&signed).expect("signed PCZT must parse");
let stamp = parsed
@@ -324,3 +687,34 @@ mod tests {
assert_eq!(request_min.as_slice(), &[1u8, 2][..]);
}
}
+
+#[cfg(all(test, feature = "multi_coins", not(feature = "cypherpunk")))]
+mod legacy_tests {
+ use super::*;
+ use zcash_vendor::{
+ pczt::roles::creator::Creator,
+ zcash_protocol::consensus::{BranchId, MainNetwork, NetworkConstants},
+ };
+
+ #[cfg(zcash_unstable = "nu6.3")]
+ #[test]
+ fn legacy_signing_rejects_v6_pczt() {
+ let pczt = Creator::new_v6(
+ BranchId::Nu6_3.into(),
+ 10,
+ MainNetwork.coin_type(),
+ [0; 32],
+ [0; 32],
+ [1; 32],
+ )
+ .build();
+
+ let result = sign_pczt(pczt, &[7u8; 32]);
+
+ assert!(matches!(
+ result,
+ Err(ZcashError::SigningError(msg))
+ if msg == "V6 or Ironwood PCZTs require cypherpunk signing support"
+ ));
+ }
+}
diff --git a/rust/apps/zcash/src/pczt/structs.rs b/rust/apps/zcash/src/pczt/structs.rs
index 0bb3742..51d5b3a 100644
--- a/rust/apps/zcash/src/pczt/structs.rs
+++ b/rust/apps/zcash/src/pczt/structs.rs
@@ -4,6 +4,7 @@ use app_utils::impl_public_struct;
impl_public_struct!(ParsedPczt {
transparent: Option<ParsedTransparent>,
orchard: Option<ParsedOrchard>,
+ ironwood: Option<ParsedOrchard>,
total_transfer_value: String,
fee_value: String,
has_sapling: bool
@@ -64,6 +65,7 @@ mod tests {
#[test]
fn test_parsed_pczt_creation() {
let pczt = ParsedPczt::new(
+ None,
None,
None,
"1.0 ZEC".to_string(),
@@ -214,6 +216,7 @@ mod tests {
let pczt = ParsedPczt::new(
Some(transparent),
Some(orchard),
+ None,
"0.5 ZEC".to_string(),
"0.1 ZEC".to_string(),
false,
@@ -227,6 +230,7 @@ mod tests {
#[test]
fn test_parsed_pczt_with_sapling() {
let pczt = ParsedPczt::new(
+ None,
None,
None,
"5.0 ZEC".to_string(),
@@ -402,7 +406,7 @@ mod tests {
#[test]
fn test_parsed_pczt_empty_values() {
- let pczt = ParsedPczt::new(None, None, "".to_string(), "".to_string(), false);
+ let pczt = ParsedPczt::new(None, None, None, "".to_string(), "".to_string(), false);
assert_eq!(pczt.get_total_transfer_value(), "");
assert_eq!(pczt.get_fee_value(), "");
}
@@ -507,6 +511,7 @@ mod tests {
let pczt = ParsedPczt::new(
Some(transparent),
Some(orchard),
+ None,
"6.0 ZEC".to_string(),
"0.1 ZEC".to_string(),
true,
@@ -540,6 +545,7 @@ mod tests {
let pczt = ParsedPczt::new(
Some(transparent),
None,
+ None,
"1.0 ZEC".to_string(),
"0.0001 ZEC".to_string(),
false,
@@ -563,6 +569,7 @@ mod tests {
let pczt = ParsedPczt::new(
None,
Some(orchard),
+ None,
"2.0 ZEC".to_string(),
"0.0001 ZEC".to_string(),
false,
diff --git a/rust/rust_c/src/zcash/structs.rs b/rust/rust_c/src/zcash/structs.rs
index d440151..0a0cf6f 100644
--- a/rust/rust_c/src/zcash/structs.rs
+++ b/rust/rust_c/src/zcash/structs.rs
@@ -6,7 +6,7 @@ use crate::common::{
types::{Ptr, PtrString},
utils::convert_c_char,
};
-use crate::{free_ptr_with_type, free_str_ptr, free_vec, impl_c_ptr, impl_c_ptrs};
+use crate::{free_str_ptr, free_vec, impl_c_ptr, impl_c_ptrs};
use alloc::vec::Vec;
use app_zcash::pczt::structs::{
ParsedFrom, ParsedOrchard, ParsedPczt, ParsedTo, ParsedTransparent,
@@ -17,6 +17,7 @@ use cstr_core;
pub struct DisplayPczt {
pub transparent: Ptr<DisplayTransparent>,
pub orchard: Ptr<DisplayOrchard>,
+ pub ironwood: Ptr<DisplayOrchard>,
pub total_transfer_value: PtrString,
pub fee_value: PtrString,
pub has_sapling: bool,
@@ -33,6 +34,10 @@ impl From<&ParsedPczt> for DisplayPczt {
.get_orchard()
.map(|o| DisplayOrchard::from(&o).c_ptr())
.unwrap_or(null_mut()),
+ ironwood: pczt
+ .get_ironwood()
+ .map(|o| DisplayOrchard::from(&o).c_ptr())
+ .unwrap_or(null_mut()),
total_transfer_value: convert_c_char(pczt.get_total_transfer_value()),
fee_value: convert_c_char(pczt.get_fee_value()),
has_sapling: pczt.get_has_sapling(),
@@ -43,9 +48,20 @@ impl From<&ParsedPczt> for DisplayPczt {
impl Free for DisplayPczt {
unsafe fn free(&self) {
free_str_ptr!(self.total_transfer_value);
- free_ptr_with_type!(self.transparent, DisplayTransparent);
- free_ptr_with_type!(self.orchard, DisplayOrchard);
+ free_str_ptr!(self.fee_value);
+ free_display_ptr(self.transparent);
+ free_display_ptr(self.orchard);
+ free_display_ptr(self.ironwood);
+ }
+}
+
+unsafe fn free_display_ptr<T: Free>(ptr: Ptr<T>) {
+ if ptr.is_null() {
+ return;
}
+
+ let boxed = alloc::boxed::Box::from_raw(ptr);
+ boxed.free();
}
#[repr(C)]
diff --git a/src/ui/gui_chain/multi/gui_zcash.c b/src/ui/gui_chain/multi/gui_zcash.c
index f55027d..1a165ba 100644
--- a/src/ui/gui_chain/multi/gui_zcash.c
+++ b/src/ui/gui_chain/multi/gui_zcash.c
@@ -85,6 +85,10 @@ void GuiZcashOverview(lv_obj_t *parent, void *totalData)
if (g_zcashData->orchard != NULL) {
last_view = GuiZcashOverviewShielded(container, last_view, g_zcashData->orchard, _("Orchard"));
}
+
+ if (g_zcashData->ironwood != NULL) {
+ last_view = GuiZcashOverviewShielded(container, last_view, g_zcashData->ironwood, _("Ironwood"));
+ }
}
static lv_obj_t* GuiZcashOverviewTransparent(lv_obj_t *parent, lv_obj_t *last_view)
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.