ironfish_primitives/transaction/components/sapling/
builder.rs

1//! Types and functions for building Sapling transaction components.
2
3use core::fmt;
4use std::sync::mpsc::Sender;
5
6use ff::Field;
7use group::GroupEncoding;
8use rand::{seq::SliceRandom, RngCore};
9
10use crate::{
11    consensus::{self, BlockHeight},
12    keys::OutgoingViewingKey,
13    memo::MemoBytes,
14    merkle_tree::MerklePath,
15    sapling::{
16        note_encryption::sapling_note_encryption,
17        prover::TxProver,
18        redjubjub::{PrivateKey, Signature},
19        spend_sig_internal,
20        util::generate_random_rseed_internal,
21        Diversifier, Node, Note, PaymentAddress,
22    },
23    transaction::{
24        builder::Progress,
25        components::{
26            amount::Amount,
27            sapling::{
28                Authorization, Authorized, Bundle, GrothProofBytes, OutputDescription,
29                SpendDescription,
30            },
31        },
32    },
33    zip32::ExtendedSpendingKey,
34};
35
36/// If there are any shielded inputs, always have at least two shielded outputs, padding
37/// with dummy outputs if necessary. See <https://github.com/zcash/zcash/issues/3615>.
38const MIN_SHIELDED_OUTPUTS: usize = 2;
39
40#[derive(Debug, PartialEq)]
41pub enum Error {
42    AnchorMismatch,
43    BindingSig,
44    InvalidAddress,
45    InvalidAmount,
46    SpendProof,
47}
48
49impl fmt::Display for Error {
50    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51        match self {
52            Error::AnchorMismatch => {
53                write!(f, "Anchor mismatch (anchors for all spends must be equal)")
54            }
55            Error::BindingSig => write!(f, "Failed to create bindingSig"),
56            Error::InvalidAddress => write!(f, "Invalid address"),
57            Error::InvalidAmount => write!(f, "Invalid amount"),
58            Error::SpendProof => write!(f, "Failed to create Sapling spend proof"),
59        }
60    }
61}
62
63#[derive(Debug, Clone)]
64pub struct SpendDescriptionInfo {
65    extsk: ExtendedSpendingKey,
66    diversifier: Diversifier,
67    note: Note,
68    alpha: ironfish_jubjub::Fr,
69    merkle_path: MerklePath<Node>,
70}
71
72#[derive(Clone)]
73struct SaplingOutput {
74    /// `None` represents the `ovk = ⊥` case.
75    ovk: Option<OutgoingViewingKey>,
76    to: PaymentAddress,
77    note: Note,
78    memo: MemoBytes,
79}
80
81impl SaplingOutput {
82    fn new_internal<P: consensus::Parameters, R: RngCore>(
83        params: &P,
84        rng: &mut R,
85        target_height: BlockHeight,
86        ovk: Option<OutgoingViewingKey>,
87        to: PaymentAddress,
88        value: Amount,
89        memo: MemoBytes,
90    ) -> Result<Self, Error> {
91        let g_d = to.g_d().ok_or(Error::InvalidAddress)?;
92        if value.is_negative() {
93            return Err(Error::InvalidAmount);
94        }
95
96        let rseed = generate_random_rseed_internal(params, target_height, rng);
97
98        let note = Note {
99            g_d,
100            pk_d: *to.pk_d(),
101            value: value.into(),
102            rseed,
103        };
104
105        Ok(SaplingOutput {
106            ovk,
107            to,
108            note,
109            memo,
110        })
111    }
112
113    fn build<P: consensus::Parameters, Pr: TxProver, R: RngCore>(
114        self,
115        prover: &Pr,
116        ctx: &mut Pr::SaplingProvingContext,
117        rng: &mut R,
118    ) -> OutputDescription<GrothProofBytes> {
119        let encryptor = sapling_note_encryption::<R, P>(
120            self.ovk,
121            self.note.clone(),
122            self.to.clone(),
123            self.memo,
124            rng,
125        );
126
127        let (zkproof, cv) = prover.output_proof(
128            ctx,
129            *encryptor.esk(),
130            self.to,
131            self.note.rcm(),
132            self.note.value,
133        );
134
135        let cmu = self.note.cmu();
136
137        let enc_ciphertext = encryptor.encrypt_note_plaintext();
138        let out_ciphertext = encryptor.encrypt_outgoing_plaintext(&cv, &cmu, rng);
139
140        let epk = *encryptor.epk();
141
142        OutputDescription {
143            cv,
144            cmu,
145            ephemeral_key: epk.to_bytes().into(),
146            enc_ciphertext,
147            out_ciphertext,
148            zkproof,
149        }
150    }
151}
152
153/// Metadata about a transaction created by a [`SaplingBuilder`].
154#[derive(Debug, Clone, PartialEq)]
155pub struct SaplingMetadata {
156    spend_indices: Vec<usize>,
157    output_indices: Vec<usize>,
158}
159
160impl SaplingMetadata {
161    pub fn empty() -> Self {
162        SaplingMetadata {
163            spend_indices: vec![],
164            output_indices: vec![],
165        }
166    }
167
168    /// Returns the index within the transaction of the [`SpendDescription`] corresponding
169    /// to the `n`-th call to [`SaplingBuilder::add_spend`].
170    ///
171    /// Note positions are randomized when building transactions for indistinguishability.
172    /// This means that the transaction consumer cannot assume that e.g. the first spend
173    /// they added (via the first call to [`SaplingBuilder::add_spend`]) is the first
174    /// [`SpendDescription`] in the transaction.
175    pub fn spend_index(&self, n: usize) -> Option<usize> {
176        self.spend_indices.get(n).copied()
177    }
178
179    /// Returns the index within the transaction of the [`OutputDescription`] corresponding
180    /// to the `n`-th call to [`SaplingBuilder::add_output`].
181    ///
182    /// Note positions are randomized when building transactions for indistinguishability.
183    /// This means that the transaction consumer cannot assume that e.g. the first output
184    /// they added (via the first call to [`SaplingBuilder::add_output`]) is the first
185    /// [`OutputDescription`] in the transaction.
186    pub fn output_index(&self, n: usize) -> Option<usize> {
187        self.output_indices.get(n).copied()
188    }
189}
190
191pub struct SaplingBuilder<P> {
192    params: P,
193    anchor: Option<blstrs::Scalar>,
194    target_height: BlockHeight,
195    value_balance: Amount,
196    spends: Vec<SpendDescriptionInfo>,
197    outputs: Vec<SaplingOutput>,
198}
199
200#[derive(Clone)]
201pub struct Unauthorized {
202    tx_metadata: SaplingMetadata,
203}
204
205impl std::fmt::Debug for Unauthorized {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
207        write!(f, "Unauthorized")
208    }
209}
210
211impl Authorization for Unauthorized {
212    type Proof = GrothProofBytes;
213    type AuthSig = SpendDescriptionInfo;
214}
215
216impl<P: consensus::Parameters> SaplingBuilder<P> {
217    pub fn new(params: P, target_height: BlockHeight) -> Self {
218        SaplingBuilder {
219            params,
220            anchor: None,
221            target_height,
222            value_balance: Amount::zero(),
223            spends: vec![],
224            outputs: vec![],
225        }
226    }
227
228    /// Returns the net value represented by the spends and outputs added to this builder.
229    pub fn value_balance(&self) -> Amount {
230        self.value_balance
231    }
232
233    /// Adds a Sapling note to be spent in this transaction.
234    ///
235    /// Returns an error if the given Merkle path does not have the same anchor as the
236    /// paths for previous Sapling notes.
237    pub fn add_spend<R: RngCore>(
238        &mut self,
239        mut rng: R,
240        extsk: ExtendedSpendingKey,
241        diversifier: Diversifier,
242        note: Note,
243        merkle_path: MerklePath<Node>,
244    ) -> Result<(), Error> {
245        // Consistency check: all anchors must equal the first one
246        let cmu = Node::new(note.cmu().to_bytes_le());
247        if let Some(anchor) = self.anchor {
248            let path_root: blstrs::Scalar = merkle_path.root(cmu).into();
249            if path_root != anchor {
250                return Err(Error::AnchorMismatch);
251            }
252        } else {
253            self.anchor = Some(merkle_path.root(cmu).into())
254        }
255
256        let alpha = ironfish_jubjub::Fr::random(&mut rng);
257
258        self.value_balance += Amount::from_u64(note.value).map_err(|_| Error::InvalidAmount)?;
259
260        self.spends.push(SpendDescriptionInfo {
261            extsk,
262            diversifier,
263            note,
264            alpha,
265            merkle_path,
266        });
267
268        Ok(())
269    }
270
271    /// Adds a Sapling address to send funds to.
272    #[allow(clippy::too_many_arguments)]
273    pub fn add_output<R: RngCore>(
274        &mut self,
275        mut rng: R,
276        ovk: Option<OutgoingViewingKey>,
277        to: PaymentAddress,
278        value: Amount,
279        memo: MemoBytes,
280    ) -> Result<(), Error> {
281        let output = SaplingOutput::new_internal(
282            &self.params,
283            &mut rng,
284            self.target_height,
285            ovk,
286            to,
287            value,
288            memo,
289        )?;
290
291        self.value_balance -= value;
292
293        self.outputs.push(output);
294
295        Ok(())
296    }
297
298    /// Send change to the specified change address. If no change address
299    /// was set, send change to the first Sapling address given as input.
300    pub fn get_candidate_change_address(&self) -> Option<(OutgoingViewingKey, PaymentAddress)> {
301        self.spends.first().and_then(|spend| {
302            PaymentAddress::from_parts(spend.diversifier, spend.note.pk_d)
303                .map(|addr| (spend.extsk.expsk.ovk, addr))
304        })
305    }
306
307    pub fn build<Pr: TxProver, R: RngCore>(
308        self,
309        prover: &Pr,
310        ctx: &mut Pr::SaplingProvingContext,
311        mut rng: R,
312        target_height: BlockHeight,
313        progress_notifier: Option<&Sender<Progress>>,
314    ) -> Result<Option<Bundle<Unauthorized>>, Error> {
315        // Record initial positions of spends and outputs
316        let params = self.params;
317        let mut indexed_spends: Vec<_> = self.spends.into_iter().enumerate().collect();
318        let mut indexed_outputs: Vec<_> = self
319            .outputs
320            .iter()
321            .enumerate()
322            .map(|(i, o)| Some((i, o)))
323            .collect();
324
325        // Set up the transaction metadata that will be used to record how
326        // inputs and outputs are shuffled.
327        let mut tx_metadata = SaplingMetadata::empty();
328        tx_metadata.spend_indices.resize(indexed_spends.len(), 0);
329        tx_metadata.output_indices.resize(indexed_outputs.len(), 0);
330
331        // Pad Sapling outputs
332        if !indexed_spends.is_empty() {
333            while indexed_outputs.len() < MIN_SHIELDED_OUTPUTS {
334                indexed_outputs.push(None);
335            }
336        }
337
338        // Randomize order of inputs and outputs
339        indexed_spends.shuffle(&mut rng);
340        indexed_outputs.shuffle(&mut rng);
341
342        // Keep track of the total number of steps computed
343        let total_progress = indexed_spends.len() as u32 + indexed_outputs.len() as u32;
344        let mut progress = 0u32;
345
346        // Create Sapling SpendDescriptions
347        let shielded_spends: Vec<SpendDescription<Unauthorized>> = if !indexed_spends.is_empty() {
348            let anchor = self
349                .anchor
350                .expect("Sapling anchor must be set if Sapling spends are present.");
351
352            indexed_spends
353                .into_iter()
354                .enumerate()
355                .map(|(i, (pos, spend))| {
356                    let proof_generation_key = spend.extsk.expsk.proof_generation_key();
357
358                    let nullifier = spend.note.nf(
359                        &proof_generation_key.to_viewing_key(),
360                        spend.merkle_path.position,
361                    );
362
363                    let (zkproof, cv, rk) = prover
364                        .spend_proof(
365                            ctx,
366                            proof_generation_key,
367                            spend.diversifier,
368                            spend.note.rseed,
369                            spend.alpha,
370                            spend.note.value,
371                            anchor,
372                            spend.merkle_path.clone(),
373                        )
374                        .map_err(|_| Error::SpendProof)?;
375
376                    // Record the post-randomized spend location
377                    tx_metadata.spend_indices[pos] = i;
378
379                    // Update progress and send a notification on the channel
380                    progress += 1;
381                    if let Some(sender) = progress_notifier {
382                        // If the send fails, we should ignore the error, not crash.
383                        sender
384                            .send(Progress::new(progress, Some(total_progress)))
385                            .unwrap_or(());
386                    }
387
388                    Ok(SpendDescription {
389                        cv,
390                        anchor,
391                        nullifier,
392                        rk,
393                        zkproof,
394                        spend_auth_sig: spend,
395                    })
396                })
397                .collect::<Result<Vec<_>, Error>>()?
398        } else {
399            vec![]
400        };
401
402        // Create Sapling OutputDescriptions
403        let shielded_outputs: Vec<OutputDescription<GrothProofBytes>> = indexed_outputs
404            .into_iter()
405            .enumerate()
406            .map(|(i, output)| {
407                let result = if let Some((pos, output)) = output {
408                    // Record the post-randomized output location
409                    tx_metadata.output_indices[pos] = i;
410
411                    output.clone().build::<P, _, _>(prover, ctx, &mut rng)
412                } else {
413                    // This is a dummy output
414                    let (dummy_to, dummy_note) = {
415                        let (diversifier, g_d) = {
416                            let mut diversifier;
417                            let g_d;
418                            loop {
419                                let mut d = [0; 11];
420                                rng.fill_bytes(&mut d);
421                                diversifier = Diversifier(d);
422                                if let Some(val) = diversifier.g_d() {
423                                    g_d = val;
424                                    break;
425                                }
426                            }
427                            (diversifier, g_d)
428                        };
429                        let (pk_d, payment_address) = loop {
430                            let dummy_ivk = ironfish_jubjub::Fr::random(&mut rng);
431                            let pk_d = g_d * dummy_ivk;
432                            if let Some(addr) = PaymentAddress::from_parts(diversifier, pk_d) {
433                                break (pk_d, addr);
434                            }
435                        };
436
437                        let rseed =
438                            generate_random_rseed_internal(&params, target_height, &mut rng);
439
440                        (
441                            payment_address,
442                            Note {
443                                g_d,
444                                pk_d,
445                                rseed,
446                                value: 0,
447                            },
448                        )
449                    };
450
451                    let esk = dummy_note.generate_or_derive_esk_internal(&mut rng);
452                    let epk = dummy_note.g_d * esk;
453
454                    let (zkproof, cv) =
455                        prover.output_proof(ctx, esk, dummy_to, dummy_note.rcm(), dummy_note.value);
456
457                    let cmu = dummy_note.cmu();
458
459                    let mut enc_ciphertext = [0u8; 580];
460                    let mut out_ciphertext = [0u8; 80];
461                    rng.fill_bytes(&mut enc_ciphertext[..]);
462                    rng.fill_bytes(&mut out_ciphertext[..]);
463
464                    OutputDescription {
465                        cv,
466                        cmu,
467                        ephemeral_key: epk.to_bytes().into(),
468                        enc_ciphertext,
469                        out_ciphertext,
470                        zkproof,
471                    }
472                };
473
474                // Update progress and send a notification on the channel
475                progress += 1;
476                if let Some(sender) = progress_notifier {
477                    // If the send fails, we should ignore the error, not crash.
478                    sender
479                        .send(Progress::new(progress, Some(total_progress)))
480                        .unwrap_or(());
481                }
482
483                result
484            })
485            .collect();
486
487        let bundle = if shielded_spends.is_empty() && shielded_outputs.is_empty() {
488            None
489        } else {
490            Some(Bundle {
491                shielded_spends,
492                shielded_outputs,
493                value_balance: self.value_balance,
494                authorization: Unauthorized { tx_metadata },
495            })
496        };
497
498        Ok(bundle)
499    }
500}
501
502impl SpendDescription<Unauthorized> {
503    pub fn apply_signature(&self, spend_auth_sig: Signature) -> SpendDescription<Authorized> {
504        SpendDescription {
505            cv: self.cv,
506            anchor: self.anchor,
507            nullifier: self.nullifier,
508            rk: self.rk.clone(),
509            zkproof: self.zkproof,
510            spend_auth_sig,
511        }
512    }
513}
514
515impl Bundle<Unauthorized> {
516    pub fn apply_signatures<Pr: TxProver, R: RngCore>(
517        self,
518        prover: &Pr,
519        ctx: &mut Pr::SaplingProvingContext,
520        rng: &mut R,
521        sighash_bytes: &[u8; 32],
522    ) -> Result<(Bundle<Authorized>, SaplingMetadata), Error> {
523        let binding_sig = prover
524            .binding_sig(ctx, self.value_balance, sighash_bytes)
525            .map_err(|_| Error::BindingSig)?;
526
527        Ok((
528            Bundle {
529                shielded_spends: self
530                    .shielded_spends
531                    .iter()
532                    .map(|spend| {
533                        spend.apply_signature(spend_sig_internal(
534                            PrivateKey(spend.spend_auth_sig.extsk.expsk.ask),
535                            spend.spend_auth_sig.alpha,
536                            sighash_bytes,
537                            rng,
538                        ))
539                    })
540                    .collect(),
541                shielded_outputs: self.shielded_outputs,
542                value_balance: self.value_balance,
543                authorization: Authorized { binding_sig },
544            },
545            self.authorization.tx_metadata,
546        ))
547    }
548}
549
550#[cfg(any(test, feature = "test-dependencies"))]
551pub mod testing {
552    use proptest::collection::vec;
553    use proptest::prelude::*;
554    use rand::{rngs::StdRng, SeedableRng};
555
556    use crate::{
557        consensus::{
558            testing::{arb_branch_id, arb_height},
559            TEST_NETWORK,
560        },
561        merkle_tree::{testing::arb_commitment_tree, IncrementalWitness},
562        sapling::{
563            prover::{mock::MockTxProver, TxProver},
564            testing::{arb_node, arb_note, arb_positive_note_value},
565            Diversifier,
566        },
567        transaction::components::{
568            amount::MAX_MONEY,
569            sapling::{Authorized, Bundle},
570        },
571        zip32::testing::arb_extended_spending_key,
572    };
573
574    use super::SaplingBuilder;
575
576    prop_compose! {
577        fn arb_bundle()(n_notes in 1..30usize)(
578            extsk in arb_extended_spending_key(),
579            spendable_notes in vec(
580                arb_positive_note_value(MAX_MONEY as u64 / 10000).prop_flat_map(arb_note),
581                n_notes
582            ),
583            commitment_trees in vec(
584                arb_commitment_tree(n_notes, arb_node(), 32).prop_map(
585                    |t| IncrementalWitness::from_tree(&t).path().unwrap()
586                ),
587                n_notes
588            ),
589            diversifiers in vec(prop::array::uniform11(any::<u8>()).prop_map(Diversifier), n_notes),
590            target_height in arb_branch_id().prop_flat_map(|b| arb_height(b, &TEST_NETWORK)),
591            rng_seed in prop::array::uniform32(any::<u8>()),
592            fake_sighash_bytes in prop::array::uniform32(any::<u8>()),
593        ) -> Bundle<Authorized> {
594            let mut builder = SaplingBuilder::new(TEST_NETWORK, target_height.unwrap());
595            let mut rng = StdRng::from_seed(rng_seed);
596
597            for ((note, path), diversifier) in spendable_notes.into_iter().zip(commitment_trees.into_iter()).zip(diversifiers.into_iter()) {
598                builder.add_spend(
599                    &mut rng,
600                    extsk.clone(),
601                    diversifier,
602                    note,
603                    path
604                ).unwrap();
605            }
606
607            let prover = MockTxProver;
608            let mut ctx = prover.new_sapling_proving_context();
609
610            let bundle = builder.build(
611                &prover,
612                &mut ctx,
613                &mut rng,
614                target_height.unwrap(),
615                None
616            ).unwrap().unwrap();
617
618            let (bundle, _) = bundle.apply_signatures(
619                &prover,
620                &mut ctx,
621                &mut rng,
622                &fake_sighash_bytes,
623            ).unwrap();
624
625            bundle
626        }
627    }
628}