ironfish_primitives/transaction/
builder.rs

1//! Structs for building transactions.
2
3use 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
81/// Reports on the progress made by the builder towards building a transaction.
82pub struct Progress {
83    /// The number of steps completed.
84    cur: u32,
85    /// The expected total number of steps (as of this progress update), if known.
86    end: Option<u32>,
87}
88
89impl Progress {
90    pub fn new(cur: u32, end: Option<u32>) -> Self {
91        Self { cur, end }
92    }
93
94    /// Returns the number of steps completed so far while building the transaction.
95    ///
96    /// Note that each step may not be of the same complexity/duration.
97    pub fn cur(&self) -> u32 {
98        self.cur
99    }
100
101    /// Returns the total expected number of steps before this transaction will be ready,
102    /// or `None` if the end is unknown as of this progress update.
103    ///
104    /// Note that each step may not be of the same complexity/duration.
105    pub fn end(&self) -> Option<u32> {
106        self.end
107    }
108}
109
110enum ChangeAddress {
111    SaplingChangeAddress(OutgoingViewingKey, PaymentAddress),
112}
113
114/// Generates a [`Transaction`] from its inputs and outputs.
115pub 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    /// Creates a new `Builder` targeted for inclusion in the block with the given height,
133    /// using default values for general transaction fields and the default OS random.
134    ///
135    /// # Default values
136    ///
137    /// The expiry height will be set to the given height plus the default transaction
138    /// expiry delta (20 blocks).
139    ///
140    /// The fee will be set to the default fee (0.0001 ZEC).
141    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    /// Creates a new `Builder` targeted for inclusion in the block with the given height
148    /// and randomness source, using default values for general transaction fields.
149    ///
150    /// # Default values
151    ///
152    /// The expiry height will be set to the given height plus the default transaction
153    /// expiry delta (20 blocks).
154    ///
155    /// The fee will be set to the default fee (0.0001 ZEC).
156    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    /// Common utility function for builder construction.
163    ///
164    /// WARNING: THIS MUST REMAIN PRIVATE AS IT ALLOWS CONSTRUCTION
165    /// OF BUILDERS WITH NON-CryptoRng RNGs
166    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    /// Adds a Sapling note to be spent in this transaction.
185    ///
186    /// Returns an error if the given Merkle path does not have the same anchor as the
187    /// paths for previous Sapling notes.
188    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    /// Adds a Sapling address to send funds to.
201    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    /// Adds a transparent coin to be spent in this transaction.
214    #[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    /// Adds a transparent address to send funds to.
228    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    /// Sets the Sapling address to which any change will be sent.
239    ///
240    /// By default, change is sent to the Sapling address corresponding to the first note
241    /// being spent (i.e. the first call to [`Builder::add_sapling_spend`]).
242    pub fn send_change_to(&mut self, ovk: OutgoingViewingKey, to: PaymentAddress) {
243        self.change_address = Some(ChangeAddress::SaplingChangeAddress(ovk, to))
244    }
245
246    /// Sets the notifier channel, where progress of building the transaction is sent.
247    ///
248    /// An update is sent after every Spend or Output is computed, and the `u32` sent
249    /// represents the total steps completed so far. It will eventually send number of
250    /// spends + outputs. If there's an error building the transaction, the channel is
251    /// closed.
252    pub fn with_progress_notifier(&mut self, progress_notifier: Sender<Progress>) {
253        self.progress_notifier = Some(progress_notifier);
254    }
255
256    /// Returns the sum of the transparent, Sapling, and TZE value balances.
257    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    /// Builds a transaction from the configured spends and outputs.
275    ///
276    /// Upon success, returns a tuple containing the final transaction, and the
277    /// [`SaplingMetadata`] generated during the build process.
278    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        // determine transaction version
285        let version = TxVersion::suggested_for_branch(consensus_branch_id);
286
287        //
288        // Consistency checks
289        //
290
291        // Valid change
292        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        //
299        // Change output
300        //
301
302        if change.is_positive() {
303            // Send change to the specified change address. If no change address
304            // was set, send change to the first Sapling address given as input.
305            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        //
351        // Signatures -- everything but the signatures must already have been added.
352        //
353        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        // the commitment being signed is shared across all Sapling inputs; once
373        // V4 transactions are deprecated this should just be the txid, but
374        // for now we need to continue to compute it here.
375        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        // The unwrap() here is safe because the txid hashing
404        // of freeze() should be infalliable.
405        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    /// Creates a new `Builder` targeted for inclusion in the block with the given height
445    /// and randomness source, using default values for general transaction fields.
446    ///
447    /// # Default values
448    ///
449    /// The expiry height will be set to the given height plus the default transaction
450    /// expiry delta (20 blocks).
451    ///
452    /// The fee will be set to the default fee (0.0001 ZEC).
453    ///
454    /// WARNING: DO NOT USE IN PRODUCTION
455    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        // Create a builder with 0 fee, so we can construct t outputs
524        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        // Create a tx with only t output. No binding_sig should be present
541        builder
542            .add_transparent_output(&TransparentAddress::PublicKey([0; 20]), Amount::zero())
543            .unwrap();
544
545        let (tx, _) = builder.build(&MockTxProver).unwrap();
546        // No binding signature, because only t input and outputs
547        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        // Create a tx with a sapling spend. binding_sig should be present
572        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        // Expect a binding signature error, because our inputs aren't valid, but this shows
581        // that a binding signature was attempted
582        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        // Just use the master key as the ExtendedSpendingKey for this test
608        let extsk = ExtendedSpendingKey::master(&[]);
609        let tx_height = TEST_NETWORK
610            .activation_height(NetworkUpgrade::Sapling)
611            .unwrap();
612
613        // Fails with no inputs or outputs
614        // 0.0001 t-ZEC fee
615        {
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        // Fail if there is only a Sapling output
630        // 0.0005 z-ZEC out, 0.00001 t-ZEC fee
631        {
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        // Fail if there is only a transparent output
650        // 0.0005 t-ZEC out, 0.00001 t-ZEC fee
651        {
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        // Fail if there is insufficient input
676        // 0.0003 z-ZEC out, 0.0002 t-ZEC out, 0.00001 t-ZEC fee, 0.00050999 z-ZEC in
677        {
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        // Succeeds if there is sufficient input
716        // 0.0003 z-ZEC out, 0.0002 t-ZEC out, 0.0001 t-ZEC fee, 0.0006 z-ZEC in
717        //
718        // (Still fails because we are using a MockTxProver which doesn't correctly
719        // compute bindingSig.)
720        {
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}