1use std::error;
4use std::fmt;
5use std::sync::mpsc::Sender;
6
7#[cfg(not(feature = "zfuture"))]
8use std::marker::PhantomData;
9
10use rand::{rngs::OsRng, CryptoRng, RngCore};
11
12use crate::{
13 consensus::{self, BlockHeight, BranchId},
14 keys::OutgoingViewingKey,
15 legacy::TransparentAddress,
16 memo::MemoBytes,
17 merkle_tree::MerklePath,
18 sapling::{prover::TxProver, Diversifier, Node, Note, PaymentAddress},
19 transaction::{
20 components::{
21 amount::{Amount, DEFAULT_FEE},
22 sapling::{
23 self,
24 builder::{SaplingBuilder, SaplingMetadata},
25 },
26 transparent::{self, builder::TransparentBuilder},
27 },
28 sighash::{signature_hash, SignableInput},
29 txid::TxIdDigester,
30 Transaction, TransactionData, TxVersion, Unauthorized,
31 },
32 zip32::ExtendedSpendingKey,
33};
34
35#[cfg(feature = "transparent-inputs")]
36use crate::transaction::components::transparent::TxOut;
37
38#[cfg(feature = "zfuture")]
39use crate::{
40 extensions::transparent::{ExtensionTxBuilder, ToPayload},
41 transaction::components::{
42 tze::builder::TzeBuilder,
43 tze::{self, TzeOut},
44 },
45};
46
47#[cfg(any(test, feature = "test-dependencies"))]
48use crate::sapling::prover::mock::MockTxProver;
49
50const DEFAULT_TX_EXPIRY_DELTA: u32 = 20;
51
52#[derive(Debug, PartialEq)]
53pub enum Error {
54 ChangeIsNegative(Amount),
55 InvalidAmount,
56 NoChangeAddress,
57 TransparentBuild(transparent::builder::Error),
58 SaplingBuild(sapling::builder::Error),
59 #[cfg(feature = "zfuture")]
60 TzeBuild(tze::builder::Error),
61}
62
63impl fmt::Display for Error {
64 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65 match self {
66 Error::ChangeIsNegative(amount) => {
67 write!(f, "Change is negative ({:?} zatoshis)", amount)
68 }
69 Error::InvalidAmount => write!(f, "Invalid amount"),
70 Error::NoChangeAddress => write!(f, "No change address specified or discoverable"),
71 Error::TransparentBuild(err) => err.fmt(f),
72 Error::SaplingBuild(err) => err.fmt(f),
73 #[cfg(feature = "zfuture")]
74 Error::TzeBuild(err) => err.fmt(f),
75 }
76 }
77}
78
79impl error::Error for Error {}
80
81pub struct Progress {
83 cur: u32,
85 end: Option<u32>,
87}
88
89impl Progress {
90 pub fn new(cur: u32, end: Option<u32>) -> Self {
91 Self { cur, end }
92 }
93
94 pub fn cur(&self) -> u32 {
98 self.cur
99 }
100
101 pub fn end(&self) -> Option<u32> {
106 self.end
107 }
108}
109
110enum ChangeAddress {
111 SaplingChangeAddress(OutgoingViewingKey, PaymentAddress),
112}
113
114pub struct Builder<'a, P, R> {
116 params: P,
117 rng: R,
118 target_height: BlockHeight,
119 expiry_height: BlockHeight,
120 fee: Amount,
121 transparent_builder: TransparentBuilder,
122 sapling_builder: SaplingBuilder<P>,
123 change_address: Option<ChangeAddress>,
124 #[cfg(feature = "zfuture")]
125 tze_builder: TzeBuilder<'a, TransactionData<Unauthorized>>,
126 #[cfg(not(feature = "zfuture"))]
127 tze_builder: PhantomData<&'a ()>,
128 progress_notifier: Option<Sender<Progress>>,
129}
130
131impl<'a, P: consensus::Parameters> Builder<'a, P, OsRng> {
132 pub fn new(params: P, target_height: BlockHeight) -> Self {
142 Builder::new_with_rng(params, target_height, OsRng)
143 }
144}
145
146impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> Builder<'a, P, R> {
147 pub fn new_with_rng(params: P, target_height: BlockHeight, rng: R) -> Builder<'a, P, R> {
157 Self::new_internal(params, target_height, rng)
158 }
159}
160
161impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
162 fn new_internal(params: P, target_height: BlockHeight, rng: R) -> Builder<'a, P, R> {
167 Builder {
168 params: params.clone(),
169 rng,
170 target_height,
171 expiry_height: target_height + DEFAULT_TX_EXPIRY_DELTA,
172 fee: DEFAULT_FEE,
173 transparent_builder: TransparentBuilder::empty(),
174 sapling_builder: SaplingBuilder::new(params, target_height),
175 change_address: None,
176 #[cfg(feature = "zfuture")]
177 tze_builder: TzeBuilder::empty(),
178 #[cfg(not(feature = "zfuture"))]
179 tze_builder: PhantomData,
180 progress_notifier: None,
181 }
182 }
183
184 pub fn add_sapling_spend(
189 &mut self,
190 extsk: ExtendedSpendingKey,
191 diversifier: Diversifier,
192 note: Note,
193 merkle_path: MerklePath<Node>,
194 ) -> Result<(), Error> {
195 self.sapling_builder
196 .add_spend(&mut self.rng, extsk, diversifier, note, merkle_path)
197 .map_err(Error::SaplingBuild)
198 }
199
200 pub fn add_sapling_output(
202 &mut self,
203 ovk: Option<OutgoingViewingKey>,
204 to: PaymentAddress,
205 value: Amount,
206 memo: MemoBytes,
207 ) -> Result<(), Error> {
208 self.sapling_builder
209 .add_output(&mut self.rng, ovk, to, value, memo)
210 .map_err(Error::SaplingBuild)
211 }
212
213 #[cfg(feature = "transparent-inputs")]
215 #[cfg_attr(docsrs, doc(cfg(feature = "transparent-inputs")))]
216 pub fn add_transparent_input(
217 &mut self,
218 sk: secp256k1::SecretKey,
219 utxo: transparent::OutPoint,
220 coin: TxOut,
221 ) -> Result<(), Error> {
222 self.transparent_builder
223 .add_input(sk, utxo, coin)
224 .map_err(Error::TransparentBuild)
225 }
226
227 pub fn add_transparent_output(
229 &mut self,
230 to: &TransparentAddress,
231 value: Amount,
232 ) -> Result<(), Error> {
233 self.transparent_builder
234 .add_output(to, value)
235 .map_err(Error::TransparentBuild)
236 }
237
238 pub fn send_change_to(&mut self, ovk: OutgoingViewingKey, to: PaymentAddress) {
243 self.change_address = Some(ChangeAddress::SaplingChangeAddress(ovk, to))
244 }
245
246 pub fn with_progress_notifier(&mut self, progress_notifier: Sender<Progress>) {
253 self.progress_notifier = Some(progress_notifier);
254 }
255
256 fn value_balance(&self) -> Result<Amount, Error> {
258 let value_balances = [
259 self.transparent_builder
260 .value_balance()
261 .ok_or(Error::InvalidAmount)?,
262 self.sapling_builder.value_balance(),
263 #[cfg(feature = "zfuture")]
264 self.tze_builder
265 .value_balance()
266 .ok_or(Error::InvalidAmount)?,
267 ];
268
269 IntoIterator::into_iter(&value_balances)
270 .sum::<Option<_>>()
271 .ok_or(Error::InvalidAmount)
272 }
273
274 pub fn build(
279 mut self,
280 prover: &impl TxProver,
281 ) -> Result<(Transaction, SaplingMetadata), Error> {
282 let consensus_branch_id = BranchId::for_height(&self.params, self.target_height);
283
284 let version = TxVersion::suggested_for_branch(consensus_branch_id);
286
287 let change = (self.value_balance()? - self.fee).ok_or(Error::InvalidAmount)?;
293
294 if change.is_negative() {
295 return Err(Error::ChangeIsNegative(change));
296 }
297
298 if change.is_positive() {
303 match self.change_address.take() {
306 Some(ChangeAddress::SaplingChangeAddress(ovk, addr)) => {
307 self.add_sapling_output(Some(ovk), addr, change, MemoBytes::empty())?;
308 }
309 None => {
310 let (ovk, addr) = self
311 .sapling_builder
312 .get_candidate_change_address()
313 .ok_or(Error::NoChangeAddress)?;
314 self.add_sapling_output(Some(ovk), addr, change, MemoBytes::empty())?;
315 }
316 }
317 }
318
319 let transparent_bundle = self.transparent_builder.build();
320
321 let mut rng = self.rng;
322 let mut ctx = prover.new_sapling_proving_context();
323 let sapling_bundle = self
324 .sapling_builder
325 .build(
326 prover,
327 &mut ctx,
328 &mut rng,
329 self.target_height,
330 self.progress_notifier.as_ref(),
331 )
332 .map_err(Error::SaplingBuild)?;
333
334 #[cfg(feature = "zfuture")]
335 let (tze_bundle, tze_signers) = self.tze_builder.build();
336
337 let unauthed_tx: TransactionData<Unauthorized> = TransactionData {
338 version,
339 consensus_branch_id: BranchId::for_height(&self.params, self.target_height),
340 lock_time: 0,
341 expiry_height: self.expiry_height,
342 transparent_bundle,
343 sprout_bundle: None,
344 sapling_bundle,
345 orchard_bundle: None,
346 #[cfg(feature = "zfuture")]
347 tze_bundle,
348 };
349
350 let txid_parts = unauthed_tx.digest(TxIdDigester);
354
355 let transparent_bundle = unauthed_tx.transparent_bundle.clone().map(|b| {
356 b.apply_signatures(
357 #[cfg(feature = "transparent-inputs")]
358 &unauthed_tx,
359 #[cfg(feature = "transparent-inputs")]
360 &txid_parts,
361 )
362 });
363
364 #[cfg(feature = "zfuture")]
365 let tze_bundle = unauthed_tx
366 .tze_bundle
367 .clone()
368 .map(|b| b.into_authorized(&unauthed_tx, tze_signers))
369 .transpose()
370 .map_err(Error::TzeBuild)?;
371
372 let shielded_sig_commitment =
376 signature_hash(&unauthed_tx, &SignableInput::Shielded, &txid_parts);
377
378 let (sapling_bundle, tx_metadata) = match unauthed_tx
379 .sapling_bundle
380 .map(|b| {
381 b.apply_signatures(prover, &mut ctx, &mut rng, shielded_sig_commitment.as_ref())
382 })
383 .transpose()
384 .map_err(Error::SaplingBuild)?
385 {
386 Some((bundle, meta)) => (Some(bundle), meta),
387 None => (None, SaplingMetadata::empty()),
388 };
389
390 let authorized_tx = TransactionData {
391 version: unauthed_tx.version,
392 consensus_branch_id: unauthed_tx.consensus_branch_id,
393 lock_time: unauthed_tx.lock_time,
394 expiry_height: unauthed_tx.expiry_height,
395 transparent_bundle,
396 sprout_bundle: unauthed_tx.sprout_bundle,
397 sapling_bundle,
398 orchard_bundle: None,
399 #[cfg(feature = "zfuture")]
400 tze_bundle,
401 };
402
403 Ok((authorized_tx.freeze().unwrap(), tx_metadata))
406 }
407}
408
409#[cfg(feature = "zfuture")]
410impl<'a, P: consensus::Parameters, R: RngCore + CryptoRng> ExtensionTxBuilder<'a>
411 for Builder<'a, P, R>
412{
413 type BuildCtx = TransactionData<Unauthorized>;
414 type BuildError = tze::builder::Error;
415
416 fn add_tze_input<WBuilder, W: ToPayload>(
417 &mut self,
418 extension_id: u32,
419 mode: u32,
420 prevout: (tze::OutPoint, TzeOut),
421 witness_builder: WBuilder,
422 ) -> Result<(), Self::BuildError>
423 where
424 WBuilder: 'a + (FnOnce(&Self::BuildCtx) -> Result<W, tze::builder::Error>),
425 {
426 self.tze_builder
427 .add_input(extension_id, mode, prevout, witness_builder);
428
429 Ok(())
430 }
431
432 fn add_tze_output<G: ToPayload>(
433 &mut self,
434 extension_id: u32,
435 value: Amount,
436 guarded_by: &G,
437 ) -> Result<(), Self::BuildError> {
438 self.tze_builder.add_output(extension_id, value, guarded_by)
439 }
440}
441
442#[cfg(any(test, feature = "test-dependencies"))]
443impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
444 pub fn test_only_new_with_rng(params: P, height: BlockHeight, rng: R) -> Builder<'a, P, R> {
456 Self::new_internal(params, height, rng)
457 }
458
459 pub fn mock_build(self) -> Result<(Transaction, SaplingMetadata), Error> {
460 self.build(&MockTxProver)
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use ff::{Field, PrimeField};
467 use rand_core::OsRng;
468
469 use crate::{
470 consensus::{NetworkUpgrade, Parameters, TEST_NETWORK},
471 legacy::TransparentAddress,
472 memo::MemoBytes,
473 merkle_tree::{CommitmentTree, IncrementalWitness},
474 sapling::{prover::mock::MockTxProver, Node, Rseed},
475 transaction::components::{
476 amount::{Amount, DEFAULT_FEE},
477 sapling::builder::{self as build_s},
478 transparent::builder::{self as build_t},
479 },
480 zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
481 };
482
483 use super::{Builder, Error, SaplingBuilder, DEFAULT_TX_EXPIRY_DELTA};
484
485 #[cfg(feature = "zfuture")]
486 use super::TzeBuilder;
487
488 #[cfg(not(feature = "zfuture"))]
489 use std::marker::PhantomData;
490
491 #[test]
492 fn fails_on_negative_output() {
493 let extsk = ExtendedSpendingKey::master(&[]);
494 let extfvk = ExtendedFullViewingKey::from(&extsk);
495 let ovk = extfvk.fvk.ovk;
496 let to = extfvk.default_address().1;
497
498 let sapling_activation_height = TEST_NETWORK
499 .activation_height(NetworkUpgrade::Sapling)
500 .unwrap();
501
502 let mut builder = Builder::new(TEST_NETWORK, sapling_activation_height);
503 assert_eq!(
504 builder.add_sapling_output(
505 Some(ovk),
506 to,
507 Amount::from_i64(-1).unwrap(),
508 MemoBytes::empty()
509 ),
510 Err(Error::SaplingBuild(build_s::Error::InvalidAmount))
511 );
512 }
513
514 #[test]
515 fn binding_sig_absent_if_no_shielded_spend_or_output() {
516 use crate::consensus::NetworkUpgrade;
517 use crate::transaction::builder::{self, TransparentBuilder};
518
519 let sapling_activation_height = TEST_NETWORK
520 .activation_height(NetworkUpgrade::Sapling)
521 .unwrap();
522
523 let mut builder = builder::Builder {
525 params: TEST_NETWORK,
526 rng: OsRng,
527 target_height: sapling_activation_height,
528 expiry_height: sapling_activation_height + DEFAULT_TX_EXPIRY_DELTA,
529 fee: Amount::zero(),
530 transparent_builder: TransparentBuilder::empty(),
531 sapling_builder: SaplingBuilder::new(TEST_NETWORK, sapling_activation_height),
532 change_address: None,
533 #[cfg(feature = "zfuture")]
534 tze_builder: TzeBuilder::empty(),
535 #[cfg(not(feature = "zfuture"))]
536 tze_builder: PhantomData,
537 progress_notifier: None,
538 };
539
540 builder
542 .add_transparent_output(&TransparentAddress::PublicKey([0; 20]), Amount::zero())
543 .unwrap();
544
545 let (tx, _) = builder.build(&MockTxProver).unwrap();
546 assert!(tx.sapling_bundle.is_none());
548 }
549
550 #[test]
551 fn binding_sig_present_if_shielded_spend() {
552 let extsk = ExtendedSpendingKey::master(&[]);
553 let extfvk = ExtendedFullViewingKey::from(&extsk);
554 let to = extfvk.default_address().1;
555
556 let mut rng = OsRng;
557
558 let note1 = to
559 .create_note(50000, Rseed::BeforeZip212(ironfish_jubjub::Fr::random(&mut rng)))
560 .unwrap();
561 let cmu1 = Node::new(note1.cmu().to_repr());
562 let mut tree = CommitmentTree::empty();
563 tree.append(cmu1).unwrap();
564 let witness1 = IncrementalWitness::from_tree(&tree);
565
566 let tx_height = TEST_NETWORK
567 .activation_height(NetworkUpgrade::Sapling)
568 .unwrap();
569 let mut builder = Builder::new(TEST_NETWORK, tx_height);
570
571 builder
573 .add_sapling_spend(extsk, *to.diversifier(), note1, witness1.path().unwrap())
574 .unwrap();
575
576 builder
577 .add_transparent_output(&TransparentAddress::PublicKey([0; 20]), Amount::zero())
578 .unwrap();
579
580 assert_eq!(
583 builder.build(&MockTxProver),
584 Err(Error::SaplingBuild(build_s::Error::BindingSig))
585 );
586 }
587
588 #[test]
589 fn fails_on_negative_transparent_output() {
590 let tx_height = TEST_NETWORK
591 .activation_height(NetworkUpgrade::Sapling)
592 .unwrap();
593 let mut builder = Builder::new(TEST_NETWORK, tx_height);
594 assert_eq!(
595 builder.add_transparent_output(
596 &TransparentAddress::PublicKey([0; 20]),
597 Amount::from_i64(-1).unwrap(),
598 ),
599 Err(Error::TransparentBuild(build_t::Error::InvalidAmount))
600 );
601 }
602
603 #[test]
604 fn fails_on_negative_change() {
605 let mut rng = OsRng;
606
607 let extsk = ExtendedSpendingKey::master(&[]);
609 let tx_height = TEST_NETWORK
610 .activation_height(NetworkUpgrade::Sapling)
611 .unwrap();
612
613 {
616 let builder = Builder::new(TEST_NETWORK, tx_height);
617 assert_eq!(
618 builder.build(&MockTxProver),
619 Err(Error::ChangeIsNegative(
620 (Amount::zero() - DEFAULT_FEE).unwrap()
621 ))
622 );
623 }
624
625 let extfvk = ExtendedFullViewingKey::from(&extsk);
626 let ovk = Some(extfvk.fvk.ovk);
627 let to = extfvk.default_address().1;
628
629 {
632 let mut builder = Builder::new(TEST_NETWORK, tx_height);
633 builder
634 .add_sapling_output(
635 ovk,
636 to.clone(),
637 Amount::from_u64(50000).unwrap(),
638 MemoBytes::empty(),
639 )
640 .unwrap();
641 assert_eq!(
642 builder.build(&MockTxProver),
643 Err(Error::ChangeIsNegative(
644 (Amount::from_i64(-50000).unwrap() - DEFAULT_FEE).unwrap()
645 ))
646 );
647 }
648
649 {
652 let mut builder = Builder::new(TEST_NETWORK, tx_height);
653 builder
654 .add_transparent_output(
655 &TransparentAddress::PublicKey([0; 20]),
656 Amount::from_u64(50000).unwrap(),
657 )
658 .unwrap();
659 assert_eq!(
660 builder.build(&MockTxProver),
661 Err(Error::ChangeIsNegative(
662 (Amount::from_i64(-50000).unwrap() - DEFAULT_FEE).unwrap()
663 ))
664 );
665 }
666
667 let note1 = to
668 .create_note(50999, Rseed::BeforeZip212(ironfish_jubjub::Fr::random(&mut rng)))
669 .unwrap();
670 let cmu1 = Node::new(note1.cmu().to_repr());
671 let mut tree = CommitmentTree::empty();
672 tree.append(cmu1).unwrap();
673 let mut witness1 = IncrementalWitness::from_tree(&tree);
674
675 {
678 let mut builder = Builder::new(TEST_NETWORK, tx_height);
679 builder
680 .add_sapling_spend(
681 extsk.clone(),
682 *to.diversifier(),
683 note1.clone(),
684 witness1.path().unwrap(),
685 )
686 .unwrap();
687 builder
688 .add_sapling_output(
689 ovk,
690 to.clone(),
691 Amount::from_u64(30000).unwrap(),
692 MemoBytes::empty(),
693 )
694 .unwrap();
695 builder
696 .add_transparent_output(
697 &TransparentAddress::PublicKey([0; 20]),
698 Amount::from_u64(20000).unwrap(),
699 )
700 .unwrap();
701 assert_eq!(
702 builder.build(&MockTxProver),
703 Err(Error::ChangeIsNegative(Amount::from_i64(-1).unwrap()))
704 );
705 }
706
707 let note2 = to
708 .create_note(1, Rseed::BeforeZip212(ironfish_jubjub::Fr::random(&mut rng)))
709 .unwrap();
710 let cmu2 = Node::new(note2.cmu().to_repr());
711 tree.append(cmu2).unwrap();
712 witness1.append(cmu2).unwrap();
713 let witness2 = IncrementalWitness::from_tree(&tree);
714
715 {
721 let mut builder = Builder::new(TEST_NETWORK, tx_height);
722 builder
723 .add_sapling_spend(
724 extsk.clone(),
725 *to.diversifier(),
726 note1,
727 witness1.path().unwrap(),
728 )
729 .unwrap();
730 builder
731 .add_sapling_spend(extsk, *to.diversifier(), note2, witness2.path().unwrap())
732 .unwrap();
733 builder
734 .add_sapling_output(
735 ovk,
736 to,
737 Amount::from_u64(30000).unwrap(),
738 MemoBytes::empty(),
739 )
740 .unwrap();
741 builder
742 .add_transparent_output(
743 &TransparentAddress::PublicKey([0; 20]),
744 Amount::from_u64(20000).unwrap(),
745 )
746 .unwrap();
747 assert_eq!(
748 builder.build(&MockTxProver),
749 Err(Error::SaplingBuild(build_s::Error::BindingSig))
750 )
751 }
752 }
753}