Skip to main content

sapling_crypto/
bundle.rs

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