fix(zcash): validate shielded outputs, memos, and ownership
What changed, and why it matters
This commit fixes validation of Zcash shielded transaction data on a Keystone hardware wallet. Previously, the device could accept transactions where it could not verify where shielded funds were going, could not read certain encrypted outputs, or could be shown invalid memos. The patch makes the wallet reject such cases, helping prevent theft or misleading transaction review.
Treat this as a security fix and include it in the next firmware release. Users relying on Zcash shielded transactions should update. No independent CVE or advisory is referenced, so monitor Keystone communications for further guidance.
Security signals we found
Added ownership check for funded shielded outputs paired with zero-value spends
Added requirement that all real shielded outputs be decryptable/recoverable before signing
Fixed Ironwood vs Orchard note-encryption domain selection during output recovery
Added UTF-8 validation for text memos per ZIP 302
Removed unsafe unwrap on memo UTF-8 decoding
Added unit tests for foreign restricted outputs and undecryptable outputs
Evidence from the diff
The patch strengthens PCZT (partially-created Zcash transaction) review in the Keystone 3 firmware’s Zcash Rust app. It adds validation that funded Orchard/Ironwood outputs paired with zero-value spends belong to the selected account unless cross-address sends are enabled. It also requires every real shielded output to be decryptable/recoverable for review, fixes Ironwood note-encryption domain handling, and rejects text memos containing invalid UTF-8. New tests cover foreign restricted outputs and undecryptable Ironwood outputs.
Changed components
rust/apps/zcash/src/pczt/check.rsrust/apps/zcash/src/pczt/parse.rsrust/apps/zcash/src/pczt/mod.rsrust/apps/zcash/src/lib.rsInspect captured patch +275 / −118
diff --git a/rust/apps/zcash/src/lib.rs b/rust/apps/zcash/src/lib.rs
index 9bea271..fea3fd4 100644
--- a/rust/apps/zcash/src/lib.rs
+++ b/rust/apps/zcash/src/lib.rs
@@ -1041,6 +1041,37 @@ mod tests {
}
}
+ #[test]
+ fn test_check_rejects_foreign_restricted_orchard_output() {
+ let sample = pczt::test_support::sample_orchard_foreign_change_pczt();
+ let expected =
+ "funded Orchard output paired with a zero-value spend does not belong to the selected account";
+
+ for result in [
+ check_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .map(|_| ()),
+ check_and_parse_batch_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &sample.bytes,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .map(|_| ()),
+ ] {
+ match result {
+ Err(ZcashError::InvalidPczt(message)) if message == expected => {}
+ other => panic!("check must reject foreign restricted output, got: {other:?}"),
+ }
+ }
+ }
+
#[test]
fn test_get_address() {
let address = get_address(&MainNetwork, "uview1s2e0495jzhdarezq4h4xsunfk4jrq7gzg22tjjmkzpd28wgse4ejm6k7yfg8weanaghmwsvc69clwxz9f9z2hwaz4gegmna0plqrf05zkeue0nevnxzm557rwdkjzl4pl4hp4q9ywyszyjca8jl54730aymaprt8t0kxj8ays4fs682kf7prj9p24dnlcgqtnd2vnskkm7u8cwz8n0ce7yrwx967cyp6dhkc2wqprt84q0jmwzwnufyxe3j0758a9zgk9ssrrnywzkwfhu6ap6cgx3jkxs3un53n75s3");
@@ -1601,6 +1632,83 @@ mod tests {
);
}
+ #[test]
+ fn test_check_rejects_undecryptable_ironwood_output() {
+ use zcash_vendor::pczt::Pczt;
+
+ /// Flips a byte inside the first verbatim occurrence of `needle`.
+ fn corrupt_first_occurrence(haystack: &mut [u8], needle: &[u8]) -> bool {
+ if needle.is_empty() || needle.len() > haystack.len() {
+ return false;
+ }
+ for start in 0..=haystack.len() - needle.len() {
+ if &haystack[start..start + needle.len()] == needle {
+ haystack[start + needle.len() / 2] ^= 0xff;
+ return true;
+ }
+ }
+ false
+ }
+
+ let sample = pczt::test_support::sample_migration_pczt();
+
+ // Extract the non-zero Ironwood output's ciphertext bytes.
+ let enc_ciphertext = {
+ let pczt = Pczt::parse(&sample.bytes).expect("sample PCZT should parse");
+ pczt.ironwood()
+ .actions()
+ .iter()
+ .find(|action| matches!(action.output().value(), Some(value) if *value != 0))
+ .expect("migration child must contain a non-zero Ironwood output")
+ .output()
+ .enc_ciphertext()
+ .clone()
+ .into_encrypted()
+ .expect("the sample's Ironwood output carries a full enc_ciphertext")
+ };
+
+ // Corrupt only the ciphertext: cmx, cv_net, the value balance, and the
+ // plaintext recipient are all untouched, so every other check still
+ // passes and only decryption/recoverability fails.
+ let mut corrupted = sample.bytes.clone();
+ assert!(
+ corrupt_first_occurrence(&mut corrupted, &enc_ciphertext),
+ "sample must embed the Ironwood output enc_ciphertext verbatim"
+ );
+ assert!(
+ Pczt::parse(&corrupted).is_ok(),
+ "corruption must keep the PCZT structurally well-formed"
+ );
+
+ let check_err = check_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &corrupted,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ 0,
+ )
+ .expect_err("check must reject a PCZT with an undecryptable output");
+ assert!(
+ matches!(&check_err, ZcashError::InvalidPczt(message) if message.contains("undecryptable")),
+ "expected an undecryptable-output rejection, got {check_err:?}"
+ );
+
+ // Both review paths enforce the same output-recoverability contract.
+ assert!(
+ matches!(
+ check_and_parse_batch_pczt_cypherpunk(
+ &pczt::test_support::Nu6_3Network,
+ &corrupted,
+ &sample.ufvk_text,
+ &sample.seed_fingerprint,
+ 0,
+ ),
+ Err(ZcashError::InvalidPczt(message)) if message.contains("undecryptable")
+ ),
+ "single-pass batch review must also reject the undecryptable output"
+ );
+ }
+
#[test]
fn test_check_resolves_compact_pczt_and_signs() {
use zcash_vendor::pczt::roles::redactor::Redactor;
diff --git a/rust/apps/zcash/src/pczt/check.rs b/rust/apps/zcash/src/pczt/check.rs
index 74b9ae8..abb9f49 100644
--- a/rust/apps/zcash/src/pczt/check.rs
+++ b/rust/apps/zcash/src/pczt/check.rs
@@ -6,7 +6,7 @@ use crate::errors::ZcashError;
#[cfg(feature = "cypherpunk")]
use zcash_vendor::{
- orchard::{self, keys::FullViewingKey, value::ValueSum, Address},
+ orchard::{self, keys::FullViewingKey, value::ValueSum},
zcash_keys::keys::UnifiedFullViewingKey,
};
@@ -367,7 +367,15 @@ fn check_shielded_bundle<P: consensus::Parameters>(
) -> Result<(), ZcashError> {
let pool_label = pool.label();
bundle.actions().iter().try_for_each(|action| {
- check_action(params, seed_fingerprint, account_index, ufvk, action, pool)?;
+ check_action(
+ params,
+ seed_fingerprint,
+ account_index,
+ ufvk,
+ action,
+ bundle.flags(),
+ pool,
+ )?;
Ok::<_, ZcashError>(())
})?;
@@ -436,8 +444,9 @@ fn check_and_parse_shielded_bundle<P: consensus::Parameters>(
}
}
- // Decode real outputs once and add them to the review.
+ // Require every real output to be recoverable for review.
let parsed_to = super::parse::parse_orchard_output(params, ufvk, action, pool)?;
+ check_restricted_zero_value_output(ufvk, action, bundle.flags(), pool)?;
if !parsed_to.get_is_dummy() {
parsed_orchard.add_to(parsed_to);
}
@@ -476,6 +485,7 @@ fn check_action<P: consensus::Parameters>(
account_index: zip32::AccountId,
ufvk: &UnifiedFullViewingKey,
action: &orchard::pczt::Action,
+ flags: &orchard::bundle::Flags,
pool: ShieldedPool,
) -> Result<(), ZcashError> {
let pool_label = pool.label();
@@ -496,7 +506,7 @@ fn check_action<P: consensus::Parameters>(
action.spend(),
pool,
)?;
- check_action_output(params, ufvk, action, pool)?;
+ check_action_output(params, ufvk, action, flags, pool)?;
Ok(())
}
@@ -546,20 +556,11 @@ fn check_action_spend<P: consensus::Parameters>(
}
#[cfg(feature = "cypherpunk")]
-fn is_wallet_orchard_address(fvk: &FullViewingKey, address: &Address) -> bool {
- let external_ivk = fvk.to_ivk(zcash_vendor::zip32::Scope::External);
- let internal_ivk = fvk.to_ivk(zcash_vendor::zip32::Scope::Internal);
-
- external_ivk.diversifier_index(address).is_some()
- || internal_ivk.diversifier_index(address).is_some()
-}
-
-#[cfg(feature = "cypherpunk")]
-// check output cmx and internal-ovk output ownership constraints
fn check_action_output<P: consensus::Parameters>(
params: &P,
ufvk: &UnifiedFullViewingKey,
action: &orchard::pczt::Action,
+ flags: &orchard::bundle::Flags,
pool: ShieldedPool,
) -> Result<(), ZcashError> {
let pool_label = pool.label();
@@ -568,40 +569,39 @@ fn check_action_output<P: consensus::Parameters>(
.verify_note_commitment(action.spend())
.map_err(|e| ZcashError::InvalidPczt(format!("invalid {pool_label} action cmx: {e:?}")))?;
- let fvk = ufvk.orchard().ok_or(ZcashError::InvalidDataError(
- "orchard fvk is not present".to_string(),
- ))?;
- let external_ovk = fvk.to_ovk(zcash_vendor::zip32::Scope::External).clone();
- let internal_ovk = fvk.to_ovk(zcash_vendor::zip32::Scope::Internal).clone();
- let transparent_internal_ovk = ufvk
- .transparent()
- .map(|k| orchard::keys::OutgoingViewingKey::from(k.internal_ovk().as_bytes()));
-
- let mut keys = vec![(Some(external_ovk), false), (Some(internal_ovk), true)];
- if let Some(ovk) = transparent_internal_ovk {
- keys.push((Some(ovk), true));
- }
+ // Decode and validate the recipient, rejecting non-zero outputs the device cannot review.
+ super::parse::parse_orchard_output(params, ufvk, action, pool)?;
+ check_restricted_zero_value_output(ufvk, action, flags, pool)?;
- for (vk, is_internal_ovk) in keys {
- 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(format!(
- "{pool_label} output was recoverable with an internal OVK but does not belong to this wallet"
- )));
- }
- break;
- }
+ Ok(())
+}
+
+#[cfg(feature = "cypherpunk")]
+fn check_restricted_zero_value_output(
+ ufvk: &UnifiedFullViewingKey,
+ action: &orchard::pczt::Action,
+ flags: &orchard::bundle::Flags,
+ pool: ShieldedPool,
+) -> Result<(), ZcashError> {
+ // A restricted action binds its spend and output to the same expanded receiver.
+ // A funded output paired with a zero-valued spend must therefore be ours.
+ let is_zero_spend = matches!(action.spend().value(), Some(value) if value.inner() == 0);
+ let is_funded_output = matches!(action.output().value(), Some(value) if value.inner() != 0);
+ if flags.cross_address_enabled() || !is_zero_spend || !is_funded_output {
+ return Ok(());
}
- if let (Some(user_address), Some(recipient)) =
- (action.output().user_address(), action.output().recipient())
- {
- super::parse::validate_orchard_user_address(params, user_address, recipient)?;
+ let recipient = action.output().recipient().ok_or_else(|| {
+ ZcashError::InvalidPczt(format!(
+ "missing recipient for funded {} output",
+ pool.label()
+ ))
+ })?;
+ if !super::parse::is_wallet_orchard_address(ufvk, &recipient)? {
+ return Err(ZcashError::InvalidPczt(format!(
+ "funded {} output paired with a zero-value spend does not belong to the selected account",
+ pool.label()
+ )));
}
Ok(())
diff --git a/rust/apps/zcash/src/pczt/mod.rs b/rust/apps/zcash/src/pczt/mod.rs
index 22e61af..a0a7d33 100644
--- a/rust/apps/zcash/src/pczt/mod.rs
+++ b/rust/apps/zcash/src/pczt/mod.rs
@@ -536,15 +536,33 @@ pub(crate) mod test_support {
}
pub(crate) fn sample_orchard_change_pczt() -> SamplePczt {
+ sample_orchard_change_pczt_for_account(0)
+ }
+
+ pub(crate) fn sample_orchard_foreign_change_pczt() -> SamplePczt {
+ sample_orchard_change_pczt_for_account(1)
+ }
+
+ fn sample_orchard_change_pczt_for_account(output_account: u32) -> 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 output_ufvk_text = derive_ufvk(
+ ¶ms,
+ &seed,
+ &alloc::format!("m/32'/133'/{output_account}'"),
+ )
+ .unwrap();
+ let output_ufvk = UnifiedFullViewingKey::decode(¶ms, &output_ufvk_text).unwrap();
+ let output_fvk = output_ufvk.orchard().unwrap().clone();
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 spend_recipient = orchard_fvk.address_at(0u32, recipient_scope);
+ let output_recipient = output_fvk.address_at(0u32, recipient_scope);
+ let orchard_ovk = output_fvk.to_ovk(recipient_scope);
let value = orchard::value::NoteValue::from_raw(1_000_000);
let note = {
@@ -556,7 +574,12 @@ pub(crate) mod test_support {
)
.expect("spends-disabled flags are valid for a coinbase bundle");
orchard_builder
- .add_output(None, recipient, value, Memo::Empty.encode().into_bytes())
+ .add_output(
+ None,
+ spend_recipient,
+ value,
+ Memo::Empty.encode().into_bytes(),
+ )
.unwrap();
let (bundle, meta) = orchard_builder.build::<i64>(&mut OsRng).unwrap().unwrap();
let action = bundle
@@ -598,9 +621,9 @@ pub(crate) mod test_support {
.unwrap();
builder
.add_change_output(
- orchard_fvk,
+ output_fvk,
Some(orchard_ovk),
- recipient,
+ output_recipient,
orchard::value::NoteValue::from_raw(990_000),
Memo::Empty.encode().into_bytes(),
)
@@ -621,21 +644,28 @@ pub(crate) mod test_support {
.unwrap();
let pczt = Updater::new(pczt)
.update_orchard_with(|mut bundle| {
- let signing_action_indices = bundle
+ let signing_action_accounts = bundle
.bundle()
.actions()
.iter()
.enumerate()
.filter_map(|(index, action)| {
- action.spend().dummy_sk().is_none().then_some(index)
+ action.spend().dummy_sk().is_none().then(|| {
+ let account = if action.spend().value().unwrap().inner() == 0 {
+ output_account
+ } else {
+ 0
+ };
+ (index, account)
+ })
})
.collect::<Vec<_>>();
- assert_eq!(signing_action_indices.len(), 2);
+ assert_eq!(signing_action_accounts.len(), 2);
- for action_index in signing_action_indices {
+ for (action_index, account) in signing_action_accounts {
let derivation = orchard::pczt::Zip32Derivation::parse(
seed_fingerprint,
- orchard_spend_path_for_account(0),
+ orchard_spend_path_for_account(account),
)
.unwrap();
bundle.update_action_with(action_index, |mut action| {
diff --git a/rust/apps/zcash/src/pczt/parse.rs b/rust/apps/zcash/src/pczt/parse.rs
index bee446d..a33bc7d 100644
--- a/rust/apps/zcash/src/pczt/parse.rs
+++ b/rust/apps/zcash/src/pczt/parse.rs
@@ -98,52 +98,49 @@ fn format_zec_value(value: f64) -> String {
/// - `Ok(None)` if the output cannot be decrypted.
/// - `Err(_)` if `ovk` is `None` and the PCZT is missing fields needed to directly
/// decrypt the output.
+///
+/// `pool` selects the note-encryption domain used for recovery.
#[cfg(feature = "cypherpunk")]
-pub fn decode_output_enc_ciphertext(
+pub(crate) fn decode_output_enc_ciphertext(
action: &orchard::pczt::Action,
ovk: Option<&OutgoingViewingKey>,
+ pool: ShieldedPool,
) -> Result<Option<(Note, Address, [u8; 512])>, ZcashError> {
- // orchard 0.15.0-pre.1 splits note encryption by version: Ironwood actions carry V3
- // note plaintexts and must be trial-decrypted with `IronwoodDomain`, while Orchard
- // actions use the V2 `OrchardDomain`. Select the domain from the action's note version
- // so both pools decrypt correctly.
- let is_ironwood = matches!(*action.output().note_version(), orchard::NoteVersion::V3);
-
if let Some(ovk) = ovk {
let out_ciphertext = &action.output().encrypted_note().out_ciphertext;
- Ok(if is_ironwood {
- try_output_recovery_with_ovk(
- &IronwoodDomain::for_pczt_action(action),
+ Ok(match pool {
+ ShieldedPool::Orchard => try_output_recovery_with_ovk(
+ &OrchardDomain::for_pczt_action(action),
ovk,
action,
action.cv_net(),
out_ciphertext,
- )
- } else {
- try_output_recovery_with_ovk(
- &OrchardDomain::for_pczt_action(action),
+ ),
+ ShieldedPool::Ironwood => try_output_recovery_with_ovk(
+ &IronwoodDomain::for_pczt_action(action),
ovk,
action,
action.cv_net(),
out_ciphertext,
- )
+ ),
})
} else {
// If we reached here, none of our OVKs matched; recover directly as the fallback.
+ let pool_label = pool.label();
let recipient = action.output().recipient().ok_or_else(|| {
- ZcashError::InvalidPczt("Missing recipient field for Orchard action".into())
+ ZcashError::InvalidPczt(format!("Missing recipient field for {pool_label} action"))
})?;
let value = action.output().value().ok_or_else(|| {
- ZcashError::InvalidPczt("Missing value field for Orchard action".into())
+ ZcashError::InvalidPczt(format!("Missing value field for {pool_label} action"))
})?;
let rho = orchard::note::Rho::from_bytes(&action.spend().nullifier().to_bytes())
.into_option()
.ok_or_else(|| {
- ZcashError::InvalidPczt("Missing rho field for Orchard action".into())
+ ZcashError::InvalidPczt(format!("Missing rho field for {pool_label} action"))
})?;
let rseed = action.output().rseed().ok_or_else(|| {
- ZcashError::InvalidPczt("Missing rseed field for Orchard action".into())
+ ZcashError::InvalidPczt(format!("Missing rseed field for {pool_label} action"))
})?;
let note = orchard::Note::from_parts(
@@ -151,29 +148,36 @@ pub fn decode_output_enc_ciphertext(
value,
rho,
rseed,
- *action.output().note_version(),
+ (*action.output().note_version()).into(),
)
.into_option()
- .ok_or_else(|| ZcashError::InvalidPczt("Orchard action contains invalid note".into()))?;
+ .ok_or_else(|| {
+ ZcashError::InvalidPczt(format!("{pool_label} action contains invalid note"))
+ })?;
- Ok(if is_ironwood {
- let pk_d = IronwoodDomain::get_pk_d(¬e);
- let esk = IronwoodDomain::derive_esk(¬e).expect("Orchard notes are post-ZIP 212");
- try_output_recovery_with_pkd_esk(
- &IronwoodDomain::for_pczt_action(action),
- pk_d,
- esk,
- action,
- )
- } else {
- let pk_d = OrchardDomain::get_pk_d(¬e);
- let esk = OrchardDomain::derive_esk(¬e).expect("Orchard notes are post-ZIP 212");
- try_output_recovery_with_pkd_esk(
- &OrchardDomain::for_pczt_action(action),
- pk_d,
- esk,
- action,
- )
+ Ok(match pool {
+ ShieldedPool::Orchard => {
+ let pk_d = OrchardDomain::get_pk_d(¬e);
+ let esk = OrchardDomain::derive_esk(¬e)
+ .expect("Orchard-shaped notes are post-ZIP 212");
+ try_output_recovery_with_pkd_esk(
+ &OrchardDomain::for_pczt_action(action),
+ pk_d,
+ esk,
+ action,
+ )
+ }
+ ShieldedPool::Ironwood => {
+ let pk_d = IronwoodDomain::get_pk_d(¬e);
+ let esk = IronwoodDomain::derive_esk(¬e)
+ .expect("Orchard-shaped notes are post-ZIP 212");
+ try_output_recovery_with_pkd_esk(
+ &IronwoodDomain::for_pczt_action(action),
+ pk_d,
+ esk,
+ action,
+ )
+ }
})
}
}
@@ -644,7 +648,7 @@ pub(crate) fn parse_orchard_spend(
}
#[cfg(feature = "cypherpunk")]
-fn is_wallet_orchard_address(
+pub(crate) fn is_wallet_orchard_address(
ufvk: &UnifiedFullViewingKey,
address: &Address,
) -> Result<bool, ZcashError> {
@@ -721,10 +725,10 @@ pub(crate) fn parse_orchard_output<P: consensus::Parameters>(
.inner();
let decode_output = |vk: Option<OutgoingViewingKey>, is_internal_ovk: bool| {
- match decode_output_enc_ciphertext(action, vk.as_ref())? {
+ match decode_output_enc_ciphertext(action, vk.as_ref(), pool)? {
Some((note, address, memo)) => {
let zec_value = format_zec_value(note.value().inner() as f64);
- let memo = decode_memo(memo);
+ let memo = decode_memo(memo)?;
// Check output recipient with decoded address here to save CPU
// if the address is not match, return error
@@ -878,7 +882,7 @@ mod legacy_tests {
}
#[cfg(feature = "cypherpunk")]
-fn decode_memo(memo_bytes: [u8; 512]) -> Option<String> {
+fn decode_memo(memo_bytes: [u8; 512]) -> Result<Option<String>, ZcashError> {
let first = memo_bytes[0];
//decode as utf8.
@@ -897,21 +901,24 @@ fn decode_memo(memo_bytes: [u8; 512]) -> Option<String> {
}
result.reverse();
- return Some(String::from_utf8(result).unwrap());
+ // ZIP 302 requires text-tagged memos to contain valid UTF-8.
+ return String::from_utf8(result)
+ .map(Some)
+ .map_err(|_| ZcashError::InvalidPczt("text memo is not valid UTF-8".to_string()));
}
if first == 0xF6 {
let temp_memo = memo_bytes.to_vec();
let result = temp_memo[1..].iter().find(|&&v| v != 0);
match result {
- Some(_v) => return Some(hex::encode(memo_bytes)),
+ Some(_v) => return Ok(Some(hex::encode(memo_bytes))),
None => {
- return None;
+ return Ok(None);
}
}
}
- Some(hex::encode(memo_bytes))
+ Ok(Some(hex::encode(memo_bytes)))
}
#[cfg(feature = "cypherpunk")]
@@ -927,6 +934,18 @@ mod tests {
extern crate std;
+ #[test]
+ fn test_decode_memo_rejects_invalid_utf8() {
+ let mut memo = [0u8; 512];
+ memo[0] = 0xC3; // start of a 2-byte UTF-8 sequence...
+ memo[1] = 0x28; // ...followed by an invalid continuation byte
+
+ assert!(matches!(
+ decode_memo(memo),
+ Err(ZcashError::InvalidPczt(msg)) if msg == "text memo is not valid UTF-8"
+ ));
+ }
+
fn p2sh_output_with_matching_seed_fingerprint(
seed_fingerprint: [u8; 32],
) -> transparent::pczt::Output {
@@ -968,12 +987,12 @@ mod tests {
{
let mut memo = [0u8; 512];
memo[0] = 0xF6;
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert_eq!(result, None);
}
{
let memo = hex::decode("74657374206b657973746f6e65206d656d6f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap().try_into().unwrap();
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap(), "test keystone memo");
}
@@ -1046,7 +1065,7 @@ mod tests {
let mut memo = [0u8; 512];
memo[0] = 0xF6;
memo[1] = 0x01;
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
let hex_str = result.unwrap();
assert!(hex_str.starts_with("f6"));
@@ -1054,7 +1073,7 @@ mod tests {
{
let mut memo = [0u8; 512];
memo[0] = 0xF5;
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
}
}
@@ -1105,7 +1124,7 @@ mod tests {
#[test]
fn test_decode_memo_empty() {
let memo = [0u8; 512];
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
let decoded = result.unwrap();
assert!(decoded.is_empty());
@@ -1114,7 +1133,7 @@ mod tests {
#[test]
fn test_decode_memo_full_text() {
let memo = [b'A'; 512];
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
let decoded = result.unwrap();
assert_eq!(decoded.len(), 512);
@@ -1125,7 +1144,7 @@ mod tests {
let mut memo = [0u8; 512];
let text = b"Hello World";
memo[..text.len()].copy_from_slice(text);
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap(), "Hello World");
}
@@ -1135,7 +1154,7 @@ mod tests {
let mut memo = [0u8; 512];
let text = "测试中文".as_bytes();
memo[..text.len()].copy_from_slice(text);
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap(), "测试中文");
}
@@ -1147,14 +1166,14 @@ mod tests {
memo[0] = b'A';
memo[1] = b'B';
memo[2] = b'C';
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap(), "ABC");
// Test with 0xF5 (should use hex encoding)
let mut memo = [0u8; 512];
memo[0] = 0xF5;
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
let hex_str = result.unwrap();
assert!(hex_str.starts_with("f5"));
@@ -1165,7 +1184,7 @@ mod tests {
// Test 0xF6 marker with all zeros after it (should return None)
let mut memo = [0u8; 512];
memo[0] = 0xF6;
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert_eq!(result, None);
}
@@ -1176,7 +1195,7 @@ mod tests {
memo[0] = 0xF6;
memo[1] = 0xFF;
memo[2] = 0xAB;
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
let hex_str = result.unwrap();
assert!(hex_str.starts_with("f6ff"));
@@ -1187,7 +1206,7 @@ mod tests {
let mut memo = [0u8; 512];
let text = b"Test!@#$%^&*()_+-=[]{}|;:',.<>?/`~";
memo[..text.len()].copy_from_slice(text);
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap(), "Test!@#$%^&*()_+-=[]{}|;:',.<>?/`~");
}
@@ -1225,7 +1244,7 @@ mod tests {
let mut memo = [0u8; 512];
let text = b"Line1\nLine2\tTabbed";
memo[..text.len()].copy_from_slice(text);
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap(), "Line1\nLine2\tTabbed");
}
@@ -1237,7 +1256,7 @@ mod tests {
for i in 32..=126 {
memo[i - 32] = i as u8;
}
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
let decoded = result.unwrap();
// Should decode all printable ASCII
@@ -1261,7 +1280,7 @@ mod tests {
let mut memo = [0u8; 512];
let text = b"Test123!@# ZEC Payment";
memo[..text.len()].copy_from_slice(text);
- let result = decode_memo(memo);
+ let result = decode_memo(memo).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap(), "Test123!@# ZEC Payment");
}
Why this scored 66/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.