Skip to main content

sapling_crypto/
bundle.rs

1use alloc::vec::Vec;
2use core::fmt::Debug;
3
4use memuse::DynamicUsage;
5
6use redjubjub::{Binding, SpendAuth};
7
8use zcash_note_encryption::{
9    note_bytes::NoteBytesData, EphemeralKeyBytes, ShieldedOutput, OUT_CIPHERTEXT_SIZE,
10};
11
12use crate::{
13    constants::GROTH_PROOF_SIZE,
14    note::ExtractedNoteCommitment,
15    note_encryption::{
16        CompactOutputDescription, NoteCiphertextBytes, SaplingDomain, COMPACT_NOTE_SIZE,
17        ENC_CIPHERTEXT_SIZE,
18    },
19    value::ValueCommitment,
20    Nullifier,
21};
22
23pub type GrothProofBytes = [u8; GROTH_PROOF_SIZE];
24
25/// Defines the authorization type of a Sapling bundle.
26pub trait Authorization: Debug {
27    type SpendProof: Clone + Debug;
28    type OutputProof: Clone + Debug;
29    type AuthSig: Clone + Debug;
30}
31
32/// Marker type for a bundle that contains no authorizing data.
33#[derive(Debug)]
34pub struct EffectsOnly;
35
36impl Authorization for EffectsOnly {
37    type SpendProof = ();
38    type OutputProof = ();
39    type AuthSig = ();
40}
41
42/// Authorizing data for a bundle of Sapling spends and outputs, ready to be committed to
43/// the ledger.
44#[derive(Debug, Copy, Clone)]
45pub struct Authorized {
46    // TODO: Make this private.
47    pub binding_sig: redjubjub::Signature<Binding>,
48}
49
50impl Authorization for Authorized {
51    type SpendProof = GrothProofBytes;
52    type OutputProof = GrothProofBytes;
53    type AuthSig = redjubjub::Signature<SpendAuth>;
54}
55
56#[derive(Debug, Clone)]
57pub struct Bundle<A: Authorization, V> {
58    shielded_spends: Vec<SpendDescription<A>>,
59    shielded_outputs: Vec<OutputDescription<A::OutputProof>>,
60    value_balance: V,
61    authorization: A,
62}
63
64impl<A: Authorization, V> Bundle<A, V> {
65    /// Constructs a `Bundle` from its constituent parts.
66    pub fn from_parts(
67        shielded_spends: Vec<SpendDescription<A>>,
68        shielded_outputs: Vec<OutputDescription<A::OutputProof>>,
69        value_balance: V,
70        authorization: A,
71    ) -> Option<Self> {
72        if shielded_spends.is_empty() && shielded_outputs.is_empty() {
73            None
74        } else {
75            Some(Bundle {
76                shielded_spends,
77                shielded_outputs,
78                value_balance,
79                authorization,
80            })
81        }
82    }
83
84    /// Returns the list of spends in this bundle.
85    pub fn shielded_spends(&self) -> &[SpendDescription<A>] {
86        &self.shielded_spends
87    }
88
89    /// Returns the list of outputs in this bundle.
90    pub fn shielded_outputs(&self) -> &[OutputDescription<A::OutputProof>] {
91        &self.shielded_outputs
92    }
93
94    /// Returns the net value moved into or out of the Sapling shielded pool.
95    ///
96    /// This is the sum of Sapling spends minus the sum of Sapling outputs.
97    pub fn value_balance(&self) -> &V {
98        &self.value_balance
99    }
100
101    /// Returns the authorization for this bundle.
102    ///
103    /// In the case of a `Bundle<Authorized>`, this is the binding signature.
104    pub fn authorization(&self) -> &A {
105        &self.authorization
106    }
107
108    /// Transitions this bundle from one authorization state to another.
109    pub fn map_authorization<R, B: Authorization>(
110        self,
111        mut context: R,
112        spend_proof: impl Fn(&mut R, A::SpendProof) -> B::SpendProof,
113        output_proof: impl Fn(&mut R, A::OutputProof) -> B::OutputProof,
114        auth_sig: impl Fn(&mut R, A::AuthSig) -> B::AuthSig,
115        auth: impl FnOnce(&mut R, A) -> B,
116    ) -> Bundle<B, V> {
117        Bundle {
118            shielded_spends: self
119                .shielded_spends
120                .into_iter()
121                .map(|d| SpendDescription {
122                    cv: d.cv,
123                    anchor: d.anchor,
124                    nullifier: d.nullifier,
125                    rk: d.rk,
126                    zkproof: spend_proof(&mut context, d.zkproof),
127                    spend_auth_sig: auth_sig(&mut context, d.spend_auth_sig),
128                })
129                .collect(),
130            shielded_outputs: self
131                .shielded_outputs
132                .into_iter()
133                .map(|o| OutputDescription {
134                    cv: o.cv,
135                    cmu: o.cmu,
136                    ephemeral_key: o.ephemeral_key,
137                    enc_ciphertext: o.enc_ciphertext,
138                    out_ciphertext: o.out_ciphertext,
139                    zkproof: output_proof(&mut context, o.zkproof),
140                })
141                .collect(),
142            value_balance: self.value_balance,
143            authorization: auth(&mut context, self.authorization),
144        }
145    }
146
147    /// Transitions this bundle from one authorization state to another.
148    pub fn try_map_authorization<R, B: Authorization, Error>(
149        self,
150        mut context: R,
151        spend_proof: impl Fn(&mut R, A::SpendProof) -> Result<B::SpendProof, Error>,
152        output_proof: impl Fn(&mut R, A::OutputProof) -> Result<B::OutputProof, Error>,
153        auth_sig: impl Fn(&mut R, A::AuthSig) -> Result<B::AuthSig, Error>,
154        auth: impl Fn(&mut R, A) -> Result<B, Error>,
155    ) -> Result<Bundle<B, V>, Error> {
156        Ok(Bundle {
157            shielded_spends: self
158                .shielded_spends
159                .into_iter()
160                .map(|d| {
161                    Ok(SpendDescription {
162                        cv: d.cv,
163                        anchor: d.anchor,
164                        nullifier: d.nullifier,
165                        rk: d.rk,
166                        zkproof: spend_proof(&mut context, d.zkproof)?,
167                        spend_auth_sig: auth_sig(&mut context, d.spend_auth_sig)?,
168                    })
169                })
170                .collect::<Result<_, _>>()?,
171            shielded_outputs: self
172                .shielded_outputs
173                .into_iter()
174                .map(|o| {
175                    Ok(OutputDescription {
176                        cv: o.cv,
177                        cmu: o.cmu,
178                        ephemeral_key: o.ephemeral_key,
179                        enc_ciphertext: o.enc_ciphertext,
180                        out_ciphertext: o.out_ciphertext,
181                        zkproof: output_proof(&mut context, o.zkproof)?,
182                    })
183                })
184                .collect::<Result<_, _>>()?,
185            value_balance: self.value_balance,
186            authorization: auth(&mut context, self.authorization)?,
187        })
188    }
189}
190
191impl<V: DynamicUsage> DynamicUsage for Bundle<Authorized, V> {
192    fn dynamic_usage(&self) -> usize {
193        self.shielded_spends.dynamic_usage()
194            + self.shielded_outputs.dynamic_usage()
195            + self.value_balance.dynamic_usage()
196    }
197
198    fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
199        let bounds = (
200            self.shielded_spends.dynamic_usage_bounds(),
201            self.shielded_outputs.dynamic_usage_bounds(),
202            self.value_balance.dynamic_usage_bounds(),
203        );
204
205        (
206            bounds.0 .0 + bounds.1 .0 + bounds.2 .0,
207            bounds
208                .0
209                 .1
210                .zip(bounds.1 .1)
211                .zip(bounds.2 .1)
212                .map(|((a, b), c)| a + b + c),
213        )
214    }
215}
216
217#[derive(Clone)]
218pub struct SpendDescription<A: Authorization> {
219    cv: ValueCommitment,
220    anchor: bls12_381::Scalar,
221    nullifier: Nullifier,
222    rk: redjubjub::VerificationKey<SpendAuth>,
223    zkproof: A::SpendProof,
224    spend_auth_sig: A::AuthSig,
225}
226
227impl<A: Authorization> core::fmt::Debug for SpendDescription<A> {
228    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
229        write!(
230            f,
231            "SpendDescription(cv = {:?}, anchor = {:?}, nullifier = {:?}, rk = {:?}, spend_auth_sig = {:?})",
232            self.cv, self.anchor, self.nullifier, self.rk, self.spend_auth_sig
233        )
234    }
235}
236
237impl<A: Authorization> SpendDescription<A> {
238    /// Constructs a v4 `SpendDescription` from its constituent parts.
239    pub fn from_parts(
240        cv: ValueCommitment,
241        anchor: bls12_381::Scalar,
242        nullifier: Nullifier,
243        rk: redjubjub::VerificationKey<SpendAuth>,
244        zkproof: A::SpendProof,
245        spend_auth_sig: A::AuthSig,
246    ) -> Self {
247        Self {
248            cv,
249            anchor,
250            nullifier,
251            rk,
252            zkproof,
253            spend_auth_sig,
254        }
255    }
256
257    /// Returns the commitment to the value consumed by this spend.
258    pub fn cv(&self) -> &ValueCommitment {
259        &self.cv
260    }
261
262    /// Returns the root of the Sapling commitment tree that this spend commits to.
263    pub fn anchor(&self) -> &bls12_381::Scalar {
264        &self.anchor
265    }
266
267    /// Returns the nullifier of the note being spent.
268    pub fn nullifier(&self) -> &Nullifier {
269        &self.nullifier
270    }
271
272    /// Returns the randomized verification key for the note being spent.
273    pub fn rk(&self) -> &redjubjub::VerificationKey<SpendAuth> {
274        &self.rk
275    }
276
277    /// Returns the proof for this spend.
278    pub fn zkproof(&self) -> &A::SpendProof {
279        &self.zkproof
280    }
281
282    /// Returns the authorization signature for this spend.
283    pub fn spend_auth_sig(&self) -> &A::AuthSig {
284        &self.spend_auth_sig
285    }
286}
287
288impl DynamicUsage for SpendDescription<Authorized> {
289    fn dynamic_usage(&self) -> usize {
290        self.zkproof.dynamic_usage()
291    }
292
293    fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
294        self.zkproof.dynamic_usage_bounds()
295    }
296}
297
298#[derive(Clone)]
299pub struct SpendDescriptionV5 {
300    cv: ValueCommitment,
301    nullifier: Nullifier,
302    rk: redjubjub::VerificationKey<SpendAuth>,
303}
304
305impl SpendDescriptionV5 {
306    /// Constructs a v5 `SpendDescription` from its constituent parts.
307    pub fn from_parts(
308        cv: ValueCommitment,
309        nullifier: Nullifier,
310        rk: redjubjub::VerificationKey<SpendAuth>,
311    ) -> Self {
312        Self { cv, nullifier, rk }
313    }
314
315    pub fn into_spend_description<A>(
316        self,
317        anchor: bls12_381::Scalar,
318        zkproof: GrothProofBytes,
319        spend_auth_sig: redjubjub::Signature<SpendAuth>,
320    ) -> SpendDescription<A>
321    where
322        A: Authorization<SpendProof = GrothProofBytes, AuthSig = redjubjub::Signature<SpendAuth>>,
323    {
324        SpendDescription {
325            cv: self.cv,
326            anchor,
327            nullifier: self.nullifier,
328            rk: self.rk,
329            zkproof,
330            spend_auth_sig,
331        }
332    }
333}
334
335#[derive(Clone)]
336pub struct OutputDescription<Proof> {
337    cv: ValueCommitment,
338    cmu: ExtractedNoteCommitment,
339    ephemeral_key: EphemeralKeyBytes,
340    enc_ciphertext: NoteCiphertextBytes,
341    out_ciphertext: [u8; OUT_CIPHERTEXT_SIZE],
342    zkproof: Proof,
343}
344
345impl<Proof> OutputDescription<Proof> {
346    /// Returns the commitment to the value consumed by this output.
347    pub fn cv(&self) -> &ValueCommitment {
348        &self.cv
349    }
350
351    /// Returns the commitment to the new note being created.
352    pub fn cmu(&self) -> &ExtractedNoteCommitment {
353        &self.cmu
354    }
355
356    pub fn ephemeral_key(&self) -> &EphemeralKeyBytes {
357        &self.ephemeral_key
358    }
359
360    /// Returns the encrypted note ciphertext.
361    pub fn enc_ciphertext(&self) -> &[u8; ENC_CIPHERTEXT_SIZE] {
362        &self.enc_ciphertext.0
363    }
364
365    /// Returns the output recovery ciphertext.
366    pub fn out_ciphertext(&self) -> &[u8; OUT_CIPHERTEXT_SIZE] {
367        &self.out_ciphertext
368    }
369
370    /// Returns the proof for this output.
371    pub fn zkproof(&self) -> &Proof {
372        &self.zkproof
373    }
374
375    /// Constructs a v4 `OutputDescription` from its constituent parts.
376    pub fn from_parts(
377        cv: ValueCommitment,
378        cmu: ExtractedNoteCommitment,
379        ephemeral_key: EphemeralKeyBytes,
380        enc_ciphertext: [u8; ENC_CIPHERTEXT_SIZE],
381        out_ciphertext: [u8; OUT_CIPHERTEXT_SIZE],
382        zkproof: Proof,
383    ) -> Self {
384        OutputDescription {
385            cv,
386            cmu,
387            ephemeral_key,
388            enc_ciphertext: NoteBytesData(enc_ciphertext),
389            out_ciphertext,
390            zkproof,
391        }
392    }
393}
394
395#[cfg(test)]
396impl<Proof> OutputDescription<Proof> {
397    pub(crate) fn cv_mut(&mut self) -> &mut ValueCommitment {
398        &mut self.cv
399    }
400    pub(crate) fn cmu_mut(&mut self) -> &mut ExtractedNoteCommitment {
401        &mut self.cmu
402    }
403    pub(crate) fn ephemeral_key_mut(&mut self) -> &mut EphemeralKeyBytes {
404        &mut self.ephemeral_key
405    }
406    pub(crate) fn enc_ciphertext_mut(&mut self) -> &mut [u8; ENC_CIPHERTEXT_SIZE] {
407        &mut self.enc_ciphertext.0
408    }
409    pub(crate) fn out_ciphertext_mut(&mut self) -> &mut [u8; OUT_CIPHERTEXT_SIZE] {
410        &mut self.out_ciphertext
411    }
412}
413
414impl<Proof: DynamicUsage> DynamicUsage for OutputDescription<Proof> {
415    fn dynamic_usage(&self) -> usize {
416        self.zkproof.dynamic_usage()
417    }
418
419    fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
420        self.zkproof.dynamic_usage_bounds()
421    }
422}
423
424impl<A> ShieldedOutput<SaplingDomain> for OutputDescription<A> {
425    fn ephemeral_key(&self) -> EphemeralKeyBytes {
426        self.ephemeral_key.clone()
427    }
428
429    fn cmstar(&self) -> &ExtractedNoteCommitment {
430        &self.cmu
431    }
432
433    fn enc_ciphertext(&self) -> Option<&NoteCiphertextBytes> {
434        Some(&self.enc_ciphertext)
435    }
436
437    fn enc_ciphertext_compact(&self) -> NoteBytesData<COMPACT_NOTE_SIZE> {
438        NoteBytesData(
439            self.enc_ciphertext.0[..COMPACT_NOTE_SIZE]
440                .try_into()
441                .expect("slice is the correct length"),
442        )
443    }
444}
445
446impl<A> core::fmt::Debug for OutputDescription<A> {
447    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
448        write!(
449            f,
450            "OutputDescription(cv = {:?}, cmu = {:?}, ephemeral_key = {:?})",
451            self.cv, self.cmu, self.ephemeral_key
452        )
453    }
454}
455
456#[derive(Clone)]
457pub struct OutputDescriptionV5 {
458    cv: ValueCommitment,
459    cmu: ExtractedNoteCommitment,
460    ephemeral_key: EphemeralKeyBytes,
461    enc_ciphertext: [u8; ENC_CIPHERTEXT_SIZE],
462    out_ciphertext: [u8; OUT_CIPHERTEXT_SIZE],
463}
464
465memuse::impl_no_dynamic_usage!(OutputDescriptionV5);
466
467impl OutputDescriptionV5 {
468    /// Constructs a v5 `OutputDescription` from its constituent parts.
469    pub fn from_parts(
470        cv: ValueCommitment,
471        cmu: ExtractedNoteCommitment,
472        ephemeral_key: EphemeralKeyBytes,
473        enc_ciphertext: [u8; ENC_CIPHERTEXT_SIZE],
474        out_ciphertext: [u8; OUT_CIPHERTEXT_SIZE],
475    ) -> Self {
476        Self {
477            cv,
478            cmu,
479            ephemeral_key,
480            enc_ciphertext,
481            out_ciphertext,
482        }
483    }
484
485    pub fn into_output_description(
486        self,
487        zkproof: GrothProofBytes,
488    ) -> OutputDescription<GrothProofBytes> {
489        OutputDescription {
490            cv: self.cv,
491            cmu: self.cmu,
492            ephemeral_key: self.ephemeral_key,
493            enc_ciphertext: NoteBytesData(self.enc_ciphertext),
494            out_ciphertext: self.out_ciphertext,
495            zkproof,
496        }
497    }
498}
499
500impl<A> From<OutputDescription<A>> for CompactOutputDescription {
501    fn from(out: OutputDescription<A>) -> CompactOutputDescription {
502        CompactOutputDescription {
503            ephemeral_key: out.ephemeral_key,
504            cmu: out.cmu,
505            enc_ciphertext: out.enc_ciphertext.0[..COMPACT_NOTE_SIZE]
506                .try_into()
507                .unwrap(),
508        }
509    }
510}
511
512#[cfg(any(test, feature = "test-dependencies"))]
513#[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))]
514pub mod testing {
515    use core::fmt;
516
517    use ff::Field;
518    use group::{Group, GroupEncoding};
519    use proptest::collection::vec;
520    use proptest::prelude::*;
521    use rand::{rngs::StdRng, SeedableRng};
522
523    use crate::{
524        constants::GROTH_PROOF_SIZE,
525        note::testing::arb_cmu,
526        value::{
527            testing::{arb_note_value_bounded, arb_trapdoor},
528            ValueCommitment, MAX_NOTE_VALUE,
529        },
530        Nullifier,
531    };
532
533    use super::{
534        Authorized, Bundle, GrothProofBytes, NoteBytesData, OutputDescription, SpendDescription,
535        ENC_CIPHERTEXT_SIZE, OUT_CIPHERTEXT_SIZE,
536    };
537
538    prop_compose! {
539        fn arb_extended_point()(rng_seed in prop::array::uniform32(any::<u8>())) -> jubjub::ExtendedPoint {
540            let mut rng = StdRng::from_seed(rng_seed);
541            let scalar = jubjub::Scalar::random(&mut rng);
542            jubjub::ExtendedPoint::generator() * scalar
543        }
544    }
545
546    prop_compose! {
547        /// produce a spend description with invalid data (useful only for serialization
548        /// roundtrip testing).
549        fn arb_spend_description(n_spends: usize)(
550            value in arb_note_value_bounded(MAX_NOTE_VALUE.checked_div(n_spends as u64).unwrap_or(0)),
551            rcv in arb_trapdoor(),
552            anchor in vec(any::<u8>(), 64)
553                .prop_map(|v| <[u8;64]>::try_from(v.as_slice()).unwrap())
554                .prop_map(|v| bls12_381::Scalar::from_bytes_wide(&v)),
555            nullifier in prop::array::uniform32(any::<u8>())
556                .prop_map(|v| Nullifier::from_slice(&v).unwrap()),
557            zkproof in vec(any::<u8>(), GROTH_PROOF_SIZE)
558                .prop_map(|v| <[u8;GROTH_PROOF_SIZE]>::try_from(v.as_slice()).unwrap()),
559            rng_seed in prop::array::uniform32(prop::num::u8::ANY),
560            fake_sighash_bytes in prop::array::uniform32(prop::num::u8::ANY),
561        ) -> SpendDescription<Authorized> {
562            let mut rng = StdRng::from_seed(rng_seed);
563            let sk1 = redjubjub::SigningKey::new(&mut rng);
564            let rk = redjubjub::VerificationKey::from(&sk1);
565            let cv = ValueCommitment::derive(value, rcv);
566            SpendDescription {
567                cv,
568                anchor,
569                nullifier,
570                rk,
571                zkproof,
572                spend_auth_sig: sk1.sign(&mut rng, &fake_sighash_bytes),
573            }
574        }
575    }
576
577    prop_compose! {
578        /// produce an output description with invalid data (useful only for serialization
579        /// roundtrip testing).
580        pub fn arb_output_description(n_outputs: usize)(
581            value in arb_note_value_bounded(MAX_NOTE_VALUE.checked_div(n_outputs as u64).unwrap_or(0)),
582            rcv in arb_trapdoor(),
583            cmu in arb_cmu(),
584            enc_ciphertext in vec(any::<u8>(), ENC_CIPHERTEXT_SIZE)
585                .prop_map(|v| <[u8; ENC_CIPHERTEXT_SIZE]>::try_from(v.as_slice()).unwrap()),
586            epk in arb_extended_point(),
587            out_ciphertext in vec(any::<u8>(), OUT_CIPHERTEXT_SIZE)
588                .prop_map(|v| <[u8; OUT_CIPHERTEXT_SIZE]>::try_from(v.as_slice()).unwrap()),
589            zkproof in vec(any::<u8>(), GROTH_PROOF_SIZE)
590                .prop_map(|v| <[u8; GROTH_PROOF_SIZE]>::try_from(v.as_slice()).unwrap()),
591        ) -> OutputDescription<GrothProofBytes> {
592            let cv = ValueCommitment::derive(value, rcv);
593            OutputDescription {
594                cv,
595                cmu,
596                ephemeral_key: epk.to_bytes().into(),
597                enc_ciphertext: NoteBytesData(enc_ciphertext),
598                out_ciphertext,
599                zkproof,
600            }
601        }
602    }
603
604    pub fn arb_bundle<V: Copy + fmt::Debug + 'static>(
605        value_balance: V,
606    ) -> impl Strategy<Value = Option<Bundle<Authorized, V>>> {
607        (0usize..30, 0usize..30)
608            .prop_flat_map(|(n_spends, n_outputs)| {
609                (
610                    vec(arb_spend_description(n_spends), n_spends),
611                    vec(arb_output_description(n_outputs), n_outputs),
612                    prop::array::uniform32(prop::num::u8::ANY),
613                    prop::array::uniform32(prop::num::u8::ANY),
614                )
615            })
616            .prop_map(
617                move |(shielded_spends, shielded_outputs, rng_seed, fake_bvk_bytes)| {
618                    if shielded_spends.is_empty() && shielded_outputs.is_empty() {
619                        None
620                    } else {
621                        let mut rng = StdRng::from_seed(rng_seed);
622                        let bsk = redjubjub::SigningKey::new(&mut rng);
623
624                        Some(Bundle {
625                            shielded_spends,
626                            shielded_outputs,
627                            value_balance,
628                            authorization: Authorized {
629                                binding_sig: bsk.sign(&mut rng, &fake_bvk_bytes),
630                            },
631                        })
632                    }
633                },
634            )
635    }
636}