Skip to main content

pczt/
sapling.rs

1//! The Sapling fields of a PCZT.
2
3use alloc::collections::BTreeMap;
4use alloc::string::String;
5use alloc::vec::Vec;
6use core::cmp::Ordering;
7
8use getset::Getters;
9use serde::{Deserialize, Serialize};
10use serde_with::serde_as;
11
12use crate::{
13    common::{Global, SecretKeyBytes, Zip32Derivation},
14    roles::combiner::{merge_map, merge_optional},
15};
16
17const GROTH_PROOF_SIZE: usize = 48 + 96 + 48;
18
19/// PCZT fields that are specific to producing the transaction's Sapling bundle (if any).
20#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Getters)]
21pub struct Bundle {
22    #[getset(get = "pub")]
23    pub(crate) spends: Vec<Spend>,
24    #[getset(get = "pub")]
25    pub(crate) outputs: Vec<Output>,
26
27    /// The net value of Sapling spends minus outputs.
28    ///
29    /// This is initialized by the Creator, and updated by the Constructor as spends or
30    /// outputs are added to the PCZT. It enables per-spend and per-output values to be
31    /// redacted from the PCZT after they are no longer necessary.
32    #[getset(get = "pub")]
33    pub(crate) value_sum: i128,
34
35    /// The Sapling anchor for this transaction.
36    ///
37    /// Set by the Creator.
38    #[getset(get = "pub")]
39    pub(crate) anchor: Option<[u8; 32]>,
40
41    /// The Sapling binding signature signing key.
42    ///
43    /// - This is `None` until it is set by the IO Finalizer.
44    /// - The Transaction Extractor uses this to produce the binding signature.
45    pub(crate) bsk: Option<SecretKeyBytes>,
46}
47
48/// The canonical empty Sapling bundle: the form the Sapling bundle of a PCZT takes
49/// when it carries no Sapling data.
50pub(crate) const EMPTY_BUNDLE: Bundle = Bundle {
51    spends: Vec::new(),
52    outputs: Vec::new(),
53    value_sum: 0,
54    anchor: None,
55    bsk: None,
56};
57
58/// The default anchor used as a parse-time placeholder for redacted empty
59/// Sapling bundles.
60pub(crate) const DEFAULT_ANCHOR: [u8; 32] = [0; 32];
61
62/// Information about a Sapling spend within a transaction.
63#[serde_as]
64#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Getters)]
65pub struct Spend {
66    //
67    // SpendDescription effecting data.
68    //
69    // These are required fields that are part of the final transaction, and are filled in
70    // by the Constructor when adding an output.
71    //
72    #[getset(get = "pub")]
73    pub(crate) cv: [u8; 32],
74    #[getset(get = "pub")]
75    pub(crate) nullifier: [u8; 32],
76    #[getset(get = "pub")]
77    pub(crate) rk: [u8; 32],
78
79    /// The Spend proof.
80    ///
81    /// This is set by the Prover.
82    #[serde_as(as = "Option<[_; GROTH_PROOF_SIZE]>")]
83    pub(crate) zkproof: Option<[u8; GROTH_PROOF_SIZE]>,
84
85    /// The spend authorization signature.
86    ///
87    /// This is set by the Signer.
88    #[serde_as(as = "Option<[_; 64]>")]
89    pub(crate) spend_auth_sig: Option<[u8; 64]>,
90
91    /// The [raw encoding] of the Sapling payment address that received the note being spent.
92    ///
93    /// - This is set by the Constructor.
94    /// - This is required by the Prover.
95    ///
96    /// [raw encoding]: https://zips.z.cash/protocol/protocol.pdf#saplingpaymentaddrencoding
97    #[serde_as(as = "Option<[_; 43]>")]
98    pub(crate) recipient: Option<[u8; 43]>,
99
100    /// The value of the input being spent.
101    ///
102    /// This may be used by Signers to verify that the value matches `cv`, and to confirm
103    /// the values and change involved in the transaction.
104    ///
105    /// This exposes the input value to all participants. For Signers who don't need this
106    /// information, or after signatures have been applied, this can be redacted.
107    pub(crate) value: Option<u64>,
108
109    /// The note commitment randomness.
110    ///
111    /// - This is set by the Constructor. It MUST NOT be set if the note has an `rseed`
112    ///   (i.e. was created after [ZIP 212] activation).
113    /// - The Prover requires either this or `rseed`.
114    ///
115    /// [ZIP 212]: https://zips.z.cash/zip-0212
116    pub(crate) rcm: Option<[u8; 32]>,
117
118    /// The seed randomness for the note being spent.
119    ///
120    /// - This is set by the Constructor. It MUST NOT be set if the note has no `rseed`
121    ///   (i.e. was created before [ZIP 212] activation).
122    /// - The Prover requires either this or `rcm`.
123    ///
124    /// [ZIP 212]: https://zips.z.cash/zip-0212
125    pub(crate) rseed: Option<[u8; 32]>,
126
127    /// The value commitment randomness.
128    ///
129    /// - This is set by the Constructor.
130    /// - The IO Finalizer compresses it into `bsk`.
131    /// - This is required by the Prover.
132    /// - This may be used by Signers to verify that the value correctly matches `cv`.
133    ///
134    /// This opens `cv` for all participants. For Signers who don't need this information,
135    /// or after proofs / signatures have been applied, this can be redacted.
136    pub(crate) rcv: Option<[u8; 32]>,
137
138    /// The proof generation key `(ak, nsk)` corresponding to the recipient that received
139    /// the note being spent.
140    ///
141    /// - This is set by the Updater.
142    /// - This is required by the Prover.
143    pub(crate) proof_generation_key: Option<([u8; 32], [u8; 32])>,
144
145    /// A witness from the note to the bundle's anchor.
146    ///
147    /// - This is set by the Updater.
148    /// - This is required by the Prover.
149    #[getset(get = "pub")]
150    pub(crate) witness: Option<(u32, [[u8; 32]; 32])>,
151
152    /// The spend authorization randomizer.
153    ///
154    /// - This is chosen by the Constructor.
155    /// - This is required by the Signer for creating `spend_auth_sig`, and may be used to
156    ///   validate `rk`.
157    /// - After `zkproof` / `spend_auth_sig` has been set, this can be redacted.
158    pub(crate) alpha: Option<[u8; 32]>,
159
160    /// The ZIP 32 derivation path at which the spending key can be found for the note
161    /// being spent.
162    pub(crate) zip32_derivation: Option<Zip32Derivation>,
163
164    /// The spend authorizing key for this spent note, if it is a dummy note.
165    ///
166    /// - This is chosen by the Constructor.
167    /// - This is required by the IO Finalizer, and is cleared by it once used.
168    /// - Signers MUST reject PCZTs that contain `dummy_ask` values.
169    pub(crate) dummy_ask: Option<SecretKeyBytes>,
170
171    /// Proprietary fields related to the note being spent.
172    #[getset(get = "pub")]
173    pub(crate) proprietary: BTreeMap<String, Vec<u8>>,
174}
175
176/// Information about a Sapling output within a transaction.
177#[serde_as]
178#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Getters)]
179pub struct Output {
180    //
181    // OutputDescription effecting data.
182    //
183    // These are required fields that are part of the final transaction, and are filled in
184    // by the Constructor when adding an output.
185    //
186    #[getset(get = "pub")]
187    pub(crate) cv: [u8; 32],
188    #[getset(get = "pub")]
189    pub(crate) cmu: [u8; 32],
190    #[getset(get = "pub")]
191    pub(crate) ephemeral_key: [u8; 32],
192    /// The encrypted note plaintext for the output.
193    ///
194    /// Encoded as a `Vec<u8>` because its length depends on the transaction version.
195    ///
196    /// Once we have [memo bundles], we will be able to set memos independently of
197    /// Outputs. For now, the Constructor sets both at the same time.
198    ///
199    /// [memo bundles]: https://zips.z.cash/zip-0231
200    #[getset(get = "pub")]
201    pub(crate) enc_ciphertext: Vec<u8>,
202    /// The encrypted note plaintext for the output.
203    ///
204    /// Encoded as a `Vec<u8>` because its length depends on the transaction version.
205    #[getset(get = "pub")]
206    pub(crate) out_ciphertext: Vec<u8>,
207
208    /// The Output proof.
209    ///
210    /// This is set by the Prover.
211    #[serde_as(as = "Option<[_; GROTH_PROOF_SIZE]>")]
212    pub(crate) zkproof: Option<[u8; GROTH_PROOF_SIZE]>,
213
214    /// The [raw encoding] of the Sapling payment address that will receive the output.
215    ///
216    /// - This is set by the Constructor.
217    /// - This is required by the Prover.
218    ///
219    /// [raw encoding]: https://zips.z.cash/protocol/protocol.pdf#saplingpaymentaddrencoding
220    #[serde_as(as = "Option<[_; 43]>")]
221    #[getset(get = "pub")]
222    pub(crate) recipient: Option<[u8; 43]>,
223
224    /// The value of the output.
225    ///
226    /// This may be used by Signers to verify that the value matches `cv`, and to confirm
227    /// the values and change involved in the transaction.
228    ///
229    /// This exposes the output value to all participants. For Signers who don't need this
230    /// information, or after signatures have been applied, this can be redacted.
231    #[getset(get = "pub")]
232    pub(crate) value: Option<u64>,
233
234    /// The seed randomness for the output.
235    ///
236    /// - This is set by the Constructor.
237    /// - This is required by the Prover, instead of disclosing `shared_secret` to them.
238    #[getset(get = "pub")]
239    pub(crate) rseed: Option<[u8; 32]>,
240
241    /// The value commitment randomness.
242    ///
243    /// - This is set by the Constructor.
244    /// - The IO Finalizer compresses it into `bsk`.
245    /// - This is required by the Prover.
246    /// - This may be used by Signers to verify that the value correctly matches `cv`.
247    ///
248    /// This opens `cv` for all participants. For Signers who don't need this information,
249    /// or after proofs / signatures have been applied, this can be redacted.
250    pub(crate) rcv: Option<[u8; 32]>,
251
252    /// The `ock` value used to encrypt `out_ciphertext`.
253    ///
254    /// This enables Signers to verify that `out_ciphertext` is correctly encrypted.
255    ///
256    /// This may be `None` if the Constructor added the output using an OVK policy of
257    /// "None", to make the output unrecoverable from the chain by the sender.
258    pub(crate) ock: Option<[u8; 32]>,
259
260    /// The ZIP 32 derivation path at which the spending key can be found for the output.
261    pub(crate) zip32_derivation: Option<Zip32Derivation>,
262
263    /// The user-facing address to which this output is being sent, if any.
264    ///
265    /// - This is set by an Updater.
266    /// - Signers must parse this address (if present) and confirm that it contains
267    ///   `recipient` (either directly, or e.g. as a receiver within a Unified Address).
268    #[getset(get = "pub")]
269    pub(crate) user_address: Option<String>,
270
271    /// Proprietary fields related to the note being spent.
272    #[getset(get = "pub")]
273    pub(crate) proprietary: BTreeMap<String, Vec<u8>>,
274}
275
276impl Bundle {
277    /// Merges this bundle with another.
278    ///
279    /// Returns `None` if the bundles have conflicting data.
280    pub(crate) fn merge(
281        mut self,
282        other: Self,
283        self_global: &Global,
284        other_global: &Global,
285    ) -> Option<Self> {
286        // Destructure `other` to ensure we handle everything.
287        let Self {
288            mut spends,
289            mut outputs,
290            value_sum,
291            anchor,
292            bsk,
293        } = other;
294
295        // If `bsk` is set on either bundle, the IO Finalizer has run, which means we
296        // cannot have differing numbers of spends or outputs, and the value balances must
297        // match.
298        match (self.bsk.as_mut(), bsk) {
299            (Some(lhs), Some(rhs)) if lhs != &rhs => return None,
300            (Some(_), _) | (_, Some(_))
301                if self.spends.len() != spends.len()
302                    || self.outputs.len() != outputs.len()
303                    || self.value_sum != value_sum =>
304            {
305                return None;
306            }
307            // IO Finalizer has run, and neither bundle has excess spends or outputs.
308            (Some(_), _) | (_, Some(_)) => (),
309            // IO Finalizer has not run on either bundle.
310            (None, None) => {
311                let (spends_cmp_other, outputs_cmp_other) = match (
312                    self.spends.len().cmp(&spends.len()),
313                    self.outputs.len().cmp(&outputs.len()),
314                ) {
315                    // These cases require us to recalculate the value sum, which we can't
316                    // do without a parsed bundle.
317                    (Ordering::Less, Ordering::Greater) | (Ordering::Greater, Ordering::Less) => {
318                        return None;
319                    }
320                    // These cases mean that at least one of the two value sums is correct
321                    // and we can use it directly.
322                    (spends, outputs) => (spends, outputs),
323                };
324
325                match (
326                    self_global.shielded_modifiable(),
327                    other_global.shielded_modifiable(),
328                    spends_cmp_other,
329                ) {
330                    // Fail if the merge would add spends to a non-modifiable bundle.
331                    (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None,
332                    // If the other bundle has more spends than us, move them over; these cannot
333                    // conflict by construction.
334                    (true, _, Ordering::Less) => {
335                        self.spends.extend(spends.drain(self.spends.len()..))
336                    }
337                    // Do nothing otherwise.
338                    (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (),
339                }
340
341                match (
342                    self_global.shielded_modifiable(),
343                    other_global.shielded_modifiable(),
344                    outputs_cmp_other,
345                ) {
346                    // Fail if the merge would add outputs to a non-modifiable bundle.
347                    (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None,
348                    // If the other bundle has more outputs than us, move them over; these cannot
349                    // conflict by construction.
350                    (true, _, Ordering::Less) => {
351                        self.outputs.extend(outputs.drain(self.outputs.len()..))
352                    }
353                    // Do nothing otherwise.
354                    (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (),
355                }
356
357                if matches!(spends_cmp_other, Ordering::Less)
358                    || matches!(outputs_cmp_other, Ordering::Less)
359                {
360                    // We check below that the overlapping spends and outputs match.
361                    // Assuming here that they will, we take the other bundle's value sum.
362                    self.value_sum = value_sum;
363                }
364            }
365        }
366
367        if !merge_optional(&mut self.anchor, anchor) {
368            return None;
369        }
370
371        // Leverage the early-exit behaviour of zip to confirm that the remaining data in
372        // the other bundle matches this one.
373        for (lhs, rhs) in self.spends.iter_mut().zip(spends) {
374            // Destructure `rhs` to ensure we handle everything.
375            let Spend {
376                cv,
377                nullifier,
378                rk,
379                zkproof,
380                spend_auth_sig,
381                recipient,
382                value,
383                rcm,
384                rseed,
385                rcv,
386                proof_generation_key,
387                witness,
388                alpha,
389                zip32_derivation,
390                dummy_ask,
391                proprietary,
392            } = rhs;
393
394            if lhs.cv != cv || lhs.nullifier != nullifier || lhs.rk != rk {
395                return None;
396            }
397
398            if !(merge_optional(&mut lhs.zkproof, zkproof)
399                && merge_optional(&mut lhs.spend_auth_sig, spend_auth_sig)
400                && merge_optional(&mut lhs.recipient, recipient)
401                && merge_optional(&mut lhs.value, value)
402                && merge_optional(&mut lhs.rcm, rcm)
403                && merge_optional(&mut lhs.rseed, rseed)
404                && merge_optional(&mut lhs.rcv, rcv)
405                && merge_optional(&mut lhs.proof_generation_key, proof_generation_key)
406                && merge_optional(&mut lhs.witness, witness)
407                && merge_optional(&mut lhs.alpha, alpha)
408                && merge_optional(&mut lhs.zip32_derivation, zip32_derivation)
409                && merge_optional(&mut lhs.dummy_ask, dummy_ask)
410                && merge_map(&mut lhs.proprietary, proprietary))
411            {
412                return None;
413            }
414        }
415
416        for (lhs, rhs) in self.outputs.iter_mut().zip(outputs) {
417            // Destructure `rhs` to ensure we handle everything.
418            let Output {
419                cv,
420                cmu,
421                ephemeral_key,
422                enc_ciphertext,
423                out_ciphertext,
424                zkproof,
425                recipient,
426                value,
427                rseed,
428                rcv,
429                ock,
430                zip32_derivation,
431                user_address,
432                proprietary,
433            } = rhs;
434
435            if lhs.cv != cv
436                || lhs.cmu != cmu
437                || lhs.ephemeral_key != ephemeral_key
438                || lhs.enc_ciphertext != enc_ciphertext
439                || lhs.out_ciphertext != out_ciphertext
440            {
441                return None;
442            }
443
444            if !(merge_optional(&mut lhs.zkproof, zkproof)
445                && merge_optional(&mut lhs.recipient, recipient)
446                && merge_optional(&mut lhs.value, value)
447                && merge_optional(&mut lhs.rseed, rseed)
448                && merge_optional(&mut lhs.rcv, rcv)
449                && merge_optional(&mut lhs.ock, ock)
450                && merge_optional(&mut lhs.zip32_derivation, zip32_derivation)
451                && merge_optional(&mut lhs.user_address, user_address)
452                && merge_map(&mut lhs.proprietary, proprietary))
453            {
454                return None;
455            }
456        }
457
458        Some(self)
459    }
460}
461
462/// Types for the v1 Sapling PCZT encoding.
463pub(crate) mod v1 {
464    use alloc::vec::Vec;
465
466    use serde::{Deserialize, Serialize};
467
468    /// PCZT fields that are specific to producing the transaction's Sapling
469    /// bundle.
470    #[derive(Clone, Debug, Serialize, Deserialize)]
471    pub(crate) struct Bundle {
472        spends: Vec<super::Spend>,
473        outputs: Vec<super::Output>,
474        value_sum: i128,
475        anchor: [u8; 32],
476        bsk: Option<crate::common::SecretKeyBytes>,
477    }
478
479    impl TryFrom<super::Bundle> for Bundle {
480        type Error = crate::EncodingError;
481
482        fn try_from(bundle: super::Bundle) -> Result<Self, Self::Error> {
483            let anchor = match bundle.anchor {
484                Some(anchor) => anchor,
485                None if bundle.spends.is_empty() => super::DEFAULT_ANCHOR,
486                None => return Err(crate::EncodingError::RequiresV2),
487            };
488
489            Ok(Self {
490                spends: bundle.spends,
491                outputs: bundle.outputs,
492                value_sum: bundle.value_sum,
493                anchor,
494                bsk: bundle.bsk,
495            })
496        }
497    }
498
499    impl From<Bundle> for super::Bundle {
500        fn from(bundle: Bundle) -> Self {
501            Self {
502                spends: bundle.spends,
503                outputs: bundle.outputs,
504                value_sum: bundle.value_sum,
505                anchor: Some(bundle.anchor),
506                bsk: bundle.bsk,
507            }
508        }
509    }
510}
511
512/// Types and operations for the v2 Sapling PCZT encoding.
513pub(crate) mod v2 {
514    /// Encodes a logical Sapling bundle for the v2 PCZT format, owning the
515    /// decision of whether the bundle can be omitted. A bundle that is exactly
516    /// equal to [`super::EMPTY_BUNDLE`] serializes to `None` and is dropped from
517    /// the encoding. An otherwise-empty bundle carrying
518    /// [`super::DEFAULT_ANCHOR`] is treated as empty for this purpose.
519    pub(crate) fn encode(bundle: super::Bundle) -> Option<super::Bundle> {
520        (!is_default_empty(&bundle)).then_some(bundle)
521    }
522
523    fn is_default_empty(bundle: &super::Bundle) -> bool {
524        let mut bundle = bundle.clone();
525        if bundle.anchor == Some(super::DEFAULT_ANCHOR) {
526            bundle.anchor = None;
527        }
528
529        bundle == super::EMPTY_BUNDLE
530    }
531}
532
533#[cfg(feature = "sapling")]
534impl Bundle {
535    /// Parses this bundle into the form used by the `sapling` crate.
536    ///
537    /// If the bundle's `anchor` is absent and `anchor_requirement` is
538    /// [`AnchorRequirement::Required`], returns [`ParseError::MissingAnchor`] unless the
539    /// bundle has no spends or outputs (in which case there is nothing to prove or
540    /// extract).
541    ///
542    /// [`AnchorRequirement::Required`]: crate::common::AnchorRequirement::Required
543    pub(crate) fn into_parsed(
544        self,
545        anchor_requirement: crate::common::AnchorRequirement,
546    ) -> Result<Parsed, ParseError> {
547        let wire_anchor = self.anchor;
548        let anchor = anchor_requirement
549            .resolve(
550                wire_anchor,
551                self.spends.is_empty() && self.outputs.is_empty(),
552            )
553            .ok_or(ParseError::MissingAnchor)?;
554
555        let spends = self
556            .spends
557            .into_iter()
558            .map(|spend| {
559                sapling::pczt::Spend::parse(
560                    spend.cv,
561                    spend.nullifier,
562                    spend.rk,
563                    spend.zkproof,
564                    spend.spend_auth_sig,
565                    spend.recipient,
566                    spend.value,
567                    spend.rcm,
568                    spend.rseed,
569                    spend.rcv,
570                    spend.proof_generation_key,
571                    spend.witness,
572                    spend.alpha,
573                    spend
574                        .zip32_derivation
575                        .map(|z| {
576                            sapling::pczt::Zip32Derivation::parse(
577                                z.seed_fingerprint,
578                                z.derivation_path,
579                            )
580                        })
581                        .transpose()?,
582                    spend.dummy_ask.as_ref().map(|ask| *ask.expose_secret()),
583                    spend.proprietary,
584                )
585            })
586            .collect::<Result<_, _>>()?;
587
588        let outputs = self
589            .outputs
590            .into_iter()
591            .map(|output| {
592                sapling::pczt::Output::parse(
593                    output.cv,
594                    output.cmu,
595                    output.ephemeral_key,
596                    output.enc_ciphertext,
597                    output.out_ciphertext,
598                    output.zkproof,
599                    output.recipient,
600                    output.value,
601                    output.rseed,
602                    output.rcv,
603                    output.ock,
604                    output
605                        .zip32_derivation
606                        .map(|z| {
607                            sapling::pczt::Zip32Derivation::parse(
608                                z.seed_fingerprint,
609                                z.derivation_path,
610                            )
611                        })
612                        .transpose()?,
613                    output.user_address,
614                    output.proprietary,
615                )
616            })
617            .collect::<Result<_, _>>()?;
618
619        let bundle = sapling::pczt::Bundle::parse(
620            spends,
621            outputs,
622            self.value_sum,
623            anchor,
624            self.bsk.as_ref().map(|bsk| *bsk.expose_secret()),
625        )?;
626
627        Ok(Parsed {
628            bundle,
629            wire_anchor,
630        })
631    }
632
633    pub(crate) fn serialize_from(bundle: sapling::pczt::Bundle) -> Self {
634        let spends = bundle
635            .spends()
636            .iter()
637            .map(|spend| {
638                let (rcm, rseed) = match spend.rseed() {
639                    Some(sapling::Rseed::BeforeZip212(rcm)) => (Some(rcm.to_bytes()), None),
640                    Some(sapling::Rseed::AfterZip212(rseed)) => (None, Some(*rseed)),
641                    None => (None, None),
642                };
643
644                Spend {
645                    cv: spend.cv().to_bytes(),
646                    nullifier: spend.nullifier().0,
647                    rk: (*spend.rk()).into(),
648                    zkproof: *spend.zkproof(),
649                    spend_auth_sig: spend.spend_auth_sig().map(|s| s.into()),
650                    recipient: spend.recipient().map(|recipient| recipient.to_bytes()),
651                    value: spend.value().map(|value| value.inner()),
652                    rcm,
653                    rseed,
654                    rcv: spend.rcv().as_ref().map(|rcv| rcv.inner().to_bytes()),
655                    proof_generation_key: spend
656                        .proof_generation_key()
657                        .as_ref()
658                        .map(|key| (key.ak().to_bytes(), key.nsk().to_bytes())),
659                    witness: spend.witness().as_ref().map(|witness| {
660                        (
661                            u32::try_from(u64::from(witness.position()))
662                                .expect("Sapling positions fit in u32"),
663                            witness
664                                .path_elems()
665                                .iter()
666                                .map(|node| node.to_bytes())
667                                .collect::<Vec<_>>()[..]
668                                .try_into()
669                                .expect("path is length 32"),
670                        )
671                    }),
672                    alpha: spend.alpha().map(|alpha| alpha.to_bytes()),
673                    zip32_derivation: spend.zip32_derivation().as_ref().map(|z| Zip32Derivation {
674                        seed_fingerprint: *z.seed_fingerprint(),
675                        derivation_path: z.derivation_path().iter().map(|i| i.index()).collect(),
676                    }),
677                    dummy_ask: spend
678                        .dummy_ask()
679                        .as_ref()
680                        .map(|dummy_ask| SecretKeyBytes::new(dummy_ask.to_bytes())),
681                    proprietary: spend.proprietary().clone(),
682                }
683            })
684            .collect();
685
686        let outputs = bundle
687            .outputs()
688            .iter()
689            .map(|output| Output {
690                cv: output.cv().to_bytes(),
691                cmu: output.cmu().to_bytes(),
692                ephemeral_key: output.ephemeral_key().0,
693                enc_ciphertext: output.enc_ciphertext().to_vec(),
694                out_ciphertext: output.out_ciphertext().to_vec(),
695                zkproof: *output.zkproof(),
696                recipient: output.recipient().map(|recipient| recipient.to_bytes()),
697                value: output.value().map(|value| value.inner()),
698                rseed: *output.rseed(),
699                rcv: output.rcv().as_ref().map(|rcv| rcv.inner().to_bytes()),
700                ock: output.ock().as_ref().map(|ock| ock.0),
701                zip32_derivation: output.zip32_derivation().as_ref().map(|z| Zip32Derivation {
702                    seed_fingerprint: *z.seed_fingerprint(),
703                    derivation_path: z.derivation_path().iter().map(|i| i.index()).collect(),
704                }),
705                user_address: output.user_address().clone(),
706                proprietary: output.proprietary().clone(),
707            })
708            .collect();
709
710        Self {
711            spends,
712            outputs,
713            value_sum: bundle.value_sum().to_raw(),
714            anchor: Some(bundle.anchor().to_bytes()),
715            bsk: bundle
716                .bsk()
717                .as_ref()
718                .map(|bsk| SecretKeyBytes::new(bsk.to_bytes())),
719        }
720    }
721}
722
723/// Errors that can occur while parsing a Sapling bundle into the form used by
724/// the `sapling` crate.
725#[cfg(feature = "sapling")]
726#[derive(Debug)]
727#[non_exhaustive]
728pub enum ParseError {
729    /// The operation requires the bundle's `anchor` to be set, but it was absent.
730    ///
731    /// For a v6 transaction, an Updater can resolve this by setting the anchor; see
732    /// [ZIP 374: Anchors and pre-authorization](https://zips.z.cash/zip-0374#anchorsandpre-authorization).
733    MissingAnchor,
734    /// The bundle's remaining fields were structurally invalid.
735    Bundle(sapling::pczt::ParseError),
736}
737
738#[cfg(feature = "sapling")]
739impl From<sapling::pczt::ParseError> for ParseError {
740    fn from(e: sapling::pczt::ParseError) -> Self {
741        ParseError::Bundle(e)
742    }
743}
744
745/// Errors that can occur while checking that a Sapling bundle's spend witnesses are
746/// consistent with its anchor.
747#[cfg(all(feature = "sapling", feature = "prover"))]
748#[derive(Debug)]
749#[non_exhaustive]
750pub enum AnchorConsistencyError {
751    /// A non-zero-valued spend has a `witness` but is missing other note data required
752    /// to compute its Merkle path root.
753    IncompleteSpendData,
754    /// A non-zero-valued spend's `witness` does not root to the given anchor.
755    WitnessDoesNotRootToAnchor,
756}
757
758/// Checks that every non-zero-valued spend in `bundle` whose `witness` is present has a
759/// Merkle path that roots to `anchor` (\[ZIP 374\] "Anchors and pre-authorization").
760///
761/// Zero-valued spends are skipped, as their Merkle paths are not checked by the Sapling
762/// circuit.
763///
764/// [ZIP 374]: https://zips.z.cash/zip-0374#anchorsandpre-authorization
765#[cfg(all(feature = "sapling", feature = "prover"))]
766pub(crate) fn verify_witnesses_root_to_anchor(
767    bundle: &sapling::pczt::Bundle,
768    anchor: sapling::Anchor,
769) -> Result<(), AnchorConsistencyError> {
770    for spend in bundle.spends() {
771        let Some(witness) = spend.witness() else {
772            continue;
773        };
774        let Some(value) = spend.value() else {
775            continue;
776        };
777        if value.inner() == 0 {
778            continue;
779        }
780
781        let recipient = spend
782            .recipient()
783            .ok_or(AnchorConsistencyError::IncompleteSpendData)?;
784        let rseed = (*spend.rseed()).ok_or(AnchorConsistencyError::IncompleteSpendData)?;
785
786        let note = sapling::Note::from_parts(recipient, *value, rseed);
787        let leaf = sapling::Node::from_cmu(&note.cmu());
788        let computed_anchor: sapling::Anchor = witness.root(leaf).into();
789
790        if computed_anchor != anchor {
791            return Err(AnchorConsistencyError::WitnessDoesNotRootToAnchor);
792        }
793    }
794
795    Ok(())
796}
797
798/// The result of parsing a Sapling bundle via [`Bundle::into_parsed`].
799///
800/// Carries the bundle's original wire `anchor` alongside the parsed form, so that
801/// [`Parsed::reserialize`] can restore it after an operation that does not itself
802/// change the anchor, even though parsing may have substituted a placeholder for it
803/// (see [ZIP 374: Anchors and pre-authorization](https://zips.z.cash/zip-0374#anchorsandpre-authorization)).
804#[cfg(feature = "sapling")]
805pub(crate) struct Parsed {
806    pub(crate) bundle: sapling::pczt::Bundle,
807    pub(crate) wire_anchor: Option<[u8; 32]>,
808}
809
810#[cfg(feature = "sapling")]
811impl Parsed {
812    /// Serializes the parsed bundle back into its wire representation, using
813    /// [`Self::wire_anchor`] as the result's `anchor` in place of any placeholder
814    /// substituted while parsing.
815    ///
816    /// Must not be used after an operation that legitimately changes the anchor;
817    /// such operations should set `wire_anchor` to the new value first.
818    pub(crate) fn reserialize(self) -> Bundle {
819        Bundle {
820            anchor: self.wire_anchor,
821            ..Bundle::serialize_from(self.bundle)
822        }
823    }
824}
825
826#[cfg(all(test, feature = "sapling"))]
827mod tests {
828    use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng};
829
830    use super::{Bundle, ParseError};
831    use crate::common::AnchorRequirement;
832
833    fn output_only_bundle() -> Bundle {
834        let recipient = sapling::zip32::ExtendedSpendingKey::master(&[0; 32])
835            .expect("the derivation path yields a valid key")
836            .to_diversifiable_full_viewing_key()
837            .default_address()
838            .1;
839        let mut builder = sapling::builder::Builder::new(
840            sapling::note_encryption::Zip212Enforcement::On,
841            sapling::builder::BundleType::DEFAULT,
842            sapling::Anchor::empty_tree(),
843        );
844        builder
845            .add_output(
846                None,
847                recipient,
848                sapling::value::NoteValue::from_raw(1),
849                [0; 512],
850            )
851            .unwrap();
852        let (bundle, _) = builder
853            .build_for_pczt(ChaCha20Rng::from_seed([0; 32]))
854            .unwrap();
855        let mut bundle = Bundle::serialize_from(bundle);
856        bundle.anchor = None;
857        bundle
858    }
859
860    #[test]
861    fn output_only_bundle_requires_anchor_when_required() {
862        assert!(matches!(
863            output_only_bundle().into_parsed(AnchorRequirement::Required),
864            Err(ParseError::MissingAnchor)
865        ));
866    }
867
868    #[test]
869    fn output_only_bundle_preserves_absent_anchor_when_not_required() {
870        let bundle = output_only_bundle();
871        let output_count = bundle.outputs.len();
872
873        let parsed = bundle.into_parsed(AnchorRequirement::NotRequired).unwrap();
874        assert!(parsed.bundle.spends().is_empty());
875        assert_eq!(parsed.bundle.outputs().len(), output_count);
876
877        let reserialized = parsed.reserialize();
878        assert!(reserialized.anchor.is_none());
879        assert_eq!(reserialized.outputs.len(), output_count);
880    }
881}