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