chore(zcash): repin pczt stack to the upstream 2557 teststack
What changed, and why it matters
This commit updates the Zcash PCZT (Partially Created Zcash Transaction) library stack to a newer upstream test revision. It is a maintenance/dependency bump that adapts the firmware's code to new library APIs, adds error handling around serialization, and adjusts how Orchard/Ironwood transaction fields are handled for transaction signing. There is no direct evidence in the commit that this fixes a known security vulnerability; it appears to be a routine dependency repin to keep up with an upstream test stack.
Treat as a routine dependency maintenance commit. Review the upstream changelog for the pinned revisions (valargroup/librustzcash 94d6e7fd and zcash/orchard 475ef0ff) to confirm whether any security fixes are included. Validate that the new serialization error paths are exercised in tests and that the zero-fallback sighash helpers cannot be reached with malformed real-world PCZTs.
Security signals we found
Dependency repin to newer pre-release upstream Zcash stack
Serialization now returns Result and errors are propagated instead of panicking
Optional PCZT v2 fields (cv_net, anchor, enc_ciphertext) now have fallback handling in sighash computation
Test fixtures updated to avoid relying on crate-private Orchard bundle layouts
No explicit security bug or CVE mentioned in commit message or diff
Evidence from the diff
The commit repins the Zcash PCZT dependencies from valargroup/librustzcash rev 644b1ecf to 94d6e7fd, and orchard from crates.io 0.15.0-pre.1 to zcash/orchard rev 475ef0ff. Code changes propagate new upstream API shapes: pczt::orchard::BundleParseError becomes pczt::roles::low_level_signer::OrchardParseError; Creator::new_v6 is replaced by Creator::new with BranchId::Nu6_3; .serialize() now returns Result and is unwrapped/mapped to errors; test fixtures switch from a full PcztMirror to a PcztHead plus raw trailing bytes to accommodate the v2 PCZT wire layout. In pczt_ext.rs, helper functions action_cv_net and action_enc_ciphertext are added to handle new optional fields in the v2 wire model, with zero fallbacks documented as unreachable in the signing path. The digest functions now treat cv_net and anchor as optional and use empty fallbacks. The commit also adds orchard_pool_bundle_type: orchard::builder::BundleType::DEFAULT to builder configs.
Changed components
rust/Cargo.lockrust/Cargo.tomlrust/apps/zcashrust/zcash_vendorZcash PCZT signing and parsing modulesOrchard/Ironwood shielded transaction handlingInspect captured patch +192 / −97
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index a3e5329..20d3346 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -484,6 +484,7 @@ dependencies = [
"hex",
"incrementalmerkletree",
"keystore",
+ "orchard",
"pczt",
"postcard",
"rand_core 0.6.4",
@@ -1676,7 +1677,7 @@ checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
[[package]]
name = "equihash"
version = "0.3.0"
-source = "git+https://github.com/valargroup/librustzcash?rev=644b1ecf4679e17a9166782f57a795a4f4194d5e#644b1ecf4679e17a9166782f57a795a4f4194d5e"
+source = "git+https://github.com/valargroup/librustzcash?rev=94d6e7fd8c76abb1d909390e10254a22f587981f#94d6e7fd8c76abb1d909390e10254a22f587981f"
dependencies = [
"blake2b_simd",
"corez",
@@ -1803,7 +1804,7 @@ dependencies = [
[[package]]
name = "f4jumble"
version = "0.1.1"
-source = "git+https://github.com/valargroup/librustzcash?rev=644b1ecf4679e17a9166782f57a795a4f4194d5e#644b1ecf4679e17a9166782f57a795a4f4194d5e"
+source = "git+https://github.com/valargroup/librustzcash?rev=94d6e7fd8c76abb1d909390e10254a22f587981f#94d6e7fd8c76abb1d909390e10254a22f587981f"
dependencies = [
"blake2b_simd",
]
@@ -3039,9 +3040,8 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "orchard"
-version = "0.15.0-pre.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8e277dd4b46f5d06deae3ffb8af1a951e8622368f028c2a4d6fe59339566403"
+version = "0.15.0-pre.2"
+source = "git+https://github.com/zcash/orchard?rev=475ef0ff77d45aebff93cb039d639250d82518a3#475ef0ff77d45aebff93cb039d639250d82518a3"
dependencies = [
"aes",
"bitvec",
@@ -3142,7 +3142,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pczt"
version = "0.7.0"
-source = "git+https://github.com/valargroup/librustzcash?rev=644b1ecf4679e17a9166782f57a795a4f4194d5e#644b1ecf4679e17a9166782f57a795a4f4194d5e"
+source = "git+https://github.com/valargroup/librustzcash?rev=94d6e7fd8c76abb1d909390e10254a22f587981f#94d6e7fd8c76abb1d909390e10254a22f587981f"
dependencies = [
"blake2b_simd",
"bls12_381",
@@ -5409,13 +5409,13 @@ dependencies = [
[[package]]
name = "zcash_address"
-version = "0.12.0"
-source = "git+https://github.com/valargroup/librustzcash?rev=644b1ecf4679e17a9166782f57a795a4f4194d5e#644b1ecf4679e17a9166782f57a795a4f4194d5e"
+version = "0.13.0-pre.0"
+source = "git+https://github.com/valargroup/librustzcash?rev=94d6e7fd8c76abb1d909390e10254a22f587981f#94d6e7fd8c76abb1d909390e10254a22f587981f"
dependencies = [
"bech32 0.11.0",
"bs58",
"corez",
- "f4jumble 0.1.1 (git+https://github.com/valargroup/librustzcash?rev=644b1ecf4679e17a9166782f57a795a4f4194d5e)",
+ "f4jumble 0.1.1 (git+https://github.com/valargroup/librustzcash?rev=94d6e7fd8c76abb1d909390e10254a22f587981f)",
"zcash_encoding",
"zcash_protocol",
]
@@ -5423,7 +5423,7 @@ dependencies = [
[[package]]
name = "zcash_encoding"
version = "0.4.0"
-source = "git+https://github.com/valargroup/librustzcash?rev=644b1ecf4679e17a9166782f57a795a4f4194d5e#644b1ecf4679e17a9166782f57a795a4f4194d5e"
+source = "git+https://github.com/valargroup/librustzcash?rev=94d6e7fd8c76abb1d909390e10254a22f587981f#94d6e7fd8c76abb1d909390e10254a22f587981f"
dependencies = [
"corez",
"hex",
@@ -5432,8 +5432,8 @@ dependencies = [
[[package]]
name = "zcash_keys"
-version = "0.14.0"
-source = "git+https://github.com/valargroup/librustzcash?rev=644b1ecf4679e17a9166782f57a795a4f4194d5e#644b1ecf4679e17a9166782f57a795a4f4194d5e"
+version = "0.15.0-pre.0"
+source = "git+https://github.com/valargroup/librustzcash?rev=94d6e7fd8c76abb1d909390e10254a22f587981f#94d6e7fd8c76abb1d909390e10254a22f587981f"
dependencies = [
"bech32 0.11.0",
"bip32",
@@ -5471,8 +5471,8 @@ dependencies = [
[[package]]
name = "zcash_primitives"
-version = "0.28.0"
-source = "git+https://github.com/valargroup/librustzcash?rev=644b1ecf4679e17a9166782f57a795a4f4194d5e#644b1ecf4679e17a9166782f57a795a4f4194d5e"
+version = "0.29.0-pre.0"
+source = "git+https://github.com/valargroup/librustzcash?rev=94d6e7fd8c76abb1d909390e10254a22f587981f#94d6e7fd8c76abb1d909390e10254a22f587981f"
dependencies = [
"blake2b_simd",
"block-buffer 0.11.0-rc.3",
@@ -5501,8 +5501,8 @@ dependencies = [
[[package]]
name = "zcash_protocol"
-version = "0.9.0"
-source = "git+https://github.com/valargroup/librustzcash?rev=644b1ecf4679e17a9166782f57a795a4f4194d5e#644b1ecf4679e17a9166782f57a795a4f4194d5e"
+version = "0.10.0-pre.0"
+source = "git+https://github.com/valargroup/librustzcash?rev=94d6e7fd8c76abb1d909390e10254a22f587981f#94d6e7fd8c76abb1d909390e10254a22f587981f"
dependencies = [
"corez",
"hex",
@@ -5540,8 +5540,8 @@ dependencies = [
[[package]]
name = "zcash_transparent"
-version = "0.8.0"
-source = "git+https://github.com/valargroup/librustzcash?rev=644b1ecf4679e17a9166782f57a795a4f4194d5e#644b1ecf4679e17a9166782f57a795a4f4194d5e"
+version = "0.9.0-pre.0"
+source = "git+https://github.com/valargroup/librustzcash?rev=94d6e7fd8c76abb1d909390e10254a22f587981f#94d6e7fd8c76abb1d909390e10254a22f587981f"
dependencies = [
"bip32",
"bs58",
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index f351571..49763da 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -122,12 +122,16 @@ zeroize = { version = "1.8.2", default-features = false }
# third party dependencies end
[patch.crates-io]
-pczt = { git = "https://github.com/valargroup/librustzcash", rev = "644b1ecf4679e17a9166782f57a795a4f4194d5e" }
-zcash_address = { git = "https://github.com/valargroup/librustzcash", rev = "644b1ecf4679e17a9166782f57a795a4f4194d5e" }
-zcash_encoding = { git = "https://github.com/valargroup/librustzcash", rev = "644b1ecf4679e17a9166782f57a795a4f4194d5e" }
-zcash_keys = { git = "https://github.com/valargroup/librustzcash", rev = "644b1ecf4679e17a9166782f57a795a4f4194d5e" }
-zcash_primitives = { git = "https://github.com/valargroup/librustzcash", rev = "644b1ecf4679e17a9166782f57a795a4f4194d5e" }
-zcash_protocol = { git = "https://github.com/valargroup/librustzcash", rev = "644b1ecf4679e17a9166782f57a795a4f4194d5e" }
-zcash_transparent = { git = "https://github.com/valargroup/librustzcash", rev = "644b1ecf4679e17a9166782f57a795a4f4194d5e" }
+pczt = { git = "https://github.com/valargroup/librustzcash", rev = "94d6e7fd8c76abb1d909390e10254a22f587981f" }
+zcash_address = { git = "https://github.com/valargroup/librustzcash", rev = "94d6e7fd8c76abb1d909390e10254a22f587981f" }
+zcash_encoding = { git = "https://github.com/valargroup/librustzcash", rev = "94d6e7fd8c76abb1d909390e10254a22f587981f" }
+zcash_keys = { git = "https://github.com/valargroup/librustzcash", rev = "94d6e7fd8c76abb1d909390e10254a22f587981f" }
+zcash_primitives = { git = "https://github.com/valargroup/librustzcash", rev = "94d6e7fd8c76abb1d909390e10254a22f587981f" }
+zcash_protocol = { git = "https://github.com/valargroup/librustzcash", rev = "94d6e7fd8c76abb1d909390e10254a22f587981f" }
+zcash_transparent = { git = "https://github.com/valargroup/librustzcash", rev = "94d6e7fd8c76abb1d909390e10254a22f587981f" }
+# Redirect every crates.io `orchard` requirement onto the merged
+# unpadded-bundles head so the graph resolves to a single orchard node,
+# matching the pczt pin.
+orchard = { git = "https://github.com/zcash/orchard", rev = "475ef0ff77d45aebff93cb039d639250d82518a3" }
# Use the upstream SDK rev with the Zcash batch registry types until they are published as a crate.
ur-registry = { git = "https://github.com/KeystoneHQ/keystone-sdk-rust.git", rev = "0884de4b2e927bc3f95a98dff62045e0d492e574" }
diff --git a/rust/apps/zcash/Cargo.toml b/rust/apps/zcash/Cargo.toml
index 59ed83c..221e3f6 100644
--- a/rust/apps/zcash/Cargo.toml
+++ b/rust/apps/zcash/Cargo.toml
@@ -27,9 +27,13 @@ serde_with = { version = "3.11.0", features = [
[dev-dependencies]
keystore = { path = "../../keystore" }
pczt = { version = "0.7", default-features = false, features = ["orchard", "sapling", "transparent", "zcp-builder"] }
-zcash_primitives = { version = "0.28", default-features = false, features = ["circuits", "test-dependencies", "transparent-inputs"] }
+zcash_primitives = { version = "0.29.0-pre.0", default-features = false, features = ["circuits", "test-dependencies", "transparent-inputs"] }
incrementalmerkletree = { version = "0.8.2", default-features = false }
shardtree = "0.6.2"
+# Nameable only so the transparent-only `legacy_test_support` can spell the
+# `BuildConfig::orchard_pool_bundle_type` value; orchard is already built transitively
+# via the `zcash_primitives` dev-dependency.
+orchard = { version = "0.15.0-pre.2", default-features = false }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
diff --git a/rust/apps/zcash/src/errors.rs b/rust/apps/zcash/src/errors.rs
index cc24297..dc0e418 100644
--- a/rust/apps/zcash/src/errors.rs
+++ b/rust/apps/zcash/src/errors.rs
@@ -30,8 +30,8 @@ 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 {
+impl From<zcash_vendor::pczt::roles::low_level_signer::OrchardParseError> for ZcashError {
+ fn from(e: zcash_vendor::pczt::roles::low_level_signer::OrchardParseError) -> Self {
Self::InvalidPczt(alloc::format!("Invalid Orchard bundle: {e:?}"))
}
}
diff --git a/rust/apps/zcash/src/lib.rs b/rust/apps/zcash/src/lib.rs
index a983518..38fdfd6 100644
--- a/rust/apps/zcash/src/lib.rs
+++ b/rust/apps/zcash/src/lib.rs
@@ -76,7 +76,8 @@ pub fn check_pczt_cypherpunk<P: consensus::Parameters>(
// FUTURE(omitted-field-recompute): recompute-or-check omitted fields here,
// mutating `pczt` so the normalized bytes carry the verified values forward.
check_parsed_pczt_cypherpunk(params, &pczt, ufvk_text, seed_fingerprint, account_index)?;
- Ok(pczt.serialize())
+ pczt.serialize()
+ .map_err(|e| ZcashError::InvalidPczt(alloc::format!("serialize normalized PCZT: {e:?}")))
}
/// `check_pczt_cypherpunk` against an already-parsed PCZT, so callers can parse
@@ -136,7 +137,8 @@ pub fn preflight_batch_pczt_cypherpunk<P: consensus::Parameters>(
if actions.is_empty() {
return Err(ZcashError::PcztNoMyInputs);
}
- Ok(pczt.serialize())
+ pczt.serialize()
+ .map_err(|e| ZcashError::InvalidPczt(alloc::format!("serialize normalized PCZT: {e:?}")))
}
/// Parses a multi-coins PCZT, checks policy, and serializes it again in one
@@ -157,7 +159,8 @@ pub fn check_pczt_multi_coins<P: consensus::Parameters>(
// FUTURE(omitted-field-recompute): recompute-or-check omitted fields here,
// mutating `pczt` so the normalized bytes carry the verified values forward.
check_parsed_pczt_multi_coins(params, &pczt, xpub, seed_fingerprint, account_index)?;
- Ok(pczt.serialize())
+ pczt.serialize()
+ .map_err(|e| ZcashError::InvalidPczt(alloc::format!("serialize normalized PCZT: {e:?}")))
}
/// `check_pczt_multi_coins` against an already-parsed PCZT, so callers can
@@ -385,19 +388,22 @@ mod legacy_tests {
#[cfg(zcash_unstable = "nu6.3")]
#[test]
fn legacy_check_rejects_v6_pczt() {
- let pczt = Creator::new_v6(
+ // `Creator::new(Nu6_3, ..)` alone yields a V6 PCZT (tx_version V6); the legacy
+ // path rejects it on version, so no Ironwood anchor is needed (and
+ // `with_ironwood_anchor` is orchard-feature-gated, unavailable in this build).
+ let pczt = Creator::new(
BranchId::Nu6_3.into(),
10,
MainNetwork.coin_type(),
[0; 32],
[0; 32],
- [1; 32],
)
+ .unwrap()
.build();
let result = check_pczt_multi_coins(
&MainNetwork,
- &pczt.serialize(),
+ &pczt.serialize().unwrap(),
"not-an-xpub",
&[7u8; 32],
0,
@@ -711,7 +717,9 @@ fn sign_checked_pczt_with_policy<P: consensus::Parameters>(
} else {
ensure_shielded_actions_are_signed(signed, &signable_actions)?
};
- Ok(signed.serialize())
+ signed
+ .serialize()
+ .map_err(|e| ZcashError::SigningError(alloc::format!("serialize signed PCZT: {e:?}")))
}
#[cfg(feature = "cypherpunk")]
@@ -727,14 +735,16 @@ mod tests {
use super::*;
extern crate std;
+ // Head of the v2 PCZT wire layout (`pczt::Pczt`'s v2 encoding), up to and including
+ // the Sapling bundle; empty bundles are omitted (`None`). The trailing Orchard and
+ // Ironwood bundles are captured as raw bytes via `postcard::take_from_bytes` and
+ // re-appended unchanged, so these fixtures only decode the fields at/before the one
+ // they mutate and never need the crate-private `orchard::v2::Bundle` layout.
#[derive(Serialize, Deserialize)]
- struct PcztMirror {
+ struct PcztHead {
global: GlobalMirror,
- transparent: ::pczt::transparent::Bundle,
- sapling: SaplingBundleMirror,
- orchard: ::pczt::orchard::Bundle,
- #[cfg(zcash_unstable = "nu6.3")]
- ironwood: ::pczt::orchard::Bundle,
+ transparent: Option<::pczt::transparent::Bundle>,
+ sapling: Option<SaplingBundleMirror>,
}
#[derive(Serialize, Deserialize)]
@@ -791,15 +801,21 @@ mod tests {
#[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());
+ let bytes = sample.bytes;
+ assert!(!::pczt::Pczt::parse(&bytes)
+ .unwrap()
+ .ironwood()
+ .actions()
+ .is_empty());
+ let (mut head, rest) = postcard::take_from_bytes::<PcztHead>(&bytes[8..]).unwrap();
- pczt.global.tx_version = constants::V5_TX_VERSION;
- pczt.global.version_group_id = constants::V5_VERSION_GROUP_ID;
+ head.global.tx_version = constants::V5_TX_VERSION;
+ head.global.version_group_id = constants::V5_VERSION_GROUP_ID;
- bytes.truncate(8);
- postcard::to_extend(&pczt, bytes).unwrap()
+ let mut out = bytes[..8].to_vec();
+ out = postcard::to_extend(&head, out).unwrap();
+ out.extend_from_slice(rest);
+ out
}
fn assert_invalid_pczt_message<T: core::fmt::Debug>(result: Result<T>, expected: &str) {
@@ -813,23 +829,33 @@ mod tests {
use ::pczt::roles::creator::Creator;
use zcash_vendor::zcash_protocol::consensus::{BranchId, NetworkConstants};
- let mut bytes = Creator::new(
+ let bytes = Creator::new(
BranchId::Nu6.into(),
10,
MainNetwork.coin_type(),
[0; 32],
[0; 32],
)
+ .unwrap()
.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;
+ .serialize()
+ .unwrap();
+ let (mut head, rest) = postcard::take_from_bytes::<PcztHead>(&bytes[8..]).unwrap();
+ // v2 omits empty bundles, so the freshly created PCZT has no Sapling bundle.
+ // Attach one that is empty except for a non-zero value sum, which `check` rejects.
+ assert!(head.sapling.is_none());
+ head.sapling = Some(SaplingBundleMirror {
+ spends: Vec::new(),
+ outputs: Vec::new(),
+ value_sum: 1,
+ anchor: [0u8; 32],
+ bsk: None,
+ });
- bytes.truncate(8);
- postcard::to_extend(&pczt, bytes).unwrap()
+ let mut out = bytes[..8].to_vec();
+ out = postcard::to_extend(&head, out).unwrap();
+ out.extend_from_slice(rest);
+ out
}
/// A PCZT whose Sapling bundle is empty but declares a non-zero value sum is malformed
@@ -912,6 +938,7 @@ mod tests {
sapling_anchor: None,
orchard_anchor: Some(orchard::Anchor::empty_tree()),
ironwood_anchor: None,
+ orchard_pool_bundle_type: orchard::builder::BundleType::DEFAULT,
},
);
builder
@@ -943,7 +970,10 @@ mod tests {
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 pczt_bytes = Creator::build_from_parts(pczt_parts)
+ .unwrap()
+ .serialize()
+ .unwrap();
let seed_fingerprint = calculate_seed_fingerprint(&victim_seed).unwrap();
let expected =
@@ -1265,8 +1295,17 @@ mod tests {
#[cfg(zcash_unstable = "nu6.3")]
fn pczt_with_sapling_output() -> pczt::test_support::SamplePczt {
let mut sample = pczt::test_support::sample_orchard_change_pczt();
- let mut pczt: PcztMirror = postcard::from_bytes(&sample.bytes[8..]).unwrap();
- pczt.sapling.outputs.push(SaplingOutputMirror {
+ let (mut head, rest) = postcard::take_from_bytes::<PcztHead>(&sample.bytes[8..]).unwrap();
+ // The orchard-change sample carries no Sapling bundle (v2 omits it); synthesize one
+ // with a single output and negative value sum so the batch check rejects it.
+ let mut sapling = head.sapling.take().unwrap_or(SaplingBundleMirror {
+ spends: Vec::new(),
+ outputs: Vec::new(),
+ value_sum: 0,
+ anchor: [0u8; 32],
+ bsk: None,
+ });
+ sapling.outputs.push(SaplingOutputMirror {
cv: [0; 32],
cmu: [0; 32],
ephemeral_key: [0; 32],
@@ -1282,10 +1321,13 @@ mod tests {
user_address: None,
proprietary: BTreeMap::new(),
});
- pczt.sapling.value_sum = -1;
+ sapling.value_sum = -1;
+ head.sapling = Some(sapling);
- sample.bytes.truncate(8);
- sample.bytes = postcard::to_extend(&pczt, sample.bytes).unwrap();
+ let mut out = sample.bytes[..8].to_vec();
+ out = postcard::to_extend(&head, out).unwrap();
+ out.extend_from_slice(rest);
+ sample.bytes = out;
sample
}
diff --git a/rust/apps/zcash/src/pczt/mod.rs b/rust/apps/zcash/src/pczt/mod.rs
index 0587337..dcc39d8 100644
--- a/rust/apps/zcash/src/pczt/mod.rs
+++ b/rust/apps/zcash/src/pczt/mod.rs
@@ -295,6 +295,7 @@ pub(crate) mod test_support {
.unwrap()
.finish()
.serialize()
+ .unwrap()
}
#[cfg(zcash_unstable = "nu6.3")]
@@ -331,6 +332,7 @@ pub(crate) mod test_support {
.unwrap()
.finish()
.serialize()
+ .unwrap()
}
#[cfg(zcash_unstable = "nu6.3")]
@@ -391,6 +393,7 @@ pub(crate) mod test_support {
sapling_anchor: None,
orchard_anchor: None,
ironwood_anchor: Some(anchor),
+ orchard_pool_bundle_type: orchard::builder::BundleType::DEFAULT,
},
);
builder
@@ -433,7 +436,7 @@ pub(crate) mod test_support {
.finish();
SamplePczt {
- bytes: pczt.serialize(),
+ bytes: pczt.serialize().unwrap(),
seed: seed.to_vec(),
ufvk_text,
seed_fingerprint,
@@ -503,6 +506,7 @@ pub(crate) mod test_support {
sapling_anchor: None,
orchard_anchor: Some(anchor),
ironwood_anchor: Some(orchard::Anchor::empty_tree()),
+ orchard_pool_bundle_type: orchard::builder::BundleType::DEFAULT,
},
);
builder
@@ -545,7 +549,7 @@ pub(crate) mod test_support {
.finish();
SamplePczt {
- bytes: pczt.serialize(),
+ bytes: pczt.serialize().unwrap(),
seed: seed.to_vec(),
ufvk_text,
seed_fingerprint,
@@ -667,7 +671,7 @@ pub(crate) mod test_support {
.finish();
SamplePczt {
- bytes: pczt.serialize(),
+ bytes: pczt.serialize().unwrap(),
seed: seed.to_vec(),
ufvk_text,
seed_fingerprint,
@@ -743,6 +747,7 @@ pub(crate) mod legacy_test_support {
.unwrap()
.finish()
.serialize()
+ .unwrap()
}
pub(crate) fn legacy_transparent_sample() -> LegacyTransparentSample {
@@ -780,6 +785,7 @@ pub(crate) mod legacy_test_support {
sapling_anchor: None,
orchard_anchor: None,
ironwood_anchor: None,
+ orchard_pool_bundle_type: orchard::builder::BundleType::DEFAULT,
},
);
builder
@@ -822,7 +828,7 @@ pub(crate) mod legacy_test_support {
.to_string();
LegacyTransparentSample {
- bytes: pczt.serialize(),
+ bytes: pczt.serialize().unwrap(),
seed: seed.to_vec(),
seed_fingerprint,
xpub,
diff --git a/rust/apps/zcash/src/pczt/parse.rs b/rust/apps/zcash/src/pczt/parse.rs
index f0b6d81..e86b6af 100644
--- a/rust/apps/zcash/src/pczt/parse.rs
+++ b/rust/apps/zcash/src/pczt/parse.rs
@@ -813,14 +813,17 @@ mod legacy_tests {
#[cfg(zcash_unstable = "nu6.3")]
#[test]
fn legacy_parse_rejects_v6_pczt() {
- let pczt = Creator::new_v6(
+ // `Creator::new(Nu6_3, ..)` alone yields a V6 PCZT (tx_version V6); the legacy
+ // path rejects it on version, so no Ironwood anchor is needed (and
+ // `with_ironwood_anchor` is orchard-feature-gated, unavailable in this build).
+ let pczt = Creator::new(
BranchId::Nu6_3.into(),
10,
MainNetwork.coin_type(),
[0; 32],
[0; 32],
- [1; 32],
)
+ .unwrap()
.build();
let result = parse_pczt_multi_coins(&MainNetwork, &[7u8; 32], &pczt);
diff --git a/rust/apps/zcash/src/pczt/sign.rs b/rust/apps/zcash/src/pczt/sign.rs
index bdaa8c1..3913733 100644
--- a/rust/apps/zcash/src/pczt/sign.rs
+++ b/rust/apps/zcash/src/pczt/sign.rs
@@ -83,7 +83,9 @@ pub fn sign_pczt(pczt: Pczt, seed: &[u8]) -> crate::Result<Vec<u8>> {
let signer = pczt_ext::sign_transparent(signer, &SeedSigner { seed })
.map_err(|e| ZcashError::SigningError(e.to_string()))?;
- Ok(stamp_and_redact(signer.finish()).serialize())
+ stamp_and_redact(signer.finish())
+ .serialize()
+ .map_err(|e| ZcashError::SigningError(format!("serialize signed PCZT: {e:?}")))
}
#[cfg(not(feature = "cypherpunk"))]
@@ -243,7 +245,9 @@ impl PcztSigner for SeedSigner<'_> {
/// Thin wrapper over `sign_pczt_to_pczt`; see it for the full contract.
#[cfg(feature = "cypherpunk")]
pub fn sign_pczt(pczt: Pczt, seed: &[u8]) -> crate::Result<Vec<u8>> {
- Ok(sign_pczt_to_pczt(pczt, seed)?.serialize())
+ sign_pczt_to_pczt(pczt, seed)?
+ .serialize()
+ .map_err(|e| ZcashError::SigningError(format!("serialize signed PCZT: {e:?}")))
}
/// `sign_pczt`, but returns the stamped, redacted PCZT without serializing it,
@@ -427,6 +431,7 @@ mod tests {
use super::*;
// RoleSigner is the upstream reference signer; tests use its sighash as the
// bit-exact oracle for the lean pczt_ext::shielded_sig_commitment.
+ use zcash_vendor::pczt::roles::redactor::Redactor;
use zcash_vendor::pczt::roles::signer::Signer as RoleSigner;
fn assert_invalid_pczt_message<T: core::fmt::Debug>(result: crate::Result<T>, expected: &str) {
@@ -542,19 +547,20 @@ mod tests {
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")
+ // The v6 Ironwood sighash must not commit the anchor. The new stack has no
+ // post-parse anchor setter, so redact the anchor instead: parse substitutes a
+ // version-gated placeholder, and the shielded sighash must be unchanged.
+ let cleared_anchor_pczt = Redactor::new(pczt.clone())
+ .redact_ironwood_with(|mut r| r.clear_anchor())
.finish();
assert_ne!(
pczt.ironwood().anchor(),
- updated_anchor_pczt.ironwood().anchor()
+ cleared_anchor_pczt.ironwood().anchor()
);
assert_eq!(
base_sighash,
- RoleSigner::new(updated_anchor_pczt)
- .expect("anchor-updated Ironwood PCZT signer should initialize")
+ RoleSigner::new(cleared_anchor_pczt)
+ .expect("anchor-cleared Ironwood PCZT signer should initialize")
.shielded_sighash(),
"v6 Ironwood spend signatures must not commit to the anchor"
);
@@ -748,14 +754,17 @@ mod legacy_tests {
#[cfg(zcash_unstable = "nu6.3")]
#[test]
fn legacy_signing_rejects_v6_pczt() {
- let pczt = Creator::new_v6(
+ // `Creator::new(Nu6_3, ..)` alone yields a V6 PCZT (tx_version V6); the legacy
+ // path rejects it on version, so no Ironwood anchor is needed (and
+ // `with_ironwood_anchor` is orchard-feature-gated, unavailable in this build).
+ let pczt = Creator::new(
BranchId::Nu6_3.into(),
10,
MainNetwork.coin_type(),
[0; 32],
[0; 32],
- [1; 32],
)
+ .unwrap()
.build();
let result = sign_pczt(pczt, &[7u8; 32]);
diff --git a/rust/zcash_vendor/Cargo.toml b/rust/zcash_vendor/Cargo.toml
index 315ee8a..0dc6b3f 100644
--- a/rust/zcash_vendor/Cargo.toml
+++ b/rust/zcash_vendor/Cargo.toml
@@ -41,20 +41,20 @@ chacha20poly1305 = { version = "0.10.1", default-features = false, features = [
] }
postcard = { version = "1.0.3", features = ["alloc"] }
getset = { version = "0.1.3" }
-orchard = { version = "0.15.0-pre.1", default-features = false, optional = true }
+orchard = { version = "0.15.0-pre.2", default-features = false, optional = true }
pczt = { version = "0.7", default-features = false }
serde = { workspace = true }
serde_with = { version = "3.11.0", features = [
"alloc",
"macros",
], default-features = false }
-transparent = { package = "zcash_transparent", version = "0.8", default-features = false, features = [
+transparent = { package = "zcash_transparent", version = "0.9.0-pre.0", default-features = false, features = [
"transparent-inputs",
] }
-zcash_address = { version = "0.12", default-features = false }
+zcash_address = { version = "0.13.0-pre.0", default-features = false }
zcash_encoding = { version = "0.4", default-features = false }
-zcash_keys = { version = "0.14", default-features = false }
-zcash_protocol = { version = "0.9", default-features = false }
+zcash_keys = { version = "0.15.0-pre.0", default-features = false }
+zcash_protocol = { version = "0.10.0-pre.0", default-features = false }
zip32 = { version = "0.2", default-features = false }
rust_tools = { workspace = true }
#zcash end
@@ -65,7 +65,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [
] }
[dev-dependencies]
-transparent = { package = "zcash_transparent", version = "0.8", default-features = false, features = [
+transparent = { package = "zcash_transparent", version = "0.9.0-pre.0", default-features = false, features = [
"transparent-inputs",
"test-dependencies",
] }
diff --git a/rust/zcash_vendor/src/pczt_ext.rs b/rust/zcash_vendor/src/pczt_ext.rs
index 5fce6ae..301c97a 100644
--- a/rust/zcash_vendor/src/pczt_ext.rs
+++ b/rust/zcash_vendor/src/pczt_ext.rs
@@ -188,6 +188,31 @@ fn hash_transparent_tx_id(t_digests: Option<TransparentDigests>) -> Hash {
h.finalize()
}
+/// Orchard/Ironwood note ciphertext length; the ZIP-244 action digests slice it as
+/// 52 (compact) | 512 (memo) | 16 (non-compact).
+const ORCHARD_ENC_CIPHERTEXT_SIZE: usize = 580;
+
+/// The action's value-commitment bytes for the sighash. `cv_net` is `Option` in the v2
+/// PCZT wire model; the checked-PCZT preflight resolves it and `check::verify_cv_net`
+/// rejects any still-missing value before signing, so it is always present here. The zero
+/// fallback only keeps this infallible digest panic-free — a malformed PCZT then yields a
+/// non-matching sighash (an invalid signature), never a silent-but-valid one.
+fn action_cv_net(action: &pczt::orchard::Action) -> &[u8; 32] {
+ static ZERO: [u8; 32] = [0; 32];
+ action.cv_net().as_ref().unwrap_or(&ZERO)
+}
+
+/// The output's encrypted note ciphertext for the sighash. `resolve_fields` restores the
+/// full ciphertext from a memo-plaintext-only (`EncCiphertext::MemoPlaintext`) output; see
+/// [`action_cv_net`] for why the zero fallback is unreachable in the signing path.
+fn action_enc_ciphertext(output: &pczt::orchard::Output) -> &[u8] {
+ static ZERO: [u8; ORCHARD_ENC_CIPHERTEXT_SIZE] = [0; ORCHARD_ENC_CIPHERTEXT_SIZE];
+ match output.enc_ciphertext() {
+ pczt::orchard::EncCiphertext::Encrypted(c) if c.len() == ORCHARD_ENC_CIPHERTEXT_SIZE => c,
+ _ => &ZERO,
+ }
+}
+
fn digest_orchard(pczt: &Pczt) -> Hash {
let mut h = hasher(ZCASH_ORCHARD_HASH_PERSONALIZATION);
@@ -199,13 +224,13 @@ fn digest_orchard(pczt: &Pczt) -> Hash {
ch.update(action.spend().nullifier());
ch.update(action.output().cmx());
ch.update(action.output().ephemeral_key());
- ch.update(&action.output().enc_ciphertext()[..52]);
+ ch.update(&action_enc_ciphertext(action.output())[..52]);
- mh.update(&action.output().enc_ciphertext()[52..564]);
+ mh.update(&action_enc_ciphertext(action.output())[52..564]);
- nh.update(action.cv_net());
+ nh.update(action_cv_net(action));
nh.update(action.spend().rk());
- nh.update(&action.output().enc_ciphertext()[564..]);
+ nh.update(&action_enc_ciphertext(action.output())[564..]);
nh.update(action.output().out_ciphertext());
}
@@ -221,7 +246,9 @@ fn digest_orchard(pczt: &Pczt) -> Hash {
};
h.update(&value_balance.to_le_bytes());
- h.update(pczt.orchard().anchor());
+ // v5 commits the anchor (v6 omits it). Present for well-formed v5 bundles; the empty
+ // fallback keeps this infallible — see `action_cv_net`.
+ h.update(pczt.orchard().anchor().as_ref().unwrap_or(&[0u8; 32]));
h.finalize()
}
@@ -354,13 +381,13 @@ fn digest_orchard_shaped_v6(
ch.update(action.spend().nullifier());
ch.update(action.output().cmx());
ch.update(action.output().ephemeral_key());
- ch.update(&action.output().enc_ciphertext()[..52]);
+ ch.update(&action_enc_ciphertext(action.output())[..52]);
- mh.update(&action.output().enc_ciphertext()[52..564]);
+ mh.update(&action_enc_ciphertext(action.output())[52..564]);
- nh.update(action.cv_net());
+ nh.update(action_cv_net(action));
nh.update(action.spend().rk());
- nh.update(&action.output().enc_ciphertext()[564..]);
+ nh.update(&action_enc_ciphertext(action.output())[564..]);
nh.update(action.output().out_ciphertext());
}
@@ -604,7 +631,7 @@ where
pub fn sign_orchard<T>(llsigner: Signer, signer: &T) -> Result<Signer, T::Error>
where
T: PcztSigner,
- T::Error: From<pczt::orchard::BundleParseError>,
+ T::Error: From<pczt::roles::low_level_signer::OrchardParseError>,
T::Error: From<orchard::pczt::ParseError>,
T::Error: From<transparent::pczt::ParseError>,
{
@@ -657,7 +684,7 @@ where
pub fn sign_ironwood<T>(llsigner: Signer, signer: &T) -> Result<Signer, T::Error>
where
T: PcztSigner,
- T::Error: From<pczt::orchard::BundleParseError>,
+ T::Error: From<pczt::roles::low_level_signer::OrchardParseError>,
T::Error: From<orchard::pczt::ParseError>,
T::Error: From<transparent::pczt::ParseError>,
{
Why this scored 29/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.