ironfish_primitives/transaction/components/
sapling.rs

1use core::fmt::Debug;
2use std::convert::TryInto;
3
4use ff::PrimeField;
5use group::GroupEncoding;
6use std::io::{self, Read, Write};
7
8use zcash_note_encryption::{
9    EphemeralKeyBytes, ShieldedOutput, COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE,
10};
11
12use crate::{
13    consensus,
14    sapling::{
15        note_encryption::{SaplingDomain, SaplingExtractedCommitmentBytes},
16        redjubjub::{self, PublicKey, Signature},
17        Nullifier,
18    },
19};
20
21use super::{amount::Amount, GROTH_PROOF_SIZE};
22
23pub type GrothProofBytes = [u8; GROTH_PROOF_SIZE];
24
25pub mod builder;
26
27pub trait Authorization: Debug {
28    type Proof: Clone + Debug;
29    type AuthSig: Clone + Debug;
30}
31
32#[derive(Debug, Copy, Clone, PartialEq)]
33pub struct Unproven;
34
35impl Authorization for Unproven {
36    type Proof = ();
37    type AuthSig = ();
38}
39
40#[derive(Debug, Copy, Clone)]
41pub struct Authorized {
42    pub binding_sig: redjubjub::Signature,
43}
44
45impl Authorization for Authorized {
46    type Proof = GrothProofBytes;
47    type AuthSig = redjubjub::Signature;
48}
49
50pub trait MapAuth<A: Authorization, B: Authorization> {
51    fn map_proof(&self, p: A::Proof) -> B::Proof;
52    fn map_auth_sig(&self, s: A::AuthSig) -> B::AuthSig;
53    fn map_authorization(&self, a: A) -> B;
54}
55
56#[derive(Debug, Clone)]
57pub struct Bundle<A: Authorization> {
58    pub shielded_spends: Vec<SpendDescription<A>>,
59    pub shielded_outputs: Vec<OutputDescription<A::Proof>>,
60    pub value_balance: Amount,
61    pub authorization: A,
62}
63
64impl<A: Authorization> Bundle<A> {
65    pub fn map_authorization<B: Authorization, F: MapAuth<A, B>>(self, f: F) -> Bundle<B> {
66        Bundle {
67            shielded_spends: self
68                .shielded_spends
69                .into_iter()
70                .map(|d| SpendDescription {
71                    cv: d.cv,
72                    anchor: d.anchor,
73                    nullifier: d.nullifier,
74                    rk: d.rk,
75                    zkproof: f.map_proof(d.zkproof),
76                    spend_auth_sig: f.map_auth_sig(d.spend_auth_sig),
77                })
78                .collect(),
79            shielded_outputs: self
80                .shielded_outputs
81                .into_iter()
82                .map(|o| OutputDescription {
83                    cv: o.cv,
84                    cmu: o.cmu,
85                    ephemeral_key: o.ephemeral_key,
86                    enc_ciphertext: o.enc_ciphertext,
87                    out_ciphertext: o.out_ciphertext,
88                    zkproof: f.map_proof(o.zkproof),
89                })
90                .collect(),
91            value_balance: self.value_balance,
92            authorization: f.map_authorization(self.authorization),
93        }
94    }
95}
96
97#[derive(Clone)]
98pub struct SpendDescription<A: Authorization> {
99    pub cv: ironfish_jubjub::ExtendedPoint,
100    pub anchor: blstrs::Scalar,
101    pub nullifier: Nullifier,
102    pub rk: PublicKey,
103    pub zkproof: A::Proof,
104    pub spend_auth_sig: A::AuthSig,
105}
106
107impl<A: Authorization> std::fmt::Debug for SpendDescription<A> {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
109        write!(
110            f,
111            "SpendDescription(cv = {:?}, anchor = {:?}, nullifier = {:?}, rk = {:?}, spend_auth_sig = {:?})",
112            self.cv, self.anchor, self.nullifier, self.rk, self.spend_auth_sig
113        )
114    }
115}
116
117/// Consensus rules (§4.4) & (§4.5):
118/// - Canonical encoding is enforced here.
119/// - "Not small order" is enforced in SaplingVerificationContext::(check_spend()/check_output())
120///   (located in zcash_proofs::sapling::verifier).
121pub fn read_point<R: Read>(mut reader: R, field: &str) -> io::Result<ironfish_jubjub::ExtendedPoint> {
122    let mut bytes = [0u8; 32];
123    reader.read_exact(&mut bytes)?;
124    let point = ironfish_jubjub::ExtendedPoint::from_bytes(&bytes);
125
126    if point.is_none().into() {
127        Err(io::Error::new(
128            io::ErrorKind::InvalidInput,
129            format!("invalid {}", field),
130        ))
131    } else {
132        Ok(point.unwrap())
133    }
134}
135
136/// Consensus rules (§7.3) & (§7.4):
137/// - Canonical encoding is enforced here
138pub fn read_base<R: Read>(mut reader: R, field: &str) -> io::Result<blstrs::Scalar> {
139    let mut f = [0u8; 32];
140    reader.read_exact(&mut f)?;
141    Option::from(blstrs::Scalar::from_repr(f)).ok_or_else(|| {
142        io::Error::new(
143            io::ErrorKind::InvalidInput,
144            format!("{} not in field", field),
145        )
146    })
147}
148
149/// Consensus rules (§4.4) & (§4.5):
150/// - Canonical encoding is enforced by the API of SaplingVerificationContext::check_spend()
151///   and SaplingVerificationContext::check_output() due to the need to parse this into a
152///   bellman::groth16::Proof.
153/// - Proof validity is enforced in SaplingVerificationContext::check_spend()
154///   and SaplingVerificationContext::check_output()
155pub fn read_zkproof<R: Read>(mut reader: R) -> io::Result<GrothProofBytes> {
156    let mut zkproof = [0u8; GROTH_PROOF_SIZE];
157    reader.read_exact(&mut zkproof)?;
158    Ok(zkproof)
159}
160
161impl SpendDescription<Authorized> {
162    pub fn read_nullifier<R: Read>(mut reader: R) -> io::Result<Nullifier> {
163        let mut nullifier = Nullifier([0u8; 32]);
164        reader.read_exact(&mut nullifier.0)?;
165        Ok(nullifier)
166    }
167
168    /// Consensus rules (§4.4):
169    /// - Canonical encoding is enforced here.
170    /// - "Not small order" is enforced in SaplingVerificationContext::check_spend()
171    pub fn read_rk<R: Read>(mut reader: R) -> io::Result<PublicKey> {
172        PublicKey::read(&mut reader)
173    }
174
175    /// Consensus rules (§4.4):
176    /// - Canonical encoding is enforced here.
177    /// - Signature validity is enforced in SaplingVerificationContext::check_spend()
178    pub fn read_spend_auth_sig<R: Read>(mut reader: R) -> io::Result<Signature> {
179        Signature::read(&mut reader)
180    }
181
182    pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
183        // Consensus rules (§4.4) & (§4.5):
184        // - Canonical encoding is enforced here.
185        // - "Not small order" is enforced in SaplingVerificationContext::(check_spend()/check_output())
186        //   (located in zcash_proofs::sapling::verifier).
187        let cv = read_point(&mut reader, "cv")?;
188        // Consensus rules (§7.3) & (§7.4):
189        // - Canonical encoding is enforced here
190        let anchor = read_base(&mut reader, "anchor")?;
191        let nullifier = Self::read_nullifier(&mut reader)?;
192        let rk = Self::read_rk(&mut reader)?;
193        let zkproof = read_zkproof(&mut reader)?;
194        let spend_auth_sig = Self::read_spend_auth_sig(&mut reader)?;
195
196        Ok(SpendDescription {
197            cv,
198            anchor,
199            nullifier,
200            rk,
201            zkproof,
202            spend_auth_sig,
203        })
204    }
205
206    pub fn write_v4<W: Write>(&self, mut writer: W) -> io::Result<()> {
207        writer.write_all(&self.cv.to_bytes())?;
208        writer.write_all(self.anchor.to_repr().as_ref())?;
209        writer.write_all(&self.nullifier.0)?;
210        self.rk.write(&mut writer)?;
211        writer.write_all(&self.zkproof)?;
212        self.spend_auth_sig.write(&mut writer)
213    }
214
215    pub fn write_v5_without_witness_data<W: Write>(&self, mut writer: W) -> io::Result<()> {
216        writer.write_all(&self.cv.to_bytes())?;
217        writer.write_all(&self.nullifier.0)?;
218        self.rk.write(&mut writer)
219    }
220}
221
222#[derive(Clone)]
223pub struct SpendDescriptionV5 {
224    pub cv: ironfish_jubjub::ExtendedPoint,
225    pub nullifier: Nullifier,
226    pub rk: PublicKey,
227}
228
229impl SpendDescriptionV5 {
230    pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> {
231        let cv = read_point(&mut reader, "cv")?;
232        let nullifier = SpendDescription::read_nullifier(&mut reader)?;
233        let rk = SpendDescription::read_rk(&mut reader)?;
234
235        Ok(SpendDescriptionV5 { cv, nullifier, rk })
236    }
237
238    pub fn into_spend_description(
239        self,
240        anchor: blstrs::Scalar,
241        zkproof: GrothProofBytes,
242        spend_auth_sig: Signature,
243    ) -> SpendDescription<Authorized> {
244        SpendDescription {
245            cv: self.cv,
246            anchor,
247            nullifier: self.nullifier,
248            rk: self.rk,
249            zkproof,
250            spend_auth_sig,
251        }
252    }
253}
254
255#[derive(Clone)]
256pub struct OutputDescription<Proof> {
257    pub cv: ironfish_jubjub::ExtendedPoint,
258    pub cmu: blstrs::Scalar,
259    pub ephemeral_key: EphemeralKeyBytes,
260    pub enc_ciphertext: [u8; 580],
261    pub out_ciphertext: [u8; 80],
262    pub zkproof: Proof,
263}
264
265impl<P: consensus::Parameters, A> ShieldedOutput<SaplingDomain<P>, ENC_CIPHERTEXT_SIZE>
266    for OutputDescription<A>
267{
268    fn ephemeral_key(&self) -> EphemeralKeyBytes {
269        self.ephemeral_key.clone()
270    }
271
272    fn cmstar_bytes(&self) -> SaplingExtractedCommitmentBytes {
273        SaplingExtractedCommitmentBytes(self.cmu.to_repr())
274    }
275
276    fn enc_ciphertext(&self) -> &[u8; ENC_CIPHERTEXT_SIZE] {
277        &self.enc_ciphertext
278    }
279}
280
281impl<A> std::fmt::Debug for OutputDescription<A> {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
283        write!(
284            f,
285            "OutputDescription(cv = {:?}, cmu = {:?}, ephemeral_key = {:?})",
286            self.cv, self.cmu, self.ephemeral_key
287        )
288    }
289}
290
291impl OutputDescription<GrothProofBytes> {
292    pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> {
293        // Consensus rules (§4.5):
294        // - Canonical encoding is enforced here.
295        // - "Not small order" is enforced in SaplingVerificationContext::check_output()
296        //   (located in zcash_proofs::sapling::verifier).
297        let cv = read_point(&mut reader, "cv")?;
298
299        // Consensus rule (§7.4): Canonical encoding is enforced here
300        let cmu = read_base(&mut reader, "cmu")?;
301
302        // Consensus rules (§4.5):
303        // - Canonical encoding is enforced in librustzcash_sapling_check_output by zcashd
304        // - "Not small order" is enforced in SaplingVerificationContext::check_output()
305        let mut ephemeral_key = EphemeralKeyBytes([0u8; 32]);
306        reader.read_exact(&mut ephemeral_key.0)?;
307
308        let mut enc_ciphertext = [0u8; 580];
309        let mut out_ciphertext = [0u8; 80];
310        reader.read_exact(&mut enc_ciphertext)?;
311        reader.read_exact(&mut out_ciphertext)?;
312
313        let zkproof = read_zkproof(&mut reader)?;
314
315        Ok(OutputDescription {
316            cv,
317            cmu,
318            ephemeral_key,
319            enc_ciphertext,
320            out_ciphertext,
321            zkproof,
322        })
323    }
324
325    pub fn write_v4<W: Write>(&self, mut writer: W) -> io::Result<()> {
326        writer.write_all(&self.cv.to_bytes())?;
327        writer.write_all(self.cmu.to_repr().as_ref())?;
328        writer.write_all(self.ephemeral_key.as_ref())?;
329        writer.write_all(&self.enc_ciphertext)?;
330        writer.write_all(&self.out_ciphertext)?;
331        writer.write_all(&self.zkproof)
332    }
333
334    pub fn write_v5_without_proof<W: Write>(&self, mut writer: W) -> io::Result<()> {
335        writer.write_all(&self.cv.to_bytes())?;
336        writer.write_all(self.cmu.to_repr().as_ref())?;
337        writer.write_all(self.ephemeral_key.as_ref())?;
338        writer.write_all(&self.enc_ciphertext)?;
339        writer.write_all(&self.out_ciphertext)
340    }
341}
342
343#[derive(Clone)]
344pub struct OutputDescriptionV5 {
345    pub cv: ironfish_jubjub::ExtendedPoint,
346    pub cmu: blstrs::Scalar,
347    pub ephemeral_key: EphemeralKeyBytes,
348    pub enc_ciphertext: [u8; 580],
349    pub out_ciphertext: [u8; 80],
350}
351
352impl OutputDescriptionV5 {
353    pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> {
354        let cv = read_point(&mut reader, "cv")?;
355        let cmu = read_base(&mut reader, "cmu")?;
356
357        // Consensus rules (§4.5):
358        // - Canonical encoding is enforced in librustzcash_sapling_check_output by zcashd
359        // - "Not small order" is enforced in SaplingVerificationContext::check_output()
360        let mut ephemeral_key = EphemeralKeyBytes([0u8; 32]);
361        reader.read_exact(&mut ephemeral_key.0)?;
362
363        let mut enc_ciphertext = [0u8; 580];
364        let mut out_ciphertext = [0u8; 80];
365        reader.read_exact(&mut enc_ciphertext)?;
366        reader.read_exact(&mut out_ciphertext)?;
367
368        Ok(OutputDescriptionV5 {
369            cv,
370            cmu,
371            ephemeral_key,
372            enc_ciphertext,
373            out_ciphertext,
374        })
375    }
376
377    pub fn into_output_description(
378        self,
379        zkproof: GrothProofBytes,
380    ) -> OutputDescription<GrothProofBytes> {
381        OutputDescription {
382            cv: self.cv,
383            cmu: self.cmu,
384            ephemeral_key: self.ephemeral_key,
385            enc_ciphertext: self.enc_ciphertext,
386            out_ciphertext: self.out_ciphertext,
387            zkproof,
388        }
389    }
390}
391
392pub struct CompactOutputDescription {
393    pub ephemeral_key: EphemeralKeyBytes,
394    pub cmu: blstrs::Scalar,
395    pub enc_ciphertext: [u8; COMPACT_NOTE_SIZE],
396}
397
398impl<A> From<OutputDescription<A>> for CompactOutputDescription {
399    fn from(out: OutputDescription<A>) -> CompactOutputDescription {
400        CompactOutputDescription {
401            ephemeral_key: out.ephemeral_key,
402            cmu: out.cmu,
403            enc_ciphertext: out.enc_ciphertext[..COMPACT_NOTE_SIZE].try_into().unwrap(),
404        }
405    }
406}
407
408impl<P: consensus::Parameters> ShieldedOutput<SaplingDomain<P>, COMPACT_NOTE_SIZE>
409    for CompactOutputDescription
410{
411    fn ephemeral_key(&self) -> EphemeralKeyBytes {
412        self.ephemeral_key.clone()
413    }
414
415    fn cmstar_bytes(&self) -> SaplingExtractedCommitmentBytes {
416        SaplingExtractedCommitmentBytes(self.cmu.to_repr())
417    }
418
419    fn enc_ciphertext(&self) -> &[u8; COMPACT_NOTE_SIZE] {
420        &self.enc_ciphertext
421    }
422}
423
424#[cfg(any(test, feature = "test-dependencies"))]
425pub mod testing {
426    use ff::Field;
427    use group::{Group, GroupEncoding};
428    use proptest::collection::vec;
429    use proptest::prelude::*;
430    use rand::{rngs::StdRng, SeedableRng};
431    use std::convert::TryFrom;
432
433    use crate::{
434        constants::{SPENDING_KEY_GENERATOR, VALUE_COMMITMENT_RANDOMNESS_GENERATOR},
435        sapling::{
436            redjubjub::{PrivateKey, PublicKey},
437            Nullifier,
438        },
439        transaction::{
440            components::{amount::testing::arb_amount, GROTH_PROOF_SIZE},
441            TxVersion,
442        },
443    };
444
445    use super::{Authorized, Bundle, GrothProofBytes, OutputDescription, SpendDescription};
446
447    prop_compose! {
448        fn arb_extended_point()(rng_seed in prop::array::uniform32(any::<u8>())) -> ironfish_jubjub::ExtendedPoint {
449            let mut rng = StdRng::from_seed(rng_seed);
450            let scalar = ironfish_jubjub::Scalar::random(&mut rng);
451            ironfish_jubjub::ExtendedPoint::generator() * scalar
452        }
453    }
454
455    prop_compose! {
456        /// produce a spend description with invalid data (useful only for serialization
457        /// roundtrip testing).
458        fn arb_spend_description()(
459            cv in arb_extended_point(),
460            anchor in vec(any::<u8>(), 32)
461                .prop_map(|v| <[u8;32]>::try_from(v.as_slice()).unwrap())
462                .prop_map(|mut v| { v[0] = 0; v })
463                .prop_map(|v| blstrs::Scalar::from_bytes_be(&v))
464                .prop_map(|v| Option::from(v).unwrap()),
465            nullifier in prop::array::uniform32(any::<u8>())
466                .prop_map(|v| Nullifier::from_slice(&v).unwrap()),
467            zkproof in vec(any::<u8>(), GROTH_PROOF_SIZE)
468                .prop_map(|v| <[u8;GROTH_PROOF_SIZE]>::try_from(v.as_slice()).unwrap()),
469            rng_seed in prop::array::uniform32(prop::num::u8::ANY),
470            fake_sighash_bytes in prop::array::uniform32(prop::num::u8::ANY),
471        ) -> SpendDescription<Authorized> {
472            let mut rng = StdRng::from_seed(rng_seed);
473            let sk1 = PrivateKey(ironfish_jubjub::Fr::random(&mut rng));
474            let rk = PublicKey::from_private(&sk1, *SPENDING_KEY_GENERATOR);
475            SpendDescription {
476                cv,
477                anchor,
478                nullifier,
479                rk,
480                zkproof,
481                spend_auth_sig: sk1.sign(&fake_sighash_bytes, &mut rng, *SPENDING_KEY_GENERATOR),
482            }
483        }
484    }
485
486    prop_compose! {
487        /// produce an output description with invalid data (useful only for serialization
488        /// roundtrip testing).
489        pub fn arb_output_description()(
490            cv in arb_extended_point(),
491            cmu in vec(any::<u8>(), 32)
492                .prop_map(|v| <[u8;32]>::try_from(v.as_slice()).unwrap())
493                .prop_map(|mut v| { v[0] = 0; v })
494                .prop_map(|v| blstrs::Scalar::from_bytes_be(&v))
495                .prop_map(|v| Option::from(v).unwrap()),
496            enc_ciphertext in vec(any::<u8>(), 580)
497                .prop_map(|v| <[u8;580]>::try_from(v.as_slice()).unwrap()),
498            epk in arb_extended_point(),
499            out_ciphertext in vec(any::<u8>(), 80)
500                .prop_map(|v| <[u8;80]>::try_from(v.as_slice()).unwrap()),
501            zkproof in vec(any::<u8>(), GROTH_PROOF_SIZE)
502                .prop_map(|v| <[u8;GROTH_PROOF_SIZE]>::try_from(v.as_slice()).unwrap()),
503        ) -> OutputDescription<GrothProofBytes> {
504            OutputDescription {
505                cv,
506                cmu,
507                ephemeral_key: epk.to_bytes().into(),
508                enc_ciphertext,
509                out_ciphertext,
510                zkproof,
511            }
512        }
513    }
514
515    prop_compose! {
516        pub fn arb_bundle()(
517            shielded_spends in vec(arb_spend_description(), 0..30),
518            shielded_outputs in vec(arb_output_description(), 0..30),
519            value_balance in arb_amount(),
520            rng_seed in prop::array::uniform32(prop::num::u8::ANY),
521            fake_bvk_bytes in prop::array::uniform32(prop::num::u8::ANY),
522        ) -> Option<Bundle<Authorized>> {
523            if shielded_spends.is_empty() && shielded_outputs.is_empty() {
524                None
525            } else {
526                let mut rng = StdRng::from_seed(rng_seed);
527                let bsk = PrivateKey(ironfish_jubjub::Fr::random(&mut rng));
528
529                Some(
530                    Bundle {
531                        shielded_spends,
532                        shielded_outputs,
533                        value_balance,
534                        authorization: Authorized { binding_sig: bsk.sign(&fake_bvk_bytes, &mut rng, *VALUE_COMMITMENT_RANDOMNESS_GENERATOR) },
535                    }
536                )
537            }
538        }
539    }
540
541    pub fn arb_bundle_for_version(
542        v: TxVersion,
543    ) -> impl Strategy<Value = Option<Bundle<Authorized>>> {
544        if v.has_sapling() {
545            Strategy::boxed(arb_bundle())
546        } else {
547            Strategy::boxed(Just(None))
548        }
549    }
550}