use alloc::{collections::BTreeMap, string::String, vec::Vec};
use core::{cmp::Ordering, fmt};
use getset::Getters;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
#[cfg(feature = "orchard")]
pub(crate) use orchard::note::NoteVersion;
#[cfg(feature = "orchard")]
use {
::orchard::{
Address, Note, ValuePool,
bundle::BundleVersion,
note::{ExtractedNoteCommitment, Nullifier, RandomSeed, Rho},
note_encryption::{CompactAction, IronwoodDomain, OrchardDomain, OrchardNoteEncryption},
value::{NoteValue, ValueCommitTrapdoor, ValueCommitment},
},
ff::PrimeField,
zcash_note_encryption::{
COMPACT_NOTE_SIZE, Domain, ENC_CIPHERTEXT_SIZE, EphemeralKeyBytes, ShieldedOutput,
try_output_recovery_with_pkd_esk,
},
zcash_protocol::consensus::{BranchId, OrchardProtocolRevision},
};
use crate::{
common::{Global, Zip32Derivation},
roles::combiner::{merge_map, merge_optional},
};
#[cfg(not(feature = "orchard"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum NoteVersion {
V2,
V3,
}
#[derive(Clone, Debug, PartialEq, Getters)]
pub struct Bundle {
#[getset(get = "pub")]
pub(crate) actions: Vec<Action>,
#[getset(get = "pub")]
pub(crate) flags: u8,
#[getset(get = "pub")]
pub(crate) value_sum: (u64, bool),
#[getset(get = "pub")]
pub(crate) anchor: Option<[u8; 32]>,
pub(crate) note_version: NoteVersion,
#[getset(get = "pub")]
pub(crate) zkproof: Option<Vec<u8>>,
pub(crate) bsk: Option<[u8; 32]>,
}
pub(crate) const ORCHARD_SPENDS_AND_OUTPUTS_ENABLED: u8 = 0b0000_0011;
pub(crate) const IRONWOOD_SPENDS_OUTPUTS_AND_CROSS_ADDRESS_ENABLED: u8 = 0b0000_0111;
pub(crate) const MEMO_SIZE: usize = 512;
pub(crate) const DEFAULT_ANCHOR: [u8; 32] = [0; 32];
pub(crate) const EMPTY_ORCHARD: Bundle = Bundle {
actions: Vec::new(),
flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
value_sum: (0, false),
anchor: None,
note_version: NoteVersion::V2,
zkproof: None,
bsk: None,
};
pub(crate) const EMPTY_IRONWOOD: Bundle = Bundle {
actions: Vec::new(),
flags: IRONWOOD_SPENDS_OUTPUTS_AND_CROSS_ADDRESS_ENABLED,
value_sum: (0, false),
anchor: None,
note_version: NoteVersion::V3,
zkproof: None,
bsk: None,
};
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
enum MemoPlaintextError {
TooLong,
NotStripped,
}
impl fmt::Display for MemoPlaintextError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MemoPlaintextError::TooLong => {
write!(f, "memo plaintext exceeds {MEMO_SIZE} bytes")
}
MemoPlaintextError::NotStripped => {
write!(f, "memo plaintext has trailing zero bytes")
}
}
}
}
#[cfg(feature = "orchard")]
pub(crate) struct Parsed {
pub(crate) bundle: orchard::pczt::Bundle,
pub(crate) wire_anchor: Option<[u8; 32]>,
}
#[cfg(feature = "orchard")]
impl Parsed {
pub(crate) fn reserialize(self) -> Bundle {
Bundle {
anchor: self.wire_anchor,
..Bundle::serialize_from(self.bundle)
}
}
}
#[cfg(all(test, feature = "orchard"))]
pub(crate) mod testing {
#[cfg(any(feature = "prover", all(feature = "signer", feature = "io-finalizer")))]
use {
super::{Action, EncCiphertext, Output, Spend},
alloc::collections::BTreeMap,
ff::Field,
pasta_curves::pallas,
};
pub(crate) fn value_commitment(value: u64, rcv: [u8; 32]) -> [u8; 32] {
let rcv = orchard::value::ValueCommitTrapdoor::from_bytes(rcv)
.into_option()
.unwrap();
let value_sum =
orchard::value::NoteValue::from_raw(value) - orchard::value::NoteValue::from_raw(0);
orchard::value::ValueCommitment::derive(value_sum, rcv).to_bytes()
}
#[cfg(any(feature = "prover", all(feature = "signer", feature = "io-finalizer")))]
pub(crate) fn randomized_verification_key() -> [u8; 32] {
let sk = orchard::keys::SpendingKey::from_bytes([7; 32]).unwrap();
let ask = orchard::keys::SpendAuthorizingKey::from(&sk);
let randomized_signing_key = ask.randomize(&pallas::Scalar::ONE);
let rk: orchard::primitives::redpallas::VerificationKey<
orchard::primitives::redpallas::SpendAuth,
> = (&randomized_signing_key).into();
(&rk).into()
}
#[cfg(any(feature = "prover", all(feature = "signer", feature = "io-finalizer")))]
pub(crate) fn dummy_action() -> Action {
Action {
cv_net: Some(value_commitment(0, [3; 32])),
spend: Spend {
nullifier: [2; 32],
rk: randomized_verification_key(),
spend_auth_sig: None,
recipient: None,
value: None,
rho: None,
rseed: None,
fvk: None,
witness: None,
alpha: None,
zip32_derivation: None,
dummy_sk: None,
proprietary: BTreeMap::new(),
},
output: Output {
cmx: Some([4; 32]),
ephemeral_key: [5; 32],
enc_ciphertext: EncCiphertext::Encrypted(alloc::vec![6; 580]),
out_ciphertext: alloc::vec![7; 80],
recipient: None,
value: None,
rseed: None,
ock: None,
zip32_derivation: None,
user_address: None,
proprietary: BTreeMap::new(),
},
rcv: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MemoPlaintext(Vec<u8>);
impl MemoPlaintext {
pub fn from_memo(memo: [u8; MEMO_SIZE]) -> Self {
let len = memo.iter().rposition(|b| *b != 0).map_or(0, |i| i + 1);
Self(memo[..len].to_vec())
}
fn from_stripped_bytes(bytes: Vec<u8>) -> Result<Self, MemoPlaintextError> {
if bytes.len() > MEMO_SIZE {
Err(MemoPlaintextError::TooLong)
} else if bytes.last() == Some(&0) {
Err(MemoPlaintextError::NotStripped)
} else {
Ok(Self(bytes))
}
}
pub fn as_stripped_bytes(&self) -> &[u8] {
&self.0
}
pub fn to_memo(&self) -> [u8; MEMO_SIZE] {
let mut memo = [0; MEMO_SIZE];
memo[..self.0.len()].copy_from_slice(&self.0);
memo
}
}
impl Serialize for MemoPlaintext {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for MemoPlaintext {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let bytes = Vec::<u8>::deserialize(deserializer)?;
Self::from_stripped_bytes(bytes).map_err(de::Error::custom)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum EncCiphertext {
Encrypted(Vec<u8>),
MemoPlaintext(MemoPlaintext),
}
impl EncCiphertext {
pub fn into_encrypted(self) -> Option<Vec<u8>> {
match self {
EncCiphertext::Encrypted(ciphertext) => Some(ciphertext),
EncCiphertext::MemoPlaintext(_) => None,
}
}
}
#[cfg(feature = "orchard")]
fn recover_memo_plaintext_from_ciphertext_and_action(
action: &Action,
note_version: NoteVersion,
) -> Option<MemoPlaintext> {
struct OutputRecoveryData {
cmx: [u8; 32],
ephemeral_key: [u8; 32],
enc_ciphertext: [u8; ENC_CIPHERTEXT_SIZE],
}
impl<D> ShieldedOutput<D, ENC_CIPHERTEXT_SIZE> for OutputRecoveryData
where
D: Domain<ExtractedCommitmentBytes = [u8; 32]>,
{
fn ephemeral_key(&self) -> EphemeralKeyBytes {
EphemeralKeyBytes(self.ephemeral_key)
}
fn cmstar_bytes(&self) -> [u8; 32] {
self.cmx
}
fn enc_ciphertext(&self) -> &[u8; ENC_CIPHERTEXT_SIZE] {
&self.enc_ciphertext
}
}
fn recover_with_domain<D>(
domain: &D,
note: &Note,
output: &OutputRecoveryData,
) -> Option<MemoPlaintext>
where
D: Domain<Note = Note, Memo = [u8; MEMO_SIZE], ExtractedCommitmentBytes = [u8; 32]>,
{
let pk_d = D::get_pk_d(note);
let esk = D::derive_esk(note)?;
try_output_recovery_with_pkd_esk(domain, pk_d, esk, output)
.map(|(_, _, memo)| MemoPlaintext::from_memo(memo))
}
let enc_ciphertext = match &action.output.enc_ciphertext {
EncCiphertext::Encrypted(ciphertext) => ciphertext.as_slice().try_into().ok()?,
EncCiphertext::MemoPlaintext(_) => return None,
};
let recipient = Option::from(Address::from_raw_address_bytes(
action.output.recipient.as_ref()?,
))?;
let rho = Option::from(Rho::from_bytes(&action.spend.nullifier))?;
let rseed = Option::from(RandomSeed::from_bytes(*action.output.rseed.as_ref()?, &rho))?;
let note = Option::from(Note::from_parts(
recipient,
NoteValue::from_raw(action.output.value?),
rho,
rseed,
note_version,
))?;
let nullifier = Option::from(Nullifier::from_bytes(&action.spend.nullifier))?;
let cmx_bytes = action.output.cmx?;
let cmx = Option::from(ExtractedNoteCommitment::from_bytes(&cmx_bytes))?;
let output = OutputRecoveryData {
cmx: cmx_bytes,
ephemeral_key: action.output.ephemeral_key,
enc_ciphertext,
};
let compact_action = CompactAction::from_parts(
nullifier,
cmx,
EphemeralKeyBytes(action.output.ephemeral_key),
output.enc_ciphertext[..COMPACT_NOTE_SIZE].try_into().ok()?,
);
match note_version {
NoteVersion::V2 => recover_with_domain(
&OrchardDomain::for_compact_action(&compact_action),
¬e,
&output,
),
NoteVersion::V3 => recover_with_domain(
&IronwoodDomain::for_compact_action(&compact_action),
¬e,
&output,
),
}
}
#[cfg(feature = "orchard")]
impl Action {
pub(crate) fn replace_enc_ciphertext_with_decrypted_memo_plaintext(
&mut self,
note_version: NoteVersion,
) {
if let Some(memo) = recover_memo_plaintext_from_ciphertext_and_action(self, note_version) {
self.output.enc_ciphertext = EncCiphertext::MemoPlaintext(memo);
}
}
pub(crate) fn compact_resolvable_fields(&mut self, note_version: NoteVersion) {
let original_enc_ciphertext = self.output.enc_ciphertext.clone();
self.replace_enc_ciphertext_with_decrypted_memo_plaintext(note_version);
if self.output.enc_ciphertext != original_enc_ciphertext {
let mut resolved = self.output.clone();
if resolved
.encrypt_ciphertext_from_memo(note_version, self.spend.nullifier)
.is_err()
|| resolved.enc_ciphertext != original_enc_ciphertext
{
self.output.enc_ciphertext = original_enc_ciphertext;
}
}
if let Some(original_cv_net) = self.cv_net {
let mut resolved = self.clone();
resolved.cv_net = None;
if resolved.resolve_cv_net().is_ok() && resolved.cv_net == Some(original_cv_net) {
self.cv_net = None;
}
}
if let Some(original_cmx) = self.output.cmx {
let mut resolved = self.output.clone();
resolved.cmx = None;
if resolved
.resolve_cmx(note_version, self.spend.nullifier)
.is_ok()
&& resolved.cmx == Some(original_cmx)
{
self.output.cmx = None;
}
}
}
}
#[derive(Clone, Debug, PartialEq, Getters)]
pub struct Action {
#[getset(get = "pub")]
pub(crate) cv_net: Option<[u8; 32]>,
#[getset(get = "pub")]
pub(crate) spend: Spend,
#[getset(get = "pub")]
pub(crate) output: Output,
pub(crate) rcv: Option<[u8; 32]>,
}
#[derive(Clone, Debug, PartialEq, Getters)]
pub struct Spend {
#[getset(get = "pub")]
pub(crate) nullifier: [u8; 32],
#[getset(get = "pub")]
pub(crate) rk: [u8; 32],
#[getset(get = "pub")]
pub(crate) spend_auth_sig: Option<[u8; 64]>,
pub(crate) recipient: Option<[u8; 43]>,
pub(crate) value: Option<u64>,
pub(crate) rho: Option<[u8; 32]>,
pub(crate) rseed: Option<[u8; 32]>,
pub(crate) fvk: Option<[u8; 96]>,
#[getset(get = "pub")]
pub(crate) witness: Option<(u32, [[u8; 32]; 32])>,
pub(crate) alpha: Option<[u8; 32]>,
pub(crate) zip32_derivation: Option<Zip32Derivation>,
#[getset(get = "pub")]
pub(crate) dummy_sk: Option<[u8; 32]>,
#[getset(get = "pub")]
pub(crate) proprietary: BTreeMap<String, Vec<u8>>,
}
#[derive(Clone, Debug, PartialEq, Getters)]
pub struct Output {
#[getset(get = "pub")]
pub(crate) cmx: Option<[u8; 32]>,
#[getset(get = "pub")]
pub(crate) ephemeral_key: [u8; 32],
#[getset(get = "pub")]
pub(crate) enc_ciphertext: EncCiphertext,
#[getset(get = "pub")]
pub(crate) out_ciphertext: Vec<u8>,
#[getset(get = "pub")]
pub(crate) recipient: Option<[u8; 43]>,
#[getset(get = "pub")]
pub(crate) value: Option<u64>,
#[getset(get = "pub")]
pub(crate) rseed: Option<[u8; 32]>,
pub(crate) ock: Option<[u8; 32]>,
pub(crate) zip32_derivation: Option<Zip32Derivation>,
#[getset(get = "pub")]
pub(crate) user_address: Option<String>,
#[getset(get = "pub")]
pub(crate) proprietary: BTreeMap<String, Vec<u8>>,
}
pub mod v1 {
use alloc::{collections::BTreeMap, string::String, vec::Vec};
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use crate::common::Zip32Derivation;
use super::NoteVersion;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Bundle {
actions: Vec<Action>,
flags: u8,
value_sum: (u64, bool),
anchor: [u8; 32],
zkproof: Option<Vec<u8>>,
bsk: Option<[u8; 32]>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct Action {
cv_net: [u8; 32],
spend: Spend,
output: Output,
rcv: Option<[u8; 32]>,
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct Spend {
nullifier: [u8; 32],
rk: [u8; 32],
#[serde_as(as = "Option<[_; 64]>")]
spend_auth_sig: Option<[u8; 64]>,
#[serde_as(as = "Option<[_; 43]>")]
recipient: Option<[u8; 43]>,
value: Option<u64>,
rho: Option<[u8; 32]>,
rseed: Option<[u8; 32]>,
#[serde_as(as = "Option<[_; 96]>")]
fvk: Option<[u8; 96]>,
witness: Option<(u32, [[u8; 32]; 32])>,
alpha: Option<[u8; 32]>,
zip32_derivation: Option<Zip32Derivation>,
dummy_sk: Option<[u8; 32]>,
proprietary: BTreeMap<String, Vec<u8>>,
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct Output {
cmx: [u8; 32],
ephemeral_key: [u8; 32],
enc_ciphertext: Vec<u8>,
out_ciphertext: Vec<u8>,
#[serde_as(as = "Option<[_; 43]>")]
recipient: Option<[u8; 43]>,
value: Option<u64>,
rseed: Option<[u8; 32]>,
ock: Option<[u8; 32]>,
zip32_derivation: Option<Zip32Derivation>,
user_address: Option<String>,
proprietary: BTreeMap<String, Vec<u8>>,
}
impl TryFrom<super::Bundle> for Bundle {
type Error = crate::EncodingError;
fn try_from(bundle: super::Bundle) -> Result<Self, Self::Error> {
if bundle.note_version != NoteVersion::V2 {
return Err(crate::EncodingError::UnsupportedOrchardNoteVersion);
}
let anchor = match bundle.anchor {
Some(anchor) => anchor,
None if bundle.actions.is_empty() => super::DEFAULT_ANCHOR,
None => return Err(crate::EncodingError::RequiresV2),
};
Ok(Self {
actions: bundle
.actions
.into_iter()
.map(Action::try_from)
.collect::<Result<Vec<_>, _>>()?,
flags: bundle.flags,
value_sum: bundle.value_sum,
anchor,
zkproof: bundle.zkproof,
bsk: bundle.bsk,
})
}
}
impl From<Bundle> for super::Bundle {
fn from(bundle: Bundle) -> Self {
let anchor = if bundle.actions.is_empty() && bundle.anchor == super::DEFAULT_ANCHOR {
None
} else {
Some(bundle.anchor)
};
Self {
actions: bundle
.actions
.into_iter()
.map(super::Action::from)
.collect(),
flags: bundle.flags,
value_sum: bundle.value_sum,
anchor,
note_version: NoteVersion::V2,
zkproof: bundle.zkproof,
bsk: bundle.bsk,
}
}
}
impl TryFrom<super::Action> for Action {
type Error = crate::EncodingError;
fn try_from(action: super::Action) -> Result<Self, Self::Error> {
Ok(Self {
cv_net: action.cv_net.ok_or(crate::EncodingError::RequiresV2)?,
spend: Spend::from(action.spend),
output: Output::try_from(action.output)?,
rcv: action.rcv,
})
}
}
impl From<Action> for super::Action {
fn from(action: Action) -> Self {
Self {
cv_net: Some(action.cv_net),
spend: super::Spend::from(action.spend),
output: super::Output::from(action.output),
rcv: action.rcv,
}
}
}
impl From<super::Spend> for Spend {
fn from(spend: super::Spend) -> Self {
Self {
nullifier: spend.nullifier,
rk: spend.rk,
spend_auth_sig: spend.spend_auth_sig,
recipient: spend.recipient,
value: spend.value,
rho: spend.rho,
rseed: spend.rseed,
fvk: spend.fvk,
witness: spend.witness,
alpha: spend.alpha,
zip32_derivation: spend.zip32_derivation,
dummy_sk: spend.dummy_sk,
proprietary: spend.proprietary,
}
}
}
impl From<Spend> for super::Spend {
fn from(spend: Spend) -> Self {
Self {
nullifier: spend.nullifier,
rk: spend.rk,
spend_auth_sig: spend.spend_auth_sig,
recipient: spend.recipient,
value: spend.value,
rho: spend.rho,
rseed: spend.rseed,
fvk: spend.fvk,
witness: spend.witness,
alpha: spend.alpha,
zip32_derivation: spend.zip32_derivation,
dummy_sk: spend.dummy_sk,
proprietary: spend.proprietary,
}
}
}
impl TryFrom<super::Output> for Output {
type Error = crate::EncodingError;
fn try_from(output: super::Output) -> Result<Self, Self::Error> {
let enc_ciphertext = output
.enc_ciphertext
.into_encrypted()
.ok_or(crate::EncodingError::RequiresV2)?;
Ok(Self {
cmx: output.cmx.ok_or(crate::EncodingError::RequiresV2)?,
ephemeral_key: output.ephemeral_key,
enc_ciphertext,
out_ciphertext: output.out_ciphertext,
recipient: output.recipient,
value: output.value,
rseed: output.rseed,
ock: output.ock,
zip32_derivation: output.zip32_derivation,
user_address: output.user_address,
proprietary: output.proprietary,
})
}
}
impl From<Output> for super::Output {
fn from(output: Output) -> Self {
Self {
cmx: Some(output.cmx),
ephemeral_key: output.ephemeral_key,
enc_ciphertext: super::EncCiphertext::Encrypted(output.enc_ciphertext),
out_ciphertext: output.out_ciphertext,
recipient: output.recipient,
value: output.value,
rseed: output.rseed,
ock: output.ock,
zip32_derivation: output.zip32_derivation,
user_address: output.user_address,
proprietary: output.proprietary,
}
}
}
}
pub(crate) mod v2 {
use alloc::{collections::BTreeMap, string::String, vec::Vec};
use getset::Getters;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use super::NoteVersion;
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
enum SerializedNoteVersion {
V2,
V3,
}
impl From<NoteVersion> for SerializedNoteVersion {
fn from(note_version: NoteVersion) -> Self {
match note_version {
NoteVersion::V2 => Self::V2,
NoteVersion::V3 => Self::V3,
}
}
}
impl From<SerializedNoteVersion> for NoteVersion {
fn from(note_version: SerializedNoteVersion) -> Self {
match note_version {
SerializedNoteVersion::V2 => Self::V2,
SerializedNoteVersion::V3 => Self::V3,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Getters)]
pub struct Bundle {
actions: Vec<Action>,
flags: u8,
value_sum: (u64, bool),
anchor: Option<[u8; 32]>,
note_version: SerializedNoteVersion,
zkproof: Option<Vec<u8>>,
bsk: Option<[u8; 32]>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct Action {
cv_net: Option<[u8; 32]>,
spend: Spend,
output: Output,
rcv: Option<[u8; 32]>,
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct Spend {
#[serde_as(as = "Option<[_; 32]>")]
nullifier: Option<[u8; 32]>,
#[serde_as(as = "Option<[_; 32]>")]
rk: Option<[u8; 32]>,
#[serde_as(as = "Option<[_; 64]>")]
spend_auth_sig: Option<[u8; 64]>,
#[serde_as(as = "Option<[_; 43]>")]
recipient: Option<[u8; 43]>,
value: Option<u64>,
rho: Option<[u8; 32]>,
rseed: Option<[u8; 32]>,
#[serde_as(as = "Option<[_; 96]>")]
fvk: Option<[u8; 96]>,
witness: Option<(u32, [[u8; 32]; 32])>,
alpha: Option<[u8; 32]>,
zip32_derivation: Option<crate::common::Zip32Derivation>,
dummy_sk: Option<[u8; 32]>,
proprietary: BTreeMap<String, Vec<u8>>,
}
#[serde_as]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct Output {
cmx: Option<[u8; 32]>,
ephemeral_key: [u8; 32],
enc_ciphertext: super::EncCiphertext,
out_ciphertext: Vec<u8>,
#[serde_as(as = "Option<[_; 43]>")]
recipient: Option<[u8; 43]>,
value: Option<u64>,
rseed: Option<[u8; 32]>,
ock: Option<[u8; 32]>,
zip32_derivation: Option<crate::common::Zip32Derivation>,
user_address: Option<String>,
proprietary: BTreeMap<String, Vec<u8>>,
}
impl TryFrom<super::Bundle> for Bundle {
type Error = crate::EncodingError;
fn try_from(bundle: super::Bundle) -> Result<Self, Self::Error> {
Ok(Self {
actions: bundle
.actions
.into_iter()
.map(Action::from)
.collect::<Vec<_>>(),
flags: bundle.flags,
value_sum: bundle.value_sum,
anchor: bundle.anchor,
note_version: bundle.note_version.into(),
zkproof: bundle.zkproof,
bsk: bundle.bsk,
})
}
}
impl Bundle {
pub(crate) fn into_logical(self) -> Result<super::Bundle, crate::ParseError> {
Ok(super::Bundle {
actions: self
.actions
.into_iter()
.map(Action::into_logical)
.collect::<Result<Vec<_>, _>>()?,
flags: self.flags,
value_sum: self.value_sum,
anchor: self.anchor,
note_version: self.note_version.into(),
zkproof: self.zkproof,
bsk: self.bsk,
})
}
}
impl From<super::Action> for Action {
fn from(action: super::Action) -> Self {
Self {
cv_net: action.cv_net,
spend: Spend::from(action.spend),
output: Output::from(action.output),
rcv: action.rcv,
}
}
}
impl Action {
fn into_logical(self) -> Result<super::Action, crate::ParseError> {
Ok(super::Action {
cv_net: self.cv_net,
spend: self.spend.into_logical()?,
output: super::Output::from(self.output),
rcv: self.rcv,
})
}
}
impl From<super::Spend> for Spend {
fn from(spend: super::Spend) -> Self {
Self {
nullifier: Some(spend.nullifier),
rk: Some(spend.rk),
spend_auth_sig: spend.spend_auth_sig,
recipient: spend.recipient,
value: spend.value,
rho: spend.rho,
rseed: spend.rseed,
fvk: spend.fvk,
witness: spend.witness,
alpha: spend.alpha,
zip32_derivation: spend.zip32_derivation,
dummy_sk: spend.dummy_sk,
proprietary: spend.proprietary,
}
}
}
impl Spend {
fn into_logical(self) -> Result<super::Spend, crate::ParseError> {
Ok(super::Spend {
nullifier: self
.nullifier
.ok_or(crate::ParseError::MissingRequiredField(
"orchard.actions[].spend.nullifier",
))?,
rk: self.rk.ok_or(crate::ParseError::MissingRequiredField(
"orchard.actions[].spend.rk",
))?,
spend_auth_sig: self.spend_auth_sig,
recipient: self.recipient,
value: self.value,
rho: self.rho,
rseed: self.rseed,
fvk: self.fvk,
witness: self.witness,
alpha: self.alpha,
zip32_derivation: self.zip32_derivation,
dummy_sk: self.dummy_sk,
proprietary: self.proprietary,
})
}
}
impl From<super::Output> for Output {
fn from(output: super::Output) -> Self {
Self {
cmx: output.cmx,
ephemeral_key: output.ephemeral_key,
enc_ciphertext: output.enc_ciphertext,
out_ciphertext: output.out_ciphertext,
recipient: output.recipient,
value: output.value,
rseed: output.rseed,
ock: output.ock,
zip32_derivation: output.zip32_derivation,
user_address: output.user_address,
proprietary: output.proprietary,
}
}
}
impl From<Output> for super::Output {
fn from(output: Output) -> Self {
Self {
cmx: output.cmx,
ephemeral_key: output.ephemeral_key,
enc_ciphertext: output.enc_ciphertext,
out_ciphertext: output.out_ciphertext,
recipient: output.recipient,
value: output.value,
rseed: output.rseed,
ock: output.ock,
zip32_derivation: output.zip32_derivation,
user_address: output.user_address,
proprietary: output.proprietary,
}
}
}
pub(crate) fn encode(
bundle: super::Bundle,
empty: &super::Bundle,
) -> Result<Option<Bundle>, crate::EncodingError> {
(!is_default_empty(&bundle, empty))
.then(|| Bundle::try_from(bundle))
.transpose()
}
fn is_default_empty(bundle: &super::Bundle, empty: &super::Bundle) -> bool {
let mut bundle = bundle.clone();
if bundle.anchor == Some(super::DEFAULT_ANCHOR) {
bundle.anchor = None;
}
bundle == *empty
}
#[cfg(test)]
mod tests {
use alloc::{collections::BTreeMap, vec::Vec};
#[cfg(feature = "orchard")]
use {
crate::roles::{creator::Creator, redactor::Redactor},
::orchard::{
Note,
keys::{FullViewingKey, Scope, SpendingKey},
note::{ExtractedNoteCommitment, RandomSeed, Rho},
note_encryption::{OrchardDomain, OrchardNoteEncryption},
value::NoteValue,
},
zcash_note_encryption::Domain,
zcash_protocol::consensus::BranchId,
};
use super::super::{
Action as LogicalAction, Bundle as LogicalBundle, EMPTY_ORCHARD, EncCiphertext,
MEMO_SIZE, MemoPlaintext, NoteVersion, ORCHARD_SPENDS_AND_OUTPUTS_ENABLED, Output,
Spend,
};
fn logical_action(cv_net: Option<[u8; 32]>, cmx: Option<[u8; 32]>) -> LogicalAction {
LogicalAction {
cv_net,
spend: Spend {
nullifier: [1; 32],
rk: [2; 32],
spend_auth_sig: None,
recipient: None,
value: None,
rho: None,
rseed: None,
fvk: None,
witness: None,
alpha: None,
zip32_derivation: None,
dummy_sk: None,
proprietary: BTreeMap::new(),
},
output: Output {
cmx,
ephemeral_key: [4; 32],
enc_ciphertext: EncCiphertext::Encrypted(Vec::new()),
out_ciphertext: Vec::new(),
recipient: None,
value: None,
rseed: None,
ock: None,
zip32_derivation: None,
user_address: None,
proprietary: BTreeMap::new(),
},
rcv: None,
}
}
fn logical_bundle(anchor: Option<[u8; 32]>, cv_net: Option<[u8; 32]>) -> LogicalBundle {
logical_bundle_with_cmx(anchor, cv_net, Some([3; 32]))
}
fn logical_bundle_with_cmx(
anchor: Option<[u8; 32]>,
cv_net: Option<[u8; 32]>,
cmx: Option<[u8; 32]>,
) -> LogicalBundle {
LogicalBundle {
actions: vec![logical_action(cv_net, cmx)],
flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
value_sum: (0, false),
anchor,
note_version: NoteVersion::V2,
zkproof: None,
bsk: None,
}
}
#[test]
fn anchor_cv_net_and_cmx_round_trip_optional_encoding() {
for (anchor, cv_net, cmx) in [
(None, None, None),
(Some([5; 32]), Some([6; 32]), Some([7; 32])),
] {
let bundle = logical_bundle_with_cmx(anchor, cv_net, cmx);
let encoded = super::Bundle::try_from(bundle.clone()).unwrap();
assert_eq!(encoded.anchor, anchor);
assert_eq!(encoded.actions[0].cv_net, cv_net);
assert_eq!(encoded.actions[0].output.cmx, cmx);
let decoded = encoded.into_logical().unwrap();
assert_eq!(decoded, bundle);
}
}
#[test]
fn missing_spend_nullifier_or_rk_is_rejected() {
let bundle = logical_bundle(Some([5; 32]), Some([6; 32]));
for (clear_field, missing_field) in [
(
(|spend: &mut super::Spend| spend.nullifier = None) as fn(&mut super::Spend),
"orchard.actions[].spend.nullifier",
),
(
|spend: &mut super::Spend| spend.rk = None,
"orchard.actions[].spend.rk",
),
] {
let mut encoded = super::Bundle::try_from(bundle.clone()).unwrap();
assert_eq!(encoded.actions[0].spend.nullifier, Some([1; 32]));
assert_eq!(encoded.actions[0].spend.rk, Some([2; 32]));
clear_field(&mut encoded.actions[0].spend);
assert!(matches!(
encoded.into_logical(),
Err(crate::ParseError::MissingRequiredField(field)) if field == missing_field
));
}
}
#[test]
fn memo_plaintext_strips_and_expands_trailing_zeroes() {
let mut memo = [0; MEMO_SIZE];
memo[..5].copy_from_slice(b"hello");
let plaintext = MemoPlaintext::from_memo(memo);
assert_eq!(plaintext.as_stripped_bytes(), b"hello");
assert_eq!(plaintext.to_memo(), memo);
}
#[cfg(feature = "orchard")]
fn decryptable_action_with_memo(memo: [u8; MEMO_SIZE]) -> LogicalAction {
let mut nullifier = [0; 32];
nullifier[0] = 1;
let rho = Option::from(Rho::from_bytes(&nullifier)).unwrap();
let (_, rseed) = (0u8..)
.find_map(|i| {
let mut rseed = [0; 32];
rseed[0] = i;
Option::from(RandomSeed::from_bytes(rseed, &rho)).map(|parsed| (rseed, parsed))
})
.unwrap();
let recipient = FullViewingKey::from(&SpendingKey::from_bytes([0; 32]).unwrap())
.address_at(0u32, Scope::External);
let value = NoteValue::from_raw(100_000);
let note = Option::from(Note::from_parts(
recipient,
value,
rho,
rseed,
NoteVersion::V2,
))
.unwrap();
let encryptor = OrchardNoteEncryption::new(None, note, memo);
LogicalAction {
cv_net: Some([0; 32]),
spend: Spend {
nullifier,
rk: [2; 32],
spend_auth_sig: None,
recipient: None,
value: None,
rho: None,
rseed: None,
fvk: None,
witness: None,
alpha: None,
zip32_derivation: None,
dummy_sk: None,
proprietary: BTreeMap::new(),
},
output: Output {
cmx: Some(ExtractedNoteCommitment::from(note.commitment()).to_bytes()),
ephemeral_key: OrchardDomain::epk_bytes(encryptor.epk()).0,
enc_ciphertext: EncCiphertext::Encrypted(
encryptor.encrypt_note_plaintext().to_vec(),
),
out_ciphertext: Vec::new(),
recipient: Some(recipient.to_raw_address_bytes()),
value: Some(value.inner()),
rseed: Some(*note.rseed().as_bytes()),
ock: None,
zip32_derivation: None,
user_address: None,
proprietary: BTreeMap::new(),
},
rcv: None,
}
}
#[cfg(feature = "orchard")]
#[test]
fn v2_round_trips_memo_plaintext_ciphertext_data() {
const HELLO_MEMO_PAYLOAD_SIZE_REDUCTION: usize = 575;
const HELLO_MEMO_SERIALIZED_SIZE_REDUCTION: usize =
HELLO_MEMO_PAYLOAD_SIZE_REDUCTION + 1;
let mut memo = [0; MEMO_SIZE];
memo[..5].copy_from_slice(b"hello");
let mut pczt = Creator::new(
BranchId::Nu6.into(),
10_000_000,
133,
Some([0; 32]),
Some([0; 32]),
)
.unwrap()
.build()
.unwrap();
pczt.orchard
.actions
.push(decryptable_action_with_memo(memo));
let encrypted_size = crate::v2::Pczt::try_from(pczt.clone())
.unwrap()
.serialize()
.len();
let redacted = Redactor::new(pczt)
.redact_orchard_with(|mut orchard| {
orchard.redact_actions(|mut action| {
action
.replace_enc_ciphertext_with_decrypted_memo_plaintext(NoteVersion::V2);
});
})
.finish();
let redacted_size = crate::v2::Pczt::try_from(redacted.clone())
.unwrap()
.serialize()
.len();
assert_eq!(
redacted.orchard.actions[0].output.enc_ciphertext,
EncCiphertext::MemoPlaintext(MemoPlaintext::from_memo(memo))
);
assert_eq!(
encrypted_size - redacted_size,
HELLO_MEMO_SERIALIZED_SIZE_REDUCTION
);
let decoded = crate::parse(&redacted.serialize().unwrap()).unwrap();
assert_eq!(
decoded.orchard.actions[0].output.enc_ciphertext,
EncCiphertext::MemoPlaintext(MemoPlaintext::from_memo(memo))
);
}
#[cfg(feature = "orchard")]
#[test]
fn resolve_fields_recomputes_cmx() {
let action = decryptable_action_with_memo([0; MEMO_SIZE]);
let expected_cmx = action.output.cmx;
let mut bundle = LogicalBundle {
actions: vec![action],
flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
value_sum: (0, false),
anchor: None,
note_version: NoteVersion::V2,
zkproof: None,
bsk: None,
};
bundle.actions[0].output.cmx = None;
bundle.resolve_fields().unwrap();
assert_eq!(bundle.actions[0].output.cmx, expected_cmx);
}
#[cfg(feature = "orchard")]
#[test]
fn decrypted_memo_plaintext_compaction_skips_decryption_failure() {
let mut action = decryptable_action_with_memo([0; MEMO_SIZE]);
let original_enc_ciphertext = match &mut action.output.enc_ciphertext {
EncCiphertext::Encrypted(enc_ciphertext) => {
enc_ciphertext[0] ^= 1;
enc_ciphertext.clone()
}
EncCiphertext::MemoPlaintext(_) => unreachable!("helper encrypts memo plaintext"),
};
action.replace_enc_ciphertext_with_decrypted_memo_plaintext(NoteVersion::V2);
assert_eq!(
action.output.enc_ciphertext,
EncCiphertext::Encrypted(original_enc_ciphertext)
);
}
#[cfg(feature = "orchard")]
#[test]
fn resolvable_field_compaction_checks_derived_values() {
let mut memo = [0; MEMO_SIZE];
memo[..5].copy_from_slice(b"hello");
let mut action = decryptable_action_with_memo(memo);
action.spend.value = Some(200_000);
action.rcv = Some([3; 32]);
action.cv_net = Some(super::super::testing::value_commitment(100_000, [3; 32]));
action.compact_resolvable_fields(NoteVersion::V2);
assert_eq!(action.cv_net, None);
assert_eq!(action.output.cmx, None);
assert_eq!(
action.output.enc_ciphertext,
EncCiphertext::MemoPlaintext(MemoPlaintext::from_memo(memo))
);
}
#[cfg(feature = "orchard")]
#[test]
fn resolvable_field_compaction_retains_unverifiable_values() {
let mut memo = [0; MEMO_SIZE];
memo[..5].copy_from_slice(b"hello");
let mut action = decryptable_action_with_memo(memo);
let original_cv_net = action.cv_net;
let original_cmx = action.output.cmx;
let original_enc_ciphertext = action.output.enc_ciphertext.clone();
action.output.recipient = None;
action.compact_resolvable_fields(NoteVersion::V2);
assert_eq!(action.cv_net, original_cv_net);
assert_eq!(action.output.cmx, original_cmx);
assert_eq!(action.output.enc_ciphertext, original_enc_ciphertext);
}
#[cfg(feature = "orchard")]
#[test]
fn resolvable_field_compaction_retains_mismatched_values() {
let mut memo = [0; MEMO_SIZE];
memo[..5].copy_from_slice(b"hello");
let mut action = decryptable_action_with_memo(memo);
action.spend.value = Some(200_000);
action.rcv = Some([3; 32]);
let mut cv_net = super::super::testing::value_commitment(100_000, [3; 32]);
cv_net[0] ^= 1;
action.cv_net = Some(cv_net);
let mut cmx = action.output.cmx.unwrap();
cmx[0] ^= 1;
action.output.cmx = Some(cmx);
let original_enc_ciphertext = action.output.enc_ciphertext.clone();
action.compact_resolvable_fields(NoteVersion::V2);
assert_eq!(action.cv_net, Some(cv_net));
assert_eq!(action.output.cmx, Some(cmx));
assert_eq!(action.output.enc_ciphertext, original_enc_ciphertext);
}
#[test]
fn v1_rejects_memo_plaintext_ciphertext_data() {
let mut bundle = logical_bundle(Some([5; 32]), Some([6; 32]));
bundle.actions[0].output.enc_ciphertext =
EncCiphertext::MemoPlaintext(MemoPlaintext::from_memo([0; MEMO_SIZE]));
assert!(matches!(
crate::orchard::v1::Bundle::try_from(bundle),
Err(crate::EncodingError::RequiresV2)
));
}
#[test]
fn v1_rejects_missing_anchor_and_cv_net() {
assert!(matches!(
crate::orchard::v1::Bundle::try_from(logical_bundle(None, Some([6; 32]))),
Err(crate::EncodingError::RequiresV2)
));
assert!(matches!(
crate::orchard::v1::Bundle::try_from(logical_bundle(Some([5; 32]), None)),
Err(crate::EncodingError::RequiresV2)
));
assert!(matches!(
crate::orchard::v1::Bundle::try_from(logical_bundle_with_cmx(
Some([5; 32]),
Some([6; 32]),
None
)),
Err(crate::EncodingError::RequiresV2)
));
}
#[test]
fn v1_empty_bundle_anchor_falls_back_to_default() {
let bundle = LogicalBundle {
actions: Vec::new(),
flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
value_sum: (0, false),
anchor: None,
note_version: NoteVersion::V2,
zkproof: None,
bsk: None,
};
let encoded = crate::orchard::v1::Bundle::try_from(bundle)
.expect("an empty bundle's anchor falls back to DEFAULT_ANCHOR");
let decoded = super::super::Bundle::from(encoded);
assert_eq!(decoded, EMPTY_ORCHARD);
}
}
}
impl Bundle {
pub fn sole_action(&self) -> Option<&Action> {
match self.actions.as_slice() {
[action] => Some(action),
_ => None,
}
}
pub fn value_carrying_outputs_all_pay(&self, recipient: &[u8; 43]) -> Option<bool> {
for action in &self.actions {
let output = &action.output;
if output.value? == 0 {
continue;
}
if &output.recipient? != recipient {
return Some(false);
}
}
Some(true)
}
pub(crate) fn merge(
mut self,
other: Self,
self_global: &Global,
other_global: &Global,
) -> Option<Self> {
let Self {
mut actions,
flags,
value_sum,
anchor,
note_version,
zkproof,
bsk,
} = other;
if self.flags != flags || self.note_version != note_version {
return None;
}
match (self.bsk.as_mut(), bsk) {
(Some(lhs), Some(rhs)) if lhs != &rhs => return None,
(Some(_), _) | (_, Some(_))
if self.actions.len() != actions.len() || self.value_sum != value_sum =>
{
return None;
}
(Some(_), _) | (_, Some(_)) => (),
(None, None) => match (
self_global.shielded_modifiable(),
other_global.shielded_modifiable(),
self.actions.len().cmp(&actions.len()),
) {
(false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None,
(true, _, Ordering::Less) => {
self.actions.extend(actions.drain(self.actions.len()..));
self.value_sum = value_sum;
}
(_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (),
},
}
if !merge_optional(&mut self.anchor, anchor) {
return None;
}
if !merge_optional(&mut self.zkproof, zkproof) {
return None;
}
for (lhs, rhs) in self.actions.iter_mut().zip(actions) {
let Action {
cv_net,
spend:
Spend {
nullifier,
rk,
spend_auth_sig,
recipient,
value,
rho,
rseed,
fvk,
witness,
alpha,
zip32_derivation: spend_zip32_derivation,
dummy_sk,
proprietary: spend_proprietary,
},
output:
Output {
cmx,
ephemeral_key,
enc_ciphertext,
out_ciphertext,
recipient: output_recipient,
value: output_value,
rseed: output_rseed,
ock,
zip32_derivation: output_zip32_derivation,
user_address,
proprietary: output_proprietary,
},
rcv,
} = rhs;
if lhs.spend.nullifier != nullifier
|| lhs.spend.rk != rk
|| lhs.output.ephemeral_key != ephemeral_key
|| lhs.output.enc_ciphertext != enc_ciphertext
|| lhs.output.out_ciphertext != out_ciphertext
{
return None;
}
if !(merge_optional(&mut lhs.cv_net, cv_net)
&& merge_optional(&mut lhs.spend.spend_auth_sig, spend_auth_sig)
&& merge_optional(&mut lhs.spend.recipient, recipient)
&& merge_optional(&mut lhs.spend.value, value)
&& merge_optional(&mut lhs.spend.rho, rho)
&& merge_optional(&mut lhs.spend.rseed, rseed)
&& merge_optional(&mut lhs.spend.fvk, fvk)
&& merge_optional(&mut lhs.spend.witness, witness)
&& merge_optional(&mut lhs.spend.alpha, alpha)
&& merge_optional(&mut lhs.spend.zip32_derivation, spend_zip32_derivation)
&& merge_optional(&mut lhs.spend.dummy_sk, dummy_sk)
&& merge_map(&mut lhs.spend.proprietary, spend_proprietary)
&& merge_optional(&mut lhs.output.cmx, cmx)
&& merge_optional(&mut lhs.output.recipient, output_recipient)
&& merge_optional(&mut lhs.output.value, output_value)
&& merge_optional(&mut lhs.output.rseed, output_rseed)
&& merge_optional(&mut lhs.output.ock, ock)
&& merge_optional(&mut lhs.output.zip32_derivation, output_zip32_derivation)
&& merge_optional(&mut lhs.output.user_address, user_address)
&& merge_map(&mut lhs.output.proprietary, output_proprietary)
&& merge_optional(&mut lhs.rcv, rcv))
{
return None;
}
}
Some(self)
}
}
#[cfg(feature = "orchard")]
pub(crate) fn bundle_version_for_revision(
revision: zcash_protocol::consensus::OrchardProtocolRevision,
pool: orchard::ValuePool,
) -> Option<BundleVersion> {
match pool {
ValuePool::Orchard => Some(match revision {
OrchardProtocolRevision::InsecureV1 => BundleVersion::orchard_insecure_v1(),
OrchardProtocolRevision::V2 => BundleVersion::orchard_v2(),
OrchardProtocolRevision::V3 => BundleVersion::orchard_v3(),
}),
ValuePool::Ironwood => match revision {
OrchardProtocolRevision::InsecureV1 | OrchardProtocolRevision::V2 => None,
OrchardProtocolRevision::V3 => Some(BundleVersion::ironwood_v3()),
},
}
}
#[cfg(feature = "orchard")]
pub(crate) fn orchard_bundle_version(global: &crate::common::Global) -> Option<BundleVersion> {
BranchId::try_from(global.consensus_branch_id)
.ok()?
.orchard_protocol_revision()
.and_then(|revision| bundle_version_for_revision(revision, orchard::ValuePool::Orchard))
}
#[cfg(feature = "orchard")]
#[derive(Debug)]
#[non_exhaustive]
pub enum ParseError {
MissingAnchor,
Bundle(orchard::pczt::ParseError),
}
#[cfg(feature = "orchard")]
impl From<orchard::pczt::ParseError> for ParseError {
fn from(e: orchard::pczt::ParseError) -> Self {
ParseError::Bundle(e)
}
}
#[cfg(all(feature = "orchard", feature = "prover"))]
#[derive(Debug)]
#[non_exhaustive]
pub enum AnchorConsistencyError {
IncompleteSpendData,
WitnessDoesNotRootToAnchor,
}
#[cfg(all(feature = "orchard", feature = "prover"))]
pub(crate) fn verify_witnesses_root_to_anchor(
bundle: &orchard::pczt::Bundle,
anchor: orchard::Anchor,
) -> Result<(), AnchorConsistencyError> {
for action in bundle.actions() {
let spend = action.spend();
let Some(witness) = spend.witness() else {
continue;
};
let Some(value) = spend.value() else {
continue;
};
if value.inner() == 0 {
continue;
}
let recipient = spend
.recipient()
.ok_or(AnchorConsistencyError::IncompleteSpendData)?;
let rho = spend
.rho()
.ok_or(AnchorConsistencyError::IncompleteSpendData)?;
let rseed = spend
.rseed()
.ok_or(AnchorConsistencyError::IncompleteSpendData)?;
let note = orchard::Note::from_parts(recipient, *value, rho, rseed, *spend.note_version())
.into_option()
.ok_or(AnchorConsistencyError::IncompleteSpendData)?;
let cmx = orchard::note::ExtractedNoteCommitment::from(note.commitment());
let computed_anchor = witness.root(cmx);
if computed_anchor != anchor {
return Err(AnchorConsistencyError::WitnessDoesNotRootToAnchor);
}
}
Ok(())
}
#[cfg(feature = "orchard")]
impl Output {
fn resolve_cmx(
&mut self,
note_version: NoteVersion,
spend_nullifier: [u8; 32],
) -> Result<(), orchard::pczt::ParseError> {
if self.cmx.is_some() {
return Ok(());
}
let recipient = Address::from_raw_address_bytes(
self.recipient
.as_ref()
.ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?,
)
.into_option()
.ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?;
let rho = Rho::from_bytes(&spend_nullifier)
.into_option()
.ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?;
let rseed = RandomSeed::from_bytes(
*self
.rseed
.as_ref()
.ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?,
&rho,
)
.into_option()
.ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?;
let note = Note::from_parts(
recipient,
NoteValue::from_raw(
self.value
.ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?,
),
rho,
rseed,
note_version,
)
.into_option()
.ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?;
self.cmx = Some(ExtractedNoteCommitment::from(note.commitment()).to_bytes());
Ok(())
}
fn encrypt_ciphertext_from_memo(
&mut self,
note_version: NoteVersion,
spend_nullifier: [u8; 32],
) -> Result<(), orchard::pczt::ParseError> {
let memo: [u8; 512] = match &self.enc_ciphertext {
EncCiphertext::Encrypted(_) => return Ok(()),
EncCiphertext::MemoPlaintext(memo) => memo.to_memo(),
};
let recipient = Address::from_raw_address_bytes(
self.recipient
.as_ref()
.ok_or(orchard::pczt::ParseError::InvalidRecipient)?,
)
.into_option()
.ok_or(orchard::pczt::ParseError::InvalidRecipient)?;
let rho = Rho::from_bytes(&spend_nullifier)
.into_option()
.ok_or(orchard::pczt::ParseError::InvalidNullifier)?;
let rseed = RandomSeed::from_bytes(
*self
.rseed
.as_ref()
.ok_or(orchard::pczt::ParseError::InvalidRandomSeed)?,
&rho,
)
.into_option()
.ok_or(orchard::pczt::ParseError::InvalidRandomSeed)?;
let note = Note::from_parts(
recipient,
NoteValue::from_raw(
self.value
.ok_or(orchard::pczt::ParseError::InvalidEncCiphertext)?,
),
rho,
rseed,
note_version,
)
.into_option()
.ok_or(orchard::pczt::ParseError::InvalidEncCiphertext)?;
let encryptor = OrchardNoteEncryption::new(None, note, memo);
let ephemeral_key = OrchardDomain::epk_bytes(encryptor.epk()).0;
let enc_ciphertext = encryptor.encrypt_note_plaintext().to_vec();
if ephemeral_key != self.ephemeral_key {
return Err(orchard::pczt::ParseError::InvalidEncCiphertext);
}
self.enc_ciphertext = EncCiphertext::Encrypted(enc_ciphertext);
Ok(())
}
}
#[cfg(feature = "orchard")]
impl Action {
fn resolve_cv_net(&mut self) -> Result<(), orchard::pczt::ParseError> {
if self.cv_net.is_some() {
return Ok(());
}
let spend_value: NoteValue = NoteValue::from_raw(
self.spend
.value
.ok_or(orchard::pczt::ParseError::InvalidValueCommitment)?,
);
let output_value = NoteValue::from_raw(
self.output
.value
.ok_or(orchard::pczt::ParseError::InvalidValueCommitment)?,
);
let rcv = ValueCommitTrapdoor::from_bytes(
self.rcv
.ok_or(orchard::pczt::ParseError::InvalidValueCommitment)?,
)
.into_option()
.ok_or(orchard::pczt::ParseError::InvalidValueCommitment)?;
self.cv_net = Some(ValueCommitment::derive(spend_value - output_value, rcv).to_bytes());
Ok(())
}
}
#[cfg(feature = "orchard")]
impl Bundle {
pub fn resolve_fields(&mut self) -> Result<(), orchard::pczt::ParseError> {
for action in &mut self.actions {
action.resolve_cv_net()?;
action
.output
.resolve_cmx(self.note_version, action.spend.nullifier)?;
action
.output
.encrypt_ciphertext_from_memo(self.note_version, action.spend.nullifier)?;
}
Ok(())
}
pub(crate) fn into_ironwood_parsed(
self,
anchor_requirement: crate::common::AnchorRequirement,
) -> Result<Parsed, ParseError> {
self.into_parsed_with_version(BundleVersion::ironwood_v3(), anchor_requirement)
}
pub(crate) fn into_ironwood_parsed_preverified_for_signing(
self,
anchor_requirement: crate::common::AnchorRequirement,
) -> Result<Parsed, ParseError> {
self.into_parsed_with_version_preverified_for_signing(
BundleVersion::ironwood_v3(),
anchor_requirement,
)
}
#[allow(dead_code)]
pub(crate) fn into_parsed_with_version(
self,
bundle_version: BundleVersion,
anchor_requirement: crate::common::AnchorRequirement,
) -> Result<Parsed, ParseError> {
self.into_parsed_inner(bundle_version, anchor_requirement, false)
}
#[allow(dead_code)]
pub(crate) fn into_parsed_with_version_preverified_for_signing(
self,
bundle_version: BundleVersion,
anchor_requirement: crate::common::AnchorRequirement,
) -> Result<Parsed, ParseError> {
self.into_parsed_inner(bundle_version, anchor_requirement, true)
}
fn into_parsed_inner(
mut self,
bundle_version: BundleVersion,
anchor_requirement: crate::common::AnchorRequirement,
preverified: bool,
) -> Result<Parsed, ParseError> {
self.resolve_fields()?;
let wire_anchor = self.anchor;
let anchor = anchor_requirement
.resolve(wire_anchor, self.actions.is_empty())
.ok_or(ParseError::MissingAnchor)?;
#[inline(never)]
fn parse_action_inner(
mut action: Action,
note_version: NoteVersion,
preverified: bool,
) -> Result<orchard::pczt::Action, orchard::pczt::ParseError> {
let spend_zip32_derivation = action
.spend
.zip32_derivation
.map(|z| {
orchard::pczt::Zip32Derivation::parse(z.seed_fingerprint, z.derivation_path)
})
.transpose()?;
let spend_nullifier = action.spend.nullifier;
let spend = if preverified {
orchard::pczt::Spend::parse_preverified_for_signing(
action.spend.nullifier,
action.spend.rk,
action.spend.spend_auth_sig,
action.spend.recipient,
action.spend.value,
action.spend.rho,
action.spend.rseed,
action.spend.fvk,
action.spend.witness,
action.spend.alpha,
spend_zip32_derivation,
action.spend.dummy_sk,
note_version,
action.spend.proprietary,
)
} else {
orchard::pczt::Spend::parse(
action.spend.nullifier,
action.spend.rk,
action.spend.spend_auth_sig,
action.spend.recipient,
action.spend.value,
action.spend.rho,
action.spend.rseed,
action.spend.fvk,
action.spend.witness,
action.spend.alpha,
spend_zip32_derivation,
action.spend.dummy_sk,
note_version,
action.spend.proprietary,
)
}?;
action
.output
.encrypt_ciphertext_from_memo(note_version, spend_nullifier)?;
let enc_ciphertext = action
.output
.enc_ciphertext
.into_encrypted()
.ok_or(orchard::pczt::ParseError::InvalidEncCiphertext)?;
let cv_net = action
.cv_net
.ok_or(orchard::pczt::ParseError::InvalidValueCommitment)?;
let output = orchard::pczt::Output::parse(
*spend.nullifier(),
action
.output
.cmx
.ok_or(orchard::pczt::ParseError::InvalidExtractedNoteCommitment)?,
action.output.ephemeral_key,
enc_ciphertext,
action.output.out_ciphertext,
action.output.recipient,
action.output.value,
action.output.rseed,
action.output.ock,
action
.output
.zip32_derivation
.map(|z| {
orchard::pczt::Zip32Derivation::parse(z.seed_fingerprint, z.derivation_path)
})
.transpose()?,
action.output.user_address,
note_version,
action.output.proprietary,
)?;
orchard::pczt::Action::parse(cv_net, spend, output, action.rcv)
}
let note_version = self.note_version;
let mut actions = Vec::with_capacity(self.actions.len());
for action in self.actions {
actions.push(parse_action_inner(action, note_version, preverified)?);
}
let bundle = orchard::pczt::Bundle::parse(
actions,
self.flags,
bundle_version,
self.value_sum,
anchor,
self.zkproof,
self.bsk,
)?;
Ok(Parsed {
bundle,
wire_anchor,
})
}
#[allow(dead_code)]
pub(crate) fn serialize_from(bundle: orchard::pczt::Bundle) -> Self {
let note_version = bundle.bundle_version().note_version();
assert!(
bundle.actions().iter().all(|action| {
action.spend().note_version() == ¬e_version
&& action.output().note_version() == ¬e_version
}),
"Orchard PCZT bundle must have a single note version"
);
let actions = bundle
.actions()
.iter()
.map(|action| {
let spend = action.spend();
let output = action.output();
Action {
cv_net: Some(action.cv_net().to_bytes()),
spend: Spend {
nullifier: spend.nullifier().to_bytes(),
rk: spend.rk().into(),
spend_auth_sig: spend.spend_auth_sig().as_ref().map(|s| s.into()),
recipient: action
.spend()
.recipient()
.map(|recipient| recipient.to_raw_address_bytes()),
value: spend.value().map(|value| value.inner()),
rho: spend.rho().map(|rho| rho.to_bytes()),
rseed: spend.rseed().map(|rseed| *rseed.as_bytes()),
fvk: spend.fvk().as_ref().map(|fvk| fvk.to_bytes()),
witness: spend.witness().as_ref().map(|witness| {
(
u32::try_from(u64::from(witness.position()))
.expect("Sapling positions fit in u32"),
witness
.auth_path()
.iter()
.map(|node| node.to_bytes())
.collect::<Vec<_>>()[..]
.try_into()
.expect("path is length 32"),
)
}),
alpha: spend.alpha().map(|alpha| alpha.to_repr()),
zip32_derivation: spend.zip32_derivation().as_ref().map(|z| {
Zip32Derivation {
seed_fingerprint: *z.seed_fingerprint(),
derivation_path: z
.derivation_path()
.iter()
.map(|i| i.index())
.collect(),
}
}),
dummy_sk: action
.spend()
.dummy_sk()
.map(|dummy_sk| *dummy_sk.to_bytes()),
proprietary: spend.proprietary().clone(),
},
output: Output {
cmx: Some(output.cmx().to_bytes()),
ephemeral_key: output.encrypted_note().epk_bytes,
enc_ciphertext: EncCiphertext::Encrypted(
output.encrypted_note().enc_ciphertext.to_vec(),
),
out_ciphertext: output.encrypted_note().out_ciphertext.to_vec(),
recipient: action
.output()
.recipient()
.map(|recipient| recipient.to_raw_address_bytes()),
value: output.value().map(|value| value.inner()),
rseed: output.rseed().map(|rseed| *rseed.as_bytes()),
ock: output.ock().as_ref().map(|ock| ock.0),
zip32_derivation: output.zip32_derivation().as_ref().map(|z| {
Zip32Derivation {
seed_fingerprint: *z.seed_fingerprint(),
derivation_path: z
.derivation_path()
.iter()
.map(|i| i.index())
.collect(),
}
}),
user_address: output.user_address().clone(),
proprietary: output.proprietary().clone(),
},
rcv: action.rcv().as_ref().map(|rcv| rcv.to_bytes()),
}
})
.collect();
let value_sum = {
let (magnitude, sign) = bundle.value_sum().magnitude_sign();
(magnitude, matches!(sign, orchard::value::Sign::Negative))
};
let anchor = (!bundle.anchor_deferred()).then(|| bundle.anchor().to_bytes());
Self {
actions,
flags: bundle.flag_byte(),
value_sum,
anchor,
note_version,
zkproof: bundle
.zkproof()
.as_ref()
.map(|zkproof| zkproof.as_ref().to_vec()),
bsk: bundle.bsk().as_ref().map(|bsk| bsk.into()),
}
}
}
#[cfg(test)]
mod shape_tests {
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use proptest::prelude::*;
use proptest::sample::Index;
use super::{
Action, Bundle, EncCiphertext, NoteVersion, ORCHARD_SPENDS_AND_OUTPUTS_ENABLED, Output,
Spend,
};
const OURS: [u8; 43] = [1; 43];
const THEIRS: [u8; 43] = [2; 43];
fn output(value: Option<u64>, recipient: Option<[u8; 43]>) -> Output {
Output {
cmx: None,
ephemeral_key: [0; 32],
enc_ciphertext: EncCiphertext::Encrypted(Vec::new()),
out_ciphertext: Vec::new(),
recipient,
value,
rseed: None,
ock: None,
zip32_derivation: None,
user_address: None,
proprietary: BTreeMap::new(),
}
}
fn spend() -> Spend {
Spend {
nullifier: [0; 32],
rk: [0; 32],
spend_auth_sig: None,
recipient: None,
value: None,
rho: None,
rseed: None,
fvk: None,
witness: None,
alpha: None,
zip32_derivation: None,
dummy_sk: None,
proprietary: BTreeMap::new(),
}
}
fn action(value: Option<u64>, recipient: Option<[u8; 43]>) -> Action {
Action {
cv_net: None,
spend: spend(),
output: output(value, recipient),
rcv: None,
}
}
fn bundle(actions: Vec<Action>) -> Bundle {
Bundle {
actions,
flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED,
value_sum: (0, false),
anchor: None,
note_version: NoteVersion::V2,
zkproof: None,
bsk: None,
}
}
fn arb_recipient() -> impl Strategy<Value = [u8; 43]> {
prop_oneof![Just(OURS), Just(THEIRS)]
}
fn arb_value() -> impl Strategy<Value = u64> {
prop_oneof![Just(0u64), 1u64..1_000]
}
fn arb_action() -> impl Strategy<Value = Action> {
(
prop_oneof![Just(None), arb_value().prop_map(Some)],
prop_oneof![Just(None), arb_recipient().prop_map(Some)],
)
.prop_map(|(value, recipient)| action(value, recipient))
}
fn arb_bundle() -> impl Strategy<Value = Bundle> {
prop::collection::vec(arb_action(), 0..6).prop_map(bundle)
}
fn arb_specified_bundle() -> impl Strategy<Value = Bundle> {
prop::collection::vec((arb_value(), arb_recipient()), 0..6).prop_map(|outputs| {
bundle(
outputs
.into_iter()
.map(|(value, recipient)| action(Some(value), Some(recipient)))
.collect(),
)
})
}
fn arb_send_to_self_bundle() -> impl Strategy<Value = Bundle> {
prop::collection::vec((arb_value(), arb_recipient()), 1..6).prop_map(|outputs| {
bundle(
outputs
.into_iter()
.map(|(value, dummy_recipient)| {
let recipient = if value == 0 { dummy_recipient } else { OURS };
action(Some(value), Some(recipient))
})
.collect(),
)
})
}
proptest! {
#[test]
fn sole_action_answers_exactly_when_there_is_one_action(bundle in arb_bundle()) {
match bundle.sole_action() {
Some(action) => {
prop_assert_eq!(bundle.actions.len(), 1);
prop_assert_eq!(action, &bundle.actions[0]);
}
None => prop_assert_ne!(bundle.actions.len(), 1),
}
}
#[test]
fn all_pay_reads_the_value_carrying_outputs(bundle in arb_specified_bundle()) {
let expected = bundle
.actions
.iter()
.all(|a| a.output.value == Some(0) || a.output.recipient == Some(OURS));
prop_assert_eq!(bundle.value_carrying_outputs_all_pay(&OURS), Some(expected));
}
#[test]
fn zero_valued_dummies_do_not_change_the_answer(
bundle in arb_bundle(),
dummy_recipient in prop_oneof![Just(None), arb_recipient().prop_map(Some)],
at in any::<Index>(),
) {
let expected = bundle.value_carrying_outputs_all_pay(&OURS);
let mut padded = bundle.clone();
let at = at.index(padded.actions.len() + 1);
padded.actions.insert(at, action(Some(0), dummy_recipient));
prop_assert_eq!(padded.value_carrying_outputs_all_pay(&OURS), expected);
}
#[test]
fn a_redacted_value_is_unanswerable(bundle in arb_send_to_self_bundle(), at in any::<Index>()) {
prop_assert_eq!(bundle.value_carrying_outputs_all_pay(&OURS), Some(true));
let mut redacted = bundle.clone();
let at = at.index(redacted.actions.len());
redacted.actions[at].output.value = None;
prop_assert_eq!(redacted.value_carrying_outputs_all_pay(&OURS), None);
}
#[test]
fn a_redacted_recipient_is_unanswerable_only_where_it_is_judged(
bundle in arb_send_to_self_bundle(),
at in any::<Index>(),
) {
let mut redacted = bundle.clone();
let at = at.index(redacted.actions.len());
let carries_value = redacted.actions[at].output.value != Some(0);
redacted.actions[at].output.recipient = None;
let expected = if carries_value { None } else { Some(true) };
prop_assert_eq!(redacted.value_carrying_outputs_all_pay(&OURS), expected);
}
#[test]
fn all_pay_is_asked_about_a_specific_recipient(bundle in arb_send_to_self_bundle()) {
let pays_nobody = bundle
.actions
.iter()
.all(|a| a.output.value == Some(0));
prop_assert_eq!(bundle.value_carrying_outputs_all_pay(&THEIRS), Some(pays_nobody));
}
}
}