1use 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
36const 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 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#[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 pub fn spend_index(&self, n: usize) -> Option<usize> {
176 self.spend_indices.get(n).copied()
177 }
178
179 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 pub fn value_balance(&self) -> Amount {
230 self.value_balance
231 }
232
233 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 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 #[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 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 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 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 if !indexed_spends.is_empty() {
333 while indexed_outputs.len() < MIN_SHIELDED_OUTPUTS {
334 indexed_outputs.push(None);
335 }
336 }
337
338 indexed_spends.shuffle(&mut rng);
340 indexed_outputs.shuffle(&mut rng);
341
342 let total_progress = indexed_spends.len() as u32 + indexed_outputs.len() as u32;
344 let mut progress = 0u32;
345
346 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 tx_metadata.spend_indices[pos] = i;
378
379 progress += 1;
381 if let Some(sender) = progress_notifier {
382 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 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 tx_metadata.output_indices[pos] = i;
410
411 output.clone().build::<P, _, _>(prover, ctx, &mut rng)
412 } else {
413 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(¶ms, 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 progress += 1;
476 if let Some(sender) = progress_notifier {
477 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}