1use std::{
2 cmp::Eq,
3 collections::HashSet,
4 convert::Infallible,
5 hash::Hash,
6 num::{NonZeroU8, NonZeroU32, NonZeroU64, NonZeroUsize},
7};
8
9use assert_matches::assert_matches;
10use incrementalmerkletree::{Hashable, Level, Position, frontier::Frontier};
11use rand::{Rng, RngExt};
12use secrecy::Secret;
13use shardtree::error::ShardTreeError;
14
15use transparent::address::TransparentAddress;
16use zcash_keys::{
17 address::{Address, UnifiedAddress},
18 keys::{UnifiedAddressRequest, UnifiedSpendingKey},
19};
20use zcash_primitives::{
21 block::BlockHash,
22 transaction::{
23 Transaction,
24 builder::{
25 BLOSSOM_DEFAULT_TX_EXPIRY_DELTA, NU7_DEFAULT_TX_EXPIRY_DELTA, default_tx_expiry_delta,
26 },
27 fees::zip317::{FeeRule as Zip317FeeRule, MARGINAL_FEE, MINIMUM_FEE},
28 },
29};
30use zcash_protocol::{
31 ShieldedPool,
32 consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters},
33 local_consensus::LocalNetwork,
34 memo::{Memo, MemoBytes},
35 value::Zatoshis,
36};
37use zip32::Scope;
38use zip321::{Payment, TransactionRequest};
39
40use crate::{
41 data_api::{
42 self, Account as _, AccountBirthday, BoundedU8, DecryptedTransaction, InputSource,
43 MaxSpendMode, NoteFilter, Ratio, TargetValue, WalletCommitmentTrees, WalletRead,
44 WalletSummary, WalletTest, WalletWrite,
45 anchor_retention::AnchorRetentionInterval,
46 chain::{self, ChainState, CommitmentTreeRoot, ScanSummary},
47 error::{AddressExpiryError, Error},
48 testing::{
49 AddressType, CacheInsertionResult, FakeCompactOutput, InitialChainState, TestBuilder,
50 single_output_change_strategy,
51 },
52 wallet::{
53 ConfirmationsPolicy, TargetHeight, TransferErrT, decrypt_and_store_transaction,
54 input_selection::{GreedyInputSelector, LockFilter},
55 },
56 },
57 decrypt_transaction,
58 fees::{
59 self, DustOutputPolicy, SplitPolicy, StandardFeeRule,
60 standard::{self, SingleOutputChangeStrategy},
61 },
62 scanning::ScanError,
63 wallet::{Note, NoteId, OvkPolicy, ReceivedNote},
64};
65
66use super::{DataStoreFactory, Reset, TestCache, TestFvk, TestState};
67
68use crate::data_api::ll::wallet::PRUNING_DEPTH;
69use crate::data_api::wallet::input_selection::GreedyInputSelectorError;
70use crate::{
71 data_api::BlockMetadata,
72 scanning::{
73 ScanningKeys, SpendIdentifiers,
74 full::{decrypt_block, scan_block},
75 },
76};
77use incrementalmerkletree::Retention;
78use nonempty::NonEmpty;
79use shardtree::{ShardTree, store::ShardStore};
80use std::collections::BTreeSet;
81use zcash_primitives::block::{Block, BlockHeaderData};
82use zcash_protocol::PoolType;
83
84#[cfg(feature = "orchard")]
85use {
86 super::orchard::OrchardPoolTester,
87 crate::data_api::wallet::propose_transfer,
88 std::collections::BTreeMap,
89 zcash_primitives::transaction::{TxVersion, builder::BundlePadding},
90 zcash_protocol::zip318::{AnchorBucketInterval, MAX_RESIDUAL_VALUE},
91};
92
93#[cfg(not(feature = "orchard"))]
94use zcash_address::{
95 ZcashAddress,
96 unified::{self, Encoding as _, Receiver, Revision, Uitem},
97};
98
99#[cfg(all(feature = "orchard", not(feature = "transparent-inputs")))]
102use crate::proposal::ProposalError;
103
104#[cfg(any(feature = "orchard", feature = "transparent-inputs"))]
106use crate::data_api::wallet::input_selection::SpendPolicy;
107
108#[cfg(feature = "transparent-inputs")]
109use {
110 crate::{
111 data_api::{
112 CoinbaseFilter, OutputOfSentTx, TransactionDataRequest, TransactionStatus,
113 wallet::input_selection::TransparentSpendPolicy,
114 },
115 fees::ChangeValue,
116 proposal::{Proposal, ProposalError, StepOutput, StepOutputIndex},
117 wallet::{Exposure, TransparentAddressSource, WalletTransparentOutput},
118 },
119 secrecy::ExposeSecret,
120 std::str::FromStr,
121 transparent::{
122 bundle::{OutPoint, TxOut},
123 keys::{NonHardenedChildIndex, TransparentKeyScope},
124 },
125 zcash_keys::keys::transparent::gap_limits::GapLimits,
126 zcash_primitives::transaction::fees::{FeeRule, transparent::InputSize, zip317},
127 zcash_protocol::{
128 TxId,
129 value::{BalanceError, MAX_MONEY, ZatBalance},
130 },
131};
132
133#[cfg(feature = "pczt")]
134use {
135 crate::data_api::wallet::{SignerView, redact_pczt_for_batch_signer, redact_pczt_for_signer},
136 pczt::roles::{combiner::Combiner, prover::Prover, signer::Signer},
137 rand::rngs::SysRng,
138 rand_core::UnwrapErr,
139 transparent::builder::TransparentSigningSet,
140 zcash_primitives::transaction::builder::{BuildConfig, Builder},
141 zcash_proofs::prover::LocalTxProver,
142 zcash_protocol::consensus::ZIP212_GRACE_PERIOD,
143 zcash_script::opcode::PushValue,
144};
145
146#[cfg(all(feature = "pczt", feature = "transparent-inputs"))]
147use {
148 crate::data_api::wallet::input_selection::LockedInputPolicy,
149 zcash_protocol::consensus::COINBASE_MATURITY_BLOCKS,
150};
151
152pub mod dsl;
153use dsl::{TestDsl, TestNoteConfig, TestScenario};
154
155pub mod locking;
156pub use locking::*;
157
158#[cfg_attr(
166 feature = "orchard",
167 doc = "[`OrchardPoolTester`]: super::orchard::OrchardPoolTester"
168)]
169#[cfg_attr(
170 not(feature = "orchard"),
171 doc = "[`OrchardPoolTester`]: https://github.com/zcash/librustzcash/blob/0777cbc2def6ba6b99f96333eaf96c314c1f3a37/zcash_client_backend/src/data_api/testing/orchard.rs#L33"
172)]
173pub trait ShieldedPoolTester {
174 const SHIELDED_PROTOCOL: ShieldedPool;
175
176 type Sk;
177 type Fvk: TestFvk;
178 type MerkleTreeHash;
179 type Note;
180
181 fn test_account_fvk<Cache, DbT: WalletTest, P: consensus::Parameters>(
182 st: &TestState<Cache, DbT, P>,
183 ) -> Self::Fvk;
184 fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk;
185 fn sk(seed: &[u8]) -> Self::Sk;
186 fn sk_to_fvk(sk: &Self::Sk) -> Self::Fvk;
187 fn sk_default_address(sk: &Self::Sk) -> Address;
188 fn fvk_default_address(fvk: &Self::Fvk) -> Address;
189 fn fvks_equal(a: &Self::Fvk, b: &Self::Fvk) -> bool;
190
191 fn random_fvk(mut rng: impl Rng) -> Self::Fvk {
192 let sk = {
193 let mut sk_bytes = vec![0; 32];
194 rng.fill_bytes(&mut sk_bytes);
195 Self::sk(&sk_bytes)
196 };
197
198 Self::sk_to_fvk(&sk)
199 }
200 fn random_address(rng: impl Rng) -> Address {
201 Self::fvk_default_address(&Self::random_fvk(rng))
202 }
203
204 fn empty_tree_leaf() -> Self::MerkleTreeHash;
205 fn empty_tree_root(level: Level) -> Self::MerkleTreeHash;
206
207 fn put_subtree_roots<Cache, DbT: WalletTest + WalletCommitmentTrees, P>(
208 st: &mut TestState<Cache, DbT, P>,
209 start_index: u64,
210 roots: &[CommitmentTreeRoot<Self::MerkleTreeHash>],
211 ) -> Result<(), ShardTreeError<<DbT as WalletCommitmentTrees>::Error>>;
212
213 fn shard_root<Cache, DbT: WalletTest + WalletCommitmentTrees, P>(
219 st: &mut TestState<Cache, DbT, P>,
220 shard_index: u64,
221 ) -> Result<Self::MerkleTreeHash, ShardTreeError<<DbT as WalletCommitmentTrees>::Error>>;
222
223 fn next_subtree_index<A: Hash + Eq>(s: &WalletSummary<A>) -> u64;
224
225 fn note_value(note: &Self::Note) -> Zatoshis;
226
227 #[allow(clippy::type_complexity)]
228 fn select_spendable_notes<Cache, DbT: InputSource + WalletTest, P>(
229 st: &TestState<Cache, DbT, P>,
230 account: <DbT as InputSource>::AccountId,
231 target_value: TargetValue,
232 target_height: TargetHeight,
233 confirmations_policy: ConfirmationsPolicy,
234 exclude: &[DbT::NoteRef],
235 ) -> Result<Vec<ReceivedNote<DbT::NoteRef, Self::Note>>, <DbT as InputSource>::Error>;
236
237 #[allow(clippy::type_complexity)]
238 fn select_unspent_notes<Cache, DbT: InputSource + WalletTest, P>(
239 st: &TestState<Cache, DbT, P>,
240 account: <DbT as InputSource>::AccountId,
241 target_height: TargetHeight,
242 exclude: &[DbT::NoteRef],
243 ) -> Result<Vec<ReceivedNote<DbT::NoteRef, Self::Note>>, <DbT as InputSource>::Error>;
244
245 fn decrypted_pool_outputs_count<A>(d_tx: &DecryptedTransaction<Transaction, A>) -> usize;
246
247 fn with_decrypted_pool_memos<A>(
248 d_tx: &DecryptedTransaction<Transaction, A>,
249 f: impl FnMut(&MemoBytes),
250 );
251
252 fn try_output_recovery<P: consensus::Parameters>(
253 params: &P,
254 height: BlockHeight,
255 tx: &Transaction,
256 fvk: &Self::Fvk,
257 ) -> Option<(Note, Address, MemoBytes)>;
258
259 fn received_note_count(summary: &ScanSummary) -> usize;
260
261 #[cfg(feature = "pczt")]
262 fn add_proof_generation_keys(
263 pczt: pczt::Pczt,
264 usk: &UnifiedSpendingKey,
265 ) -> Result<pczt::Pczt, pczt::roles::updater::SaplingError>;
266
267 #[cfg(feature = "pczt")]
268 fn apply_signatures_to_pczt(
269 signer: &mut Signer,
270 usk: &UnifiedSpendingKey,
271 ) -> Result<(), pczt::roles::signer::Error>;
272}
273
274fn anchor_grid<P: consensus::Parameters>(
276 network: &P,
277 interval: zcash_protocol::zip318::AnchorBucketInterval,
278) -> zcash_protocol::zip318::AnchorGrid {
279 zcash_protocol::zip318::AnchorGrid::new(
280 interval,
281 zcash_protocol::zip318::BlockSpacing::for_network(network),
282 )
283}
284
285pub fn send_single_step_proposed_transfer<T: ShieldedPoolTester>(
296 dsf: impl DataStoreFactory,
297 cache: impl TestCache,
298) {
299 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
300
301 let (h, _, _) = st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60000));
303
304 let to_extsk = T::sk(&[0xf5; 32]);
305 let to: Address = T::sk_default_address(&to_extsk);
306 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
307 to.to_zcash_address(st.network()),
308 Zatoshis::const_from_u64(10000),
309 )])
310 .unwrap();
311
312 let fee_rule = StandardFeeRule::Zip317;
313
314 let change_memo = "Test change memo".parse::<Memo>().unwrap();
315 let change_strategy = standard::SingleOutputChangeStrategy::new(
316 fee_rule,
317 Some(change_memo.clone().into()),
318 T::SHIELDED_PROTOCOL,
319 DustOutputPolicy::default(),
320 );
321 let input_selector = GreedyInputSelector::new();
322
323 let account = st.get_account();
324 let proposal = st
325 .propose_transfer(
326 account.id(),
327 &input_selector,
328 &change_strategy,
329 request,
330 ConfirmationsPolicy::MIN,
331 )
332 .unwrap();
333
334 let sent_tx_id = st.create_proposed_expecting(&proposal, 1)[0];
335
336 let tx = st
338 .wallet()
339 .get_transaction(sent_tx_id)
340 .unwrap()
341 .expect("Created transaction was stored.");
342 let ufvks = [(account.id(), account.usk().to_unified_full_viewing_key())]
343 .into_iter()
344 .collect();
345 let d_tx = decrypt_transaction(st.network(), None, Some(h), &tx, &ufvks);
346 assert_eq!(T::decrypted_pool_outputs_count(&d_tx), 2);
347
348 let mut found_tx_change_memo = false;
349 let mut found_tx_empty_memo = false;
350 T::with_decrypted_pool_memos(&d_tx, |memo| {
351 if Memo::try_from(memo).unwrap() == change_memo {
352 found_tx_change_memo = true
353 }
354 if Memo::try_from(memo).unwrap() == Memo::Empty {
355 found_tx_empty_memo = true
356 }
357 });
358 assert!(found_tx_change_memo);
359 assert!(found_tx_empty_memo);
360
361 let sent_note_ids = st
363 .wallet()
364 .get_sent_note_ids(&sent_tx_id, T::SHIELDED_PROTOCOL)
365 .unwrap();
366 assert_eq!(sent_note_ids.len(), 2);
367
368 let mut found_sent_change_memo = false;
371 let mut found_sent_empty_memo = false;
372 for sent_note_id in sent_note_ids {
373 match st
374 .wallet()
375 .get_memo(sent_note_id)
376 .expect("Note id is valid")
377 .as_ref()
378 {
379 Some(m) if m == &change_memo => {
380 found_sent_change_memo = true;
381 }
382 Some(m) if m == &Memo::Empty => {
383 found_sent_empty_memo = true;
384 }
385 Some(other) => panic!("Unexpected memo value: {other:?}"),
386 None => panic!("Memo should not be stored as NULL"),
387 }
388 }
389 assert!(found_sent_change_memo);
390 assert!(found_sent_empty_memo);
391
392 assert_matches!(
394 st.wallet()
395 .get_memo(NoteId::new(sent_tx_id, T::SHIELDED_PROTOCOL, 12345)),
396 Ok(None)
397 );
398
399 let tx_history = st.wallet().get_tx_history().unwrap();
400 assert_eq!(tx_history.len(), 2);
401 {
402 let tx_0 = &tx_history[0];
403 assert_eq!(tx_0.total_spent(), Zatoshis::const_from_u64(0));
404 assert_eq!(tx_0.total_received(), Zatoshis::const_from_u64(60000));
405 }
406
407 {
408 let tx_1 = &tx_history[1];
409 assert_eq!(tx_1.total_spent(), Zatoshis::const_from_u64(60000));
410 assert_eq!(tx_1.total_received(), Zatoshis::const_from_u64(40000));
411 }
412
413 let network = *st.network();
414 assert_matches!(
415 decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, None),
416 Ok(_)
417 );
418}
419
420pub fn scan_full_block_detects_outputs<T: ShieldedPoolTester>(
427 dsf: impl DataStoreFactory,
428 cache: impl TestCache,
429) {
430 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
431
432 let (h, _, _) = st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60000));
434
435 let to_extsk = T::sk(&[0xf5; 32]);
438 let to: Address = T::sk_default_address(&to_extsk);
439 let request = TransactionRequest::new(vec![Payment::without_memo(
440 to.to_zcash_address(st.network()),
441 Zatoshis::const_from_u64(10000),
442 )])
443 .unwrap();
444
445 let change_strategy =
446 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
447 let input_selector = GreedyInputSelector::new();
448
449 let account = st.get_account();
450 let proposal = st
451 .propose_transfer(
452 account.id(),
453 &input_selector,
454 &change_strategy,
455 request,
456 ConfirmationsPolicy::MIN,
457 )
458 .unwrap();
459
460 let txids = st
461 .create_proposed_transactions::<Infallible, _, Infallible, _>(
462 account.usk(),
463 OvkPolicy::Sender,
464 &proposal,
465 )
466 .unwrap();
467 assert_eq!(txids.len(), 1);
468
469 let tx = st
470 .wallet()
471 .get_transaction(*txids.first())
472 .unwrap()
473 .expect("the created transaction was stored");
474
475 let ufvk = account.usk().to_unified_full_viewing_key();
478 let scanning_keys = ScanningKeys::from_account_ufvks([(zip32::AccountId::ZERO, ufvk)]);
479
480 let network = *st.network();
484 let header = BlockHeaderData {
485 version: 4,
486 prev_block: BlockHash([0; 32]),
487 merkle_root: [0; 32],
488 final_sapling_root: [0; 32],
489 time: 0,
490 bits: 0,
491 nonce: [0; 32],
492 solution: vec![],
493 }
494 .freeze()
495 .unwrap();
496 let block = Block::from_parts(header, NonEmpty::singleton(tx), h);
497
498 let prior_block_metadata = BlockMetadata::from_parts(
499 h - 1,
500 BlockHash([0; 32]),
501 Some(0),
502 #[cfg(feature = "orchard")]
503 Some(0),
504 #[cfg(feature = "orchard")]
505 Some(0),
506 );
507
508 let (header, vtx) = decrypt_block(&network, block, &scanning_keys);
510
511 #[cfg(feature = "transparent-inputs")]
513 let scanned = scan_block(
514 &network,
515 h,
516 &header,
517 vtx,
518 &scanning_keys,
519 &SpendIdentifiers::empty(),
520 Some(&prior_block_metadata),
521 |_addr| Ok::<_, Infallible>(None),
522 )
523 .expect("scanning the block succeeds");
524 #[cfg(not(feature = "transparent-inputs"))]
525 let scanned = scan_block::<_, _, _, Infallible>(
526 &network,
527 h,
528 &header,
529 vtx,
530 &scanning_keys,
531 &SpendIdentifiers::empty(),
532 Some(&prior_block_metadata),
533 )
534 .expect("scanning the block succeeds");
535
536 assert_eq!(scanned.transactions().len(), 1);
539 let received_outputs: usize = scanned
540 .transactions()
541 .iter()
542 .map(|wtx| {
543 let n = wtx.sapling_outputs().len();
544 #[cfg(feature = "orchard")]
545 let n = n + wtx.orchard_outputs().len();
546 n
547 })
548 .sum();
549 assert_eq!(received_outputs, 1);
550
551 let total_commitments = scanned.sapling().commitments().len();
556 #[cfg(feature = "orchard")]
557 let total_commitments = total_commitments + scanned.orchard().commitments().len();
558 assert_eq!(total_commitments, 2);
559
560 let total_final_tree_size = scanned.sapling().final_tree_size();
561 #[cfg(feature = "orchard")]
562 let total_final_tree_size = total_final_tree_size + scanned.orchard().final_tree_size();
563 assert_eq!(total_final_tree_size, 2);
564
565 let last_retention = scanned.sapling().commitments().last().map(|(_, r)| *r);
570 #[cfg(feature = "orchard")]
571 let last_retention = scanned
572 .orchard()
573 .commitments()
574 .last()
575 .map(|(_, r)| *r)
576 .or(last_retention);
577 assert!(
578 matches!(last_retention, Some(Retention::Checkpoint { id, .. }) if id == h),
579 "final note commitment should be a checkpoint at height {h:?}, got {last_retention:?}",
580 );
581}
582
583#[derive(Clone, Copy, Debug, PartialEq)]
584struct ConfirmationStep {
585 i: u32,
586 confirmation_requirement: u32,
587 number_of_confirmations: u32,
588 pending_balance: Zatoshis,
589 spendable_balance: Zatoshis,
590 total_balance: Zatoshis,
591}
592
593#[derive(Clone, Copy, Debug, PartialEq, Eq)]
596pub enum InputTrust {
597 Internal,
599 ExternalUntrusted,
601 ExternalTrusted,
603}
604
605pub fn zip_315_confirmations_test_steps<T: ShieldedPoolTester>(
613 dsf: impl DataStoreFactory,
614 cache: impl TestCache,
615 input_trust: InputTrust,
616) {
617 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
618 let account = st.test_account().cloned().unwrap();
619 let starting_balance = Zatoshis::const_from_u64(60_000);
620
621 let confirmations_policy = ConfirmationsPolicy::default();
624 let (address_type, min_confirmations) = match input_trust {
625 InputTrust::Internal => (AddressType::Internal, confirmations_policy.trusted()),
626 InputTrust::ExternalUntrusted => (
627 AddressType::DefaultExternal,
628 confirmations_policy.untrusted(),
629 ),
630 InputTrust::ExternalTrusted => {
631 (AddressType::DefaultExternal, confirmations_policy.trusted())
632 }
633 };
634 let min_confirmations = u32::from(min_confirmations);
635
636 let (_, r, _) = st.add_a_single_note_checking_balance(
637 TestNoteConfig::from(starting_balance).with_address_type(address_type),
638 );
639 let txid = r.txids()[0];
640
641 let trusted = input_trust == InputTrust::ExternalTrusted;
643 if trusted {
644 st.wallet_mut().set_tx_trust(txid, true).unwrap();
645 }
646
647 let add_confirmation = |i: u32| {
648 let (h, _) = st.generate_empty_block();
649 st.scan_cached_blocks(h, 1);
650 let outputs = st
651 .wallet()
652 .get_received_outputs(txid, TargetHeight::from(h + 1), confirmations_policy)
653 .unwrap();
654 assert_eq!(outputs.len(), 1);
655 assert_eq!(
656 outputs[0].confirmations_until_spendable(),
657 u32::from(if trusted {
658 confirmations_policy.trusted()
659 } else {
660 confirmations_policy.untrusted()
661 })
662 .saturating_sub(i + 1)
663 );
664 ConfirmationStep {
665 i,
666 confirmation_requirement: min_confirmations,
667 number_of_confirmations: 1 + i,
668 pending_balance: st.get_pending_shielded_balance(account.id(), confirmations_policy),
669 spendable_balance: st.get_spendable_balance(account.id(), confirmations_policy),
670 total_balance: st.get_total_balance(account.id()),
671 }
672 };
673
674 let steps = (1u32..min_confirmations)
676 .map(add_confirmation)
677 .collect::<Vec<_>>();
678
679 assert!(
680 steps
681 .iter()
682 .filter(|step| step.number_of_confirmations < min_confirmations)
683 .all(|step| step.spendable_balance == Zatoshis::ZERO),
684 "spendable balance is equal to starting balance until we have sufficient confirmations"
685 );
686
687 let to = T::random_address(st.rng_mut());
688 let proposed = st.propose_standard_transfer::<Infallible>(
690 account.id(),
691 StandardFeeRule::Zip317,
692 confirmations_policy,
693 &to,
694 Zatoshis::const_from_u64(10_000),
695 None,
696 None,
697 T::SHIELDED_PROTOCOL,
698 );
699 assert!(
700 proposed.is_ok(),
701 "Could not spend funds by confirmation policy ({input_trust:?}): {proposed:#?}\n\
702 steps: {steps:#?}",
703 );
704}
705
706pub fn spend_max_spendable_single_step_proposed_transfer<T: ShieldedPoolTester>(
718 dsf: impl DataStoreFactory,
719 cache: impl TestCache,
720) {
721 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
722
723 let value = Zatoshis::const_from_u64(60000);
725 let h = st
726 .add_notes_checking_balance([Some(value), None, None, None, Some(value)])
727 .block_height()
728 .unwrap();
729
730 let account = st.test_account().cloned().unwrap();
732 let confirmation_policy = ConfirmationsPolicy::new_symmetrical(
733 NonZeroU32::new(2).expect("2 is not zero"),
734 #[cfg(feature = "transparent-inputs")]
735 false,
736 );
737 assert_eq!(
738 st.get_spendable_balance(account.id(), confirmation_policy),
739 value
740 );
741
742 let to_extsk = T::sk(&[0xf5; 32]);
743 let to: Address = T::sk_default_address(&to_extsk);
744
745 let fee_rule = StandardFeeRule::Zip317;
746
747 let send_max_memo = "Test Send Max memo".parse::<Memo>().unwrap();
748
749 let addy = to.to_zcash_address(st.network());
750 let proposal = st
751 .propose_send_max_transfer(
752 account.id(),
753 &fee_rule,
754 addy,
755 Some(MemoBytes::from(send_max_memo.clone())),
756 MaxSpendMode::MaxSpendable,
757 confirmation_policy,
758 )
759 .unwrap();
760
761 let sent_tx_id = st.create_proposed_expecting(&proposal, 1)[0];
762
763 let tx = st
765 .wallet()
766 .get_transaction(sent_tx_id)
767 .unwrap()
768 .expect("Created transaction was stored.");
769 let ufvks = [(account.id(), account.usk().to_unified_full_viewing_key())]
770 .into_iter()
771 .collect();
772 let d_tx = decrypt_transaction(st.network(), None, Some(h), &tx, &ufvks);
773 assert_eq!(T::decrypted_pool_outputs_count(&d_tx), 1);
774
775 let mut found_send_max_memo = false;
776 let mut found_tx_empty_memo = false;
777 T::with_decrypted_pool_memos(&d_tx, |memo| {
778 if Memo::try_from(memo).unwrap() == send_max_memo {
779 found_send_max_memo = true
780 }
781 if Memo::try_from(memo).unwrap() == Memo::Empty {
782 found_tx_empty_memo = true
783 }
784 });
785 assert!(found_send_max_memo);
786 assert!(!found_tx_empty_memo); let sent_note_ids = st
790 .wallet()
791 .get_sent_note_ids(&sent_tx_id, T::SHIELDED_PROTOCOL)
792 .unwrap();
793 assert_eq!(sent_note_ids.len(), 1);
794
795 let mut found_sent_empty_memo = false;
797 let mut found_sent_max_memo = false;
798 for sent_note_id in sent_note_ids {
799 match st
800 .wallet()
801 .get_memo(sent_note_id)
802 .expect("Memo retrieval should succeed")
803 .as_ref()
804 {
805 Some(m) if m == &Memo::Empty => {
806 found_sent_empty_memo = true;
807 }
808 Some(m) if m == &send_max_memo => {
809 found_sent_max_memo = true;
810 }
811 Some(other) => panic!("Unexpected memo value: {other:?}"),
812 None => panic!("Memo should not be stored as NULL"),
813 }
814 }
815
816 assert!(found_sent_max_memo);
817 assert!(!found_sent_empty_memo);
818
819 assert_matches!(
821 st.wallet()
822 .get_memo(NoteId::new(sent_tx_id, T::SHIELDED_PROTOCOL, 12345)),
823 Ok(None)
824 );
825
826 let tx_history = st.wallet().get_tx_history().unwrap();
827 assert_eq!(tx_history.len(), 3);
828 {
829 let tx_0 = &tx_history[0];
830 assert_eq!(tx_0.total_spent(), Zatoshis::const_from_u64(0));
831 assert_eq!(tx_0.total_received(), Zatoshis::const_from_u64(60000));
832 }
833 {
834 let tx_1 = &tx_history[1];
835 assert_eq!(tx_1.total_spent(), Zatoshis::const_from_u64(0));
836 assert_eq!(tx_1.total_received(), Zatoshis::const_from_u64(60000));
837 }
838 {
839 let tx_2 = &tx_history[2];
840 assert_eq!(tx_2.total_spent(), Zatoshis::const_from_u64(60000));
841 assert_eq!(tx_2.total_received(), Zatoshis::const_from_u64(0));
842 }
843
844 let network = *st.network();
845 assert_matches!(
846 decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, None),
847 Ok(_)
848 );
849}
850
851pub fn spend_everything_single_step_proposed_transfer<T: ShieldedPoolTester>(
863 dsf: impl DataStoreFactory,
864 cache: impl TestCache,
865) {
866 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
867
868 let account = st.test_account().cloned().unwrap();
869
870 let (h, _, _) = st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60000));
872
873 let to_extsk = T::sk(&[0xf5; 32]);
874 let to: Address = T::sk_default_address(&to_extsk);
875
876 let fee_rule = StandardFeeRule::Zip317;
877
878 let send_max_memo = "Test Send Max memo".parse::<Memo>().unwrap();
879
880 let addy = to.to_zcash_address(st.network());
881 let proposal = st
882 .propose_send_max_transfer(
883 account.id(),
884 &fee_rule,
885 addy,
886 Some(MemoBytes::from(send_max_memo.clone())),
887 MaxSpendMode::Everything,
888 ConfirmationsPolicy::MIN,
889 )
890 .unwrap();
891
892 let sent_tx_id = st.create_proposed_expecting(&proposal, 1)[0];
893
894 let tx = st
896 .wallet()
897 .get_transaction(sent_tx_id)
898 .unwrap()
899 .expect("Created transaction was stored.");
900 let ufvks = [(account.id(), account.usk().to_unified_full_viewing_key())]
901 .into_iter()
902 .collect();
903 let d_tx = decrypt_transaction(st.network(), None, Some(h), &tx, &ufvks);
904 assert_eq!(T::decrypted_pool_outputs_count(&d_tx), 1);
905
906 let mut found_send_max_memo = false;
907 let mut found_tx_empty_memo = false;
908 T::with_decrypted_pool_memos(&d_tx, |memo| {
909 if Memo::try_from(memo).unwrap() == send_max_memo {
910 found_send_max_memo = true
911 }
912 if Memo::try_from(memo).unwrap() == Memo::Empty {
913 found_tx_empty_memo = true
914 }
915 });
916 assert!(found_send_max_memo);
917 assert!(!found_tx_empty_memo); let sent_note_ids = st
921 .wallet()
922 .get_sent_note_ids(&sent_tx_id, T::SHIELDED_PROTOCOL)
923 .unwrap();
924 assert_eq!(sent_note_ids.len(), 1);
925
926 let mut found_sent_empty_memo = false;
928 let mut found_sent_max_memo = false;
929 for sent_note_id in sent_note_ids {
930 match st
931 .wallet()
932 .get_memo(sent_note_id)
933 .expect("Memo retrieval should succeed")
934 .as_ref()
935 {
936 Some(m) if m == &Memo::Empty => {
937 found_sent_empty_memo = true;
938 }
939 Some(m) if m == &send_max_memo => {
940 found_sent_max_memo = true;
941 }
942 Some(other) => panic!("Unexpected memo value: {other:?}"),
943 None => panic!("Memo should not be stored as NULL"),
944 }
945 }
946
947 assert!(found_sent_max_memo);
948 assert!(!found_sent_empty_memo);
949
950 assert_matches!(
952 st.wallet()
953 .get_memo(NoteId::new(sent_tx_id, T::SHIELDED_PROTOCOL, 12345)),
954 Ok(None)
955 );
956
957 let tx_history = st.wallet().get_tx_history().unwrap();
958 assert_eq!(tx_history.len(), 2);
959 {
960 let tx_0 = &tx_history[0];
961 assert_eq!(tx_0.total_spent(), Zatoshis::const_from_u64(0));
962 assert_eq!(tx_0.total_received(), Zatoshis::const_from_u64(60000));
963 }
964
965 {
966 let tx_1 = &tx_history[1];
967 assert_eq!(tx_1.total_spent(), Zatoshis::const_from_u64(60000));
968 assert_eq!(tx_1.total_received(), Zatoshis::const_from_u64(0));
969 }
970
971 let network = *st.network();
972 assert_matches!(
973 decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, None),
974 Ok(_)
975 );
976}
977
978#[cfg(feature = "transparent-inputs")]
987pub fn fails_to_send_max_spendable_to_transparent_with_memo<T: ShieldedPoolTester>(
988 dsf: impl DataStoreFactory,
989 cache: impl TestCache,
990) {
991 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
992
993 st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60000));
995
996 let account = st.test_account().cloned().unwrap();
997 let (default_addr, _) = account.usk().default_transparent_address();
998
999 let to: Address = Address::Transparent(default_addr);
1000
1001 let fee_rule = StandardFeeRule::Zip317;
1002
1003 let send_max_memo = "Test Send Max memo".parse::<Memo>().unwrap();
1004
1005 let addy = to.to_zcash_address(st.network());
1006 assert_matches!(
1007 st.propose_send_max_transfer(
1008 account.id(),
1009 &fee_rule,
1010 addy,
1011 Some(MemoBytes::from(send_max_memo.clone())),
1012 MaxSpendMode::Everything,
1013 ConfirmationsPolicy::MIN
1014 ),
1015 Err(data_api::error::Error::Payment(
1016 zip321::PaymentError::TransparentMemo
1017 ))
1018 );
1019}
1020
1021#[cfg(feature = "transparent-inputs")]
1031pub fn send_max_spendable_to_transparent<T: ShieldedPoolTester>(
1032 dsf: impl DataStoreFactory,
1033 cache: impl TestCache,
1034) {
1035 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
1036
1037 let value = Zatoshis::const_from_u64(60000);
1039 st.add_a_single_note_checking_balance(value);
1040
1041 let account = st.test_account().cloned().unwrap();
1042 let to: Address = Address::Transparent(TransparentAddress::PublicKeyHash([0x7f; 20]));
1043
1044 let fee_rule = StandardFeeRule::Zip317;
1045
1046 let expected_fee = (zip317::MARGINAL_FEE * 3u64).unwrap();
1049 let expected_payment = (value - expected_fee).unwrap();
1050
1051 let addy = to.to_zcash_address(st.network());
1052 let proposal = st
1053 .propose_send_max_transfer(
1054 account.id(),
1055 &fee_rule,
1056 addy,
1057 None,
1058 MaxSpendMode::Everything,
1059 ConfirmationsPolicy::MIN,
1060 )
1061 .unwrap();
1062
1063 let steps: Vec<_> = proposal.steps().iter().cloned().collect();
1064 assert_eq!(steps.len(), 1);
1065 assert_eq!(steps[0].balance().fee_required(), expected_fee);
1066 assert_eq!(steps[0].balance().proposed_change(), []);
1067 assert_eq!(
1068 steps[0].payment_pools(),
1069 &std::collections::BTreeMap::from([(0, PoolType::TRANSPARENT)])
1070 );
1071 assert_matches!(
1072 steps[0].transaction_request().payments().get(&0),
1073 Some(payment) if payment.amount() == Some(expected_payment)
1074 );
1075
1076 st.create_proposed_expecting(&proposal, 1);
1077}
1078
1079#[cfg(feature = "transparent-inputs")]
1082pub fn send_max_fee_overflow_is_an_error<T: ShieldedPoolTester>(
1083 dsf: impl DataStoreFactory,
1084 cache: impl TestCache,
1085) {
1086 #[derive(Clone, Debug)]
1088 struct MaxMoneyFeeRule;
1089
1090 impl FeeRule for MaxMoneyFeeRule {
1091 type Error = Infallible;
1092
1093 fn fee_required<P: consensus::Parameters>(
1094 &self,
1095 _params: &P,
1096 _target_height: BlockHeight,
1097 _transparent_input_sizes: impl IntoIterator<Item = InputSize>,
1098 _transparent_output_sizes: impl IntoIterator<Item = usize>,
1099 _sapling_input_count: usize,
1100 _sapling_output_count: usize,
1101 _orchard_action_count: usize,
1102 _ironwood_action_count: usize,
1103 ) -> Result<Zatoshis, Self::Error> {
1104 Ok(Zatoshis::const_from_u64(MAX_MONEY))
1105 }
1106 }
1107
1108 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
1109
1110 st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60000));
1112
1113 let account = st.test_account().cloned().unwrap();
1114
1115 let tex_addr = Address::Tex([0x4; 20]);
1118 let addy = tex_addr.to_zcash_address(st.network());
1119
1120 assert_matches!(
1121 st.propose_send_max_transfer(
1122 account.id(),
1123 &MaxMoneyFeeRule,
1124 addy,
1125 None,
1126 MaxSpendMode::Everything,
1127 ConfirmationsPolicy::MIN,
1128 ),
1129 Err(data_api::error::Error::NoteSelection(
1130 GreedyInputSelectorError::Balance(BalanceError::Overflow)
1131 ))
1132 );
1133}
1134
1135#[cfg(feature = "orchard")]
1146pub fn send_max_spends_inputs_across_pools<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
1147 ds_factory: impl DataStoreFactory,
1148 cache: impl TestCache,
1149) {
1150 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<P0>();
1151 let account = st.test_account().cloned().unwrap();
1152
1153 let p0_fvk = P0::test_account_fvk(&st);
1155 let p1_fvk = P1::test_account_fvk(&st);
1156 let note_value = Zatoshis::const_from_u64(350000);
1157 st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value);
1158 st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value);
1159 st.scan_cached_blocks(account.birthday().height(), 2);
1160
1161 let total = (note_value * 2u64).unwrap();
1162 assert_eq!(
1163 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
1164 total
1165 );
1166
1167 let to: Address = P1::sk_default_address(&P1::sk(&[0xf5; 32]));
1168 let fee_rule = StandardFeeRule::Zip317;
1169
1170 let expected_fee = (MARGINAL_FEE * 4u64).unwrap();
1174 let expected_payment = (total - expected_fee).unwrap();
1175
1176 let addy = to.to_zcash_address(st.network());
1177 let proposal = st
1178 .propose_send_max_transfer(
1179 account.id(),
1180 &fee_rule,
1181 addy,
1182 None,
1183 MaxSpendMode::Everything,
1184 ConfirmationsPolicy::MIN,
1185 )
1186 .unwrap();
1187
1188 let steps: Vec<_> = proposal.steps().iter().cloned().collect();
1189 assert_eq!(steps.len(), 1);
1190 assert_eq!(steps[0].balance().fee_required(), expected_fee);
1191 assert_eq!(steps[0].balance().proposed_change(), []);
1192 assert_eq!(
1193 steps[0].payment_pools(),
1194 &BTreeMap::from([(0, PoolType::Shielded(P1::SHIELDED_PROTOCOL))])
1195 );
1196 assert_matches!(
1197 steps[0].transaction_request().payments().get(&0),
1198 Some(payment) if payment.amount() == Some(expected_payment)
1199 );
1200
1201 let input_notes = steps[0]
1203 .shielded_inputs()
1204 .expect("the proposal has shielded inputs")
1205 .notes();
1206 assert_eq!(input_notes.len(), 2);
1207 let input_pools = input_notes
1208 .iter()
1209 .map(|n| n.note().pool())
1210 .collect::<BTreeSet<_>>();
1211 assert_eq!(
1212 input_pools,
1213 BTreeSet::from([P0::SHIELDED_PROTOCOL, P1::SHIELDED_PROTOCOL])
1214 );
1215
1216 let txids = st.create_proposed_expecting(&proposal, 1);
1217
1218 let (h, _) = st.generate_next_block_including(txids[0]);
1220 st.scan_cached_blocks(h, 1);
1221 assert_eq!(st.get_total_balance(account.id()), Zatoshis::ZERO);
1222}
1223
1224#[cfg(feature = "orchard")]
1227const DUST_SHORTFALL_P0_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(60_000);
1228#[cfg(feature = "orchard")]
1232const DUST_SHORTFALL_PAYMENT: Zatoshis = Zatoshis::const_from_u64(47_000);
1233#[cfg(feature = "orchard")]
1236const DUST_SHORTFALL_P1_NOTE_COVERING_FEE: Zatoshis = Zatoshis::const_from_u64(20_000);
1237#[cfg(feature = "orchard")]
1240const DUST_SHORTFALL_P1_NOTE_BELOW_FEE: Zatoshis = Zatoshis::const_from_u64(6_000);
1241
1242#[cfg(feature = "orchard")]
1245fn two_bundle_fee() -> Zatoshis {
1246 const BUNDLES: u64 = 2;
1247 const PADDED_ACTIONS_PER_BUNDLE: u64 = 2;
1248 (MARGINAL_FEE * (BUNDLES * PADDED_ACTIONS_PER_BUNDLE)).unwrap()
1249}
1250
1251#[cfg(feature = "orchard")]
1255struct DustShortfallScenario<P0: ShieldedPoolTester, Cache, Dsf: DataStoreFactory> {
1256 st: TestDsl<dsl::TestScenario<P0, Cache, Dsf>>,
1257 recipient: Address,
1258 amount: Zatoshis,
1259 total: Zatoshis,
1261}
1262
1263#[cfg(feature = "orchard")]
1266fn dust_shortfall_across_pools<P0, P1, Cache, Dsf>(
1267 ds_factory: Dsf,
1268 cache: Cache,
1269 p1_note_value: Zatoshis,
1270) -> DustShortfallScenario<P0, Cache, Dsf>
1271where
1272 P0: ShieldedPoolTester,
1273 P1: ShieldedPoolTester,
1274 Cache: TestCache,
1275 Dsf: DataStoreFactory,
1276{
1277 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<P0>();
1278 let account = st.test_account().cloned().unwrap();
1279
1280 let p0_fvk = P0::test_account_fvk(&st);
1281 let p1_fvk = P1::test_account_fvk(&st);
1282 st.generate_next_block(
1283 &p0_fvk,
1284 AddressType::DefaultExternal,
1285 DUST_SHORTFALL_P0_NOTE_VALUE,
1286 );
1287 st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, p1_note_value);
1288 st.scan_cached_blocks(account.birthday().height(), 2);
1289
1290 DustShortfallScenario {
1291 st,
1292 recipient: P0::sk_default_address(&P0::sk(&[0xf5; 32])),
1293 amount: DUST_SHORTFALL_PAYMENT,
1294 total: (DUST_SHORTFALL_P0_NOTE_VALUE + p1_note_value).unwrap(),
1295 }
1296}
1297
1298#[cfg(feature = "orchard")]
1308pub fn transfer_grows_across_pools_at_a_dust_shortfall<
1309 P0: ShieldedPoolTester,
1310 P1: ShieldedPoolTester,
1311>(
1312 ds_factory: impl DataStoreFactory,
1313 cache: impl TestCache,
1314) {
1315 let DustShortfallScenario {
1316 mut st,
1317 recipient,
1318 amount,
1319 total,
1320 } = dust_shortfall_across_pools::<P0, P1, _, _>(
1321 ds_factory,
1322 cache,
1323 DUST_SHORTFALL_P1_NOTE_COVERING_FEE,
1324 );
1325
1326 let expected_fee = two_bundle_fee();
1327 let expected_change = (total - amount - expected_fee).unwrap();
1328
1329 let proposal = st.propose_transfer_to(&recipient, amount);
1330
1331 assert_eq!(proposal.steps().len(), 1);
1332 let step = &proposal.steps().head;
1333 assert_eq!(step.balance().fee_required(), expected_fee);
1334 assert_matches!(
1335 step.transaction_request().payments().get(&0),
1336 Some(payment) if payment.amount() == Some(amount)
1337 );
1338 let input_pools = step
1339 .shielded_inputs()
1340 .expect("the proposal has shielded inputs")
1341 .notes()
1342 .iter()
1343 .map(|n| n.note().pool())
1344 .collect::<BTreeSet<_>>();
1345 assert_eq!(
1346 input_pools,
1347 BTreeSet::from([P0::SHIELDED_PROTOCOL, P1::SHIELDED_PROTOCOL])
1348 );
1349 let change = step.balance().proposed_change();
1350 assert_eq!(change.len(), 1);
1351 assert_eq!(change[0].value(), expected_change);
1352
1353 let txids = st.create_proposed_expecting(&proposal, 1);
1354 let (h, _) = st.generate_next_block_including(txids[0]);
1355 st.scan_cached_blocks(h, 1);
1356 assert_eq!(st.get_total_balance(st.get_account().id()), expected_change);
1357}
1358
1359#[cfg(feature = "orchard")]
1368pub fn transfer_fails_across_pools_at_a_dust_shortfall<
1369 P0: ShieldedPoolTester,
1370 P1: ShieldedPoolTester,
1371>(
1372 ds_factory: impl DataStoreFactory,
1373 cache: impl TestCache,
1374) {
1375 let DustShortfallScenario {
1376 mut st,
1377 recipient,
1378 amount,
1379 total,
1380 } = dust_shortfall_across_pools::<P0, P1, _, _>(
1381 ds_factory,
1382 cache,
1383 DUST_SHORTFALL_P1_NOTE_BELOW_FEE,
1384 );
1385
1386 let required = (amount + two_bundle_fee()).unwrap();
1387
1388 st.expect_insufficient_funds(&recipient, amount, total, required);
1389}
1390
1391#[cfg(not(feature = "transparent-inputs"))]
1394pub fn send_max_to_tex_fails_without_transparent_inputs<T: ShieldedPoolTester>(
1395 dsf: impl DataStoreFactory,
1396 cache: impl TestCache,
1397) {
1398 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
1399
1400 st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60000));
1402
1403 let account = st.test_account().cloned().unwrap();
1404 let tex_addr = Address::Tex([0x4; 20]);
1405 let fee_rule = StandardFeeRule::Zip317;
1406
1407 let addy = tex_addr.to_zcash_address(st.network());
1408 assert_matches!(
1409 st.propose_send_max_transfer(
1410 account.id(),
1411 &fee_rule,
1412 addy,
1413 None,
1414 MaxSpendMode::Everything,
1415 ConfirmationsPolicy::MIN,
1416 ),
1417 Err(data_api::error::Error::NoteSelection(
1418 GreedyInputSelectorError::UnsupportedTexAddress
1419 ))
1420 );
1421}
1422
1423#[cfg(not(feature = "orchard"))]
1427pub fn send_max_delivers_via_sapling_when_orchard_is_unavailable<T: ShieldedPoolTester>(
1428 dsf: impl DataStoreFactory,
1429 cache: impl TestCache,
1430) {
1431 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
1432
1433 let value = Zatoshis::const_from_u64(60000);
1435 st.add_a_single_note_checking_balance(value);
1436
1437 let account = st.test_account().cloned().unwrap();
1438
1439 let sapling_receiver = match T::sk_default_address(&T::sk(&[0xf5; 32])) {
1443 Address::Sapling(addr) => addr.to_bytes(),
1444 _ => panic!("expected a Sapling address"),
1445 };
1446 let ua = unified::Address::try_from_items(
1447 Revision::R0,
1448 vec![
1449 Uitem::Data(Receiver::Sapling(sapling_receiver)),
1450 Uitem::Data(Receiver::Orchard([0xab; 43])),
1451 ],
1452 )
1453 .unwrap();
1454 let addy = ZcashAddress::try_from_encoded(&ua.encode(&st.network().network_type())).unwrap();
1455
1456 let fee_rule = StandardFeeRule::Zip317;
1457
1458 let expected_fee = MINIMUM_FEE;
1461 let expected_payment = (value - expected_fee).unwrap();
1462
1463 let proposal = st
1464 .propose_send_max_transfer(
1465 account.id(),
1466 &fee_rule,
1467 addy,
1468 None,
1469 MaxSpendMode::Everything,
1470 ConfirmationsPolicy::MIN,
1471 )
1472 .unwrap();
1473
1474 let steps: Vec<_> = proposal.steps().iter().cloned().collect();
1475 assert_eq!(steps.len(), 1);
1476 assert_eq!(steps[0].balance().fee_required(), expected_fee);
1477 assert_eq!(
1478 steps[0].payment_pools(),
1479 &std::collections::BTreeMap::from([(0, PoolType::SAPLING)])
1480 );
1481 assert_matches!(
1482 steps[0].transaction_request().payments().get(&0),
1483 Some(payment) if payment.amount() == Some(expected_payment)
1484 );
1485
1486 st.create_proposed_expecting(&proposal, 1);
1487}
1488
1489#[cfg(not(feature = "orchard"))]
1493pub fn send_max_to_orchard_only_ua_fails_without_orchard<T: ShieldedPoolTester>(
1494 dsf: impl DataStoreFactory,
1495 cache: impl TestCache,
1496) {
1497 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
1498
1499 st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60000));
1501
1502 let account = st.test_account().cloned().unwrap();
1503
1504 let ua = unified::Address::try_from_items(
1507 Revision::R0,
1508 vec![Uitem::Data(Receiver::Orchard([0xab; 43]))],
1509 )
1510 .unwrap();
1511 let addy = ZcashAddress::try_from_encoded(&ua.encode(&st.network().network_type())).unwrap();
1512
1513 let fee_rule = StandardFeeRule::Zip317;
1514
1515 assert_matches!(
1516 st.propose_send_max_transfer(
1517 account.id(),
1518 &fee_rule,
1519 addy,
1520 None,
1521 MaxSpendMode::Everything,
1522 ConfirmationsPolicy::MIN,
1523 ),
1524 Err(data_api::error::Error::NoteSelection(
1525 GreedyInputSelectorError::UnsupportedAddress(_)
1526 ))
1527 );
1528}
1529
1530pub fn send_max_fails_when_balance_is_consumed_by_fees<T: ShieldedPoolTester>(
1534 dsf: impl DataStoreFactory,
1535 cache: impl TestCache,
1536) {
1537 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
1538
1539 let value = MINIMUM_FEE;
1542 st.add_a_single_note_checking_balance(value);
1543
1544 let account = st.test_account().cloned().unwrap();
1545 let to: Address = T::sk_default_address(&T::sk(&[0xf5; 32]));
1546 let fee_rule = StandardFeeRule::Zip317;
1547
1548 let addy = to.to_zcash_address(st.network());
1549 assert_matches!(
1550 st.propose_send_max_transfer(
1551 account.id(),
1552 &fee_rule,
1553 addy,
1554 None,
1555 MaxSpendMode::Everything,
1556 ConfirmationsPolicy::MIN,
1557 ),
1558 Err(data_api::error::Error::InsufficientFunds { available, required })
1559 if available == value && required > value
1560 );
1561}
1562
1563pub fn spend_everything_proposal_fails_when_unconfirmed_funds_present<T: ShieldedPoolTester>(
1576 dsf: impl DataStoreFactory,
1577 cache: impl TestCache,
1578) {
1579 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
1580 st.add_notes_checking_balance([
1581 Some(Zatoshis::const_from_u64(60000)),
1582 None,
1583 None,
1584 Some(Zatoshis::const_from_u64(123456)),
1585 ]);
1586
1587 let account = st.test_account().cloned().unwrap();
1589 let total_balance = st.get_total_balance(account.id());
1590 let spendable_balance = st.get_spendable_balance(
1591 account.id(),
1592 ConfirmationsPolicy::new_symmetrical_unchecked(
1593 2,
1594 #[cfg(feature = "transparent-inputs")]
1595 true,
1596 ),
1597 );
1598 assert_ne!(total_balance, spendable_balance);
1599
1600 let to_extsk = T::sk(&[0xf5; 32]);
1601 let to: Address = T::sk_default_address(&to_extsk);
1602
1603 let fee_rule = StandardFeeRule::Zip317;
1604
1605 let send_max_memo = "Test Send Max memo".parse::<Memo>().unwrap();
1606
1607 let addy = to.to_zcash_address(st.network());
1608 assert_matches!(
1609 st.propose_send_max_transfer(
1610 account.id(),
1611 &fee_rule,
1612 addy,
1613 Some(MemoBytes::from(send_max_memo.clone())),
1614 MaxSpendMode::Everything,
1615 ConfirmationsPolicy::new_symmetrical_unchecked(
1616 2,
1617 #[cfg(feature = "transparent-inputs")]
1618 true
1619 )
1620 ),
1621 Err(data_api::error::Error::DataSource(_))
1622 );
1623}
1624
1625pub fn send_max_spendable_proposal_succeeds_when_unconfirmed_funds_present<
1637 T: ShieldedPoolTester,
1638>(
1639 dsf: impl DataStoreFactory,
1640 cache: impl TestCache,
1641) {
1642 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
1643 let h = st
1644 .add_notes_checking_balance([
1645 Some(Zatoshis::const_from_u64(60000)),
1646 None,
1647 None,
1648 Some(Zatoshis::const_from_u64(123456)),
1649 ])
1650 .block_height()
1651 .unwrap();
1652
1653 let account = st.test_account().cloned().unwrap();
1655 let total_balance = st.get_total_balance(account.id());
1656 let spendable_balance = st.get_spendable_balance(
1657 account.id(),
1658 ConfirmationsPolicy::new_symmetrical_unchecked(
1659 2,
1660 #[cfg(feature = "transparent-inputs")]
1661 true,
1662 ),
1663 );
1664 assert_ne!(total_balance, spendable_balance);
1665
1666 let to_extsk = T::sk(&[0xf5; 32]);
1667 let to: Address = T::sk_default_address(&to_extsk);
1668
1669 let fee_rule = StandardFeeRule::Zip317;
1670
1671 let send_max_memo = "Test Send Max memo".parse::<Memo>().unwrap();
1672
1673 let addy = to.to_zcash_address(st.network());
1674 let proposal = st
1675 .propose_send_max_transfer(
1676 account.id(),
1677 &fee_rule,
1678 addy,
1679 Some(MemoBytes::from(send_max_memo.clone())),
1680 MaxSpendMode::MaxSpendable,
1681 ConfirmationsPolicy::new_symmetrical_unchecked(
1682 2,
1683 #[cfg(feature = "transparent-inputs")]
1684 true,
1685 ),
1686 )
1687 .unwrap();
1688
1689 let sent_tx_id = st.create_proposed_expecting(&proposal, 1)[0];
1690
1691 let tx = st
1693 .wallet()
1694 .get_transaction(sent_tx_id)
1695 .unwrap()
1696 .expect("Created transaction was stored.");
1697 let ufvks = [(account.id(), account.usk().to_unified_full_viewing_key())]
1698 .into_iter()
1699 .collect();
1700 let d_tx = decrypt_transaction(st.network(), None, Some(h), &tx, &ufvks);
1701 assert_eq!(T::decrypted_pool_outputs_count(&d_tx), 1);
1702
1703 let mut found_send_max_memo = false;
1704 let mut found_tx_empty_memo = false;
1705 T::with_decrypted_pool_memos(&d_tx, |memo| {
1706 if Memo::try_from(memo).unwrap() == send_max_memo {
1707 found_send_max_memo = true
1708 }
1709 if Memo::try_from(memo).unwrap() == Memo::Empty {
1710 found_tx_empty_memo = true
1711 }
1712 });
1713 assert!(found_send_max_memo);
1714 assert!(!found_tx_empty_memo); let sent_note_ids = st
1718 .wallet()
1719 .get_sent_note_ids(&sent_tx_id, T::SHIELDED_PROTOCOL)
1720 .unwrap();
1721 assert_eq!(sent_note_ids.len(), 1);
1722
1723 let mut found_sent_empty_memo = false;
1725 let mut found_sent_max_memo = false;
1726 for sent_note_id in sent_note_ids {
1727 match st
1728 .wallet()
1729 .get_memo(sent_note_id)
1730 .expect("Memo retrieval should succeed")
1731 .as_ref()
1732 {
1733 Some(m) if m == &Memo::Empty => {
1734 found_sent_empty_memo = true;
1735 }
1736 Some(m) if m == &send_max_memo => {
1737 found_sent_max_memo = true;
1738 }
1739 Some(other) => panic!("Unexpected memo value: {other:?}"),
1740 None => panic!("Memo should not be stored as NULL"),
1741 }
1742 }
1743
1744 assert!(found_sent_max_memo);
1745 assert!(!found_sent_empty_memo);
1746
1747 assert_matches!(
1749 st.wallet()
1750 .get_memo(NoteId::new(sent_tx_id, T::SHIELDED_PROTOCOL, 12345)),
1751 Ok(None)
1752 );
1753
1754 let tx_history = st.wallet().get_tx_history().unwrap();
1755 assert_eq!(tx_history.len(), 2);
1756 {
1757 let tx_0 = &tx_history[0];
1758 assert_eq!(tx_0.total_spent(), Zatoshis::const_from_u64(0));
1759 assert_eq!(tx_0.total_received(), Zatoshis::const_from_u64(60000));
1760 }
1761
1762 {
1763 let tx_1 = &tx_history[1];
1764 assert_eq!(tx_1.total_spent(), Zatoshis::const_from_u64(60000));
1765 assert_eq!(tx_1.total_received(), Zatoshis::const_from_u64(0));
1766 }
1767
1768 let network = *st.network();
1769 assert_matches!(
1770 decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, None),
1771 Ok(_)
1772 );
1773}
1774
1775#[cfg(feature = "transparent-inputs")]
1778pub fn spend_everything_multi_step_single_note_proposed_transfer<T: ShieldedPoolTester, Dsf>(
1779 ds_factory: Dsf,
1780 cache: impl TestCache,
1781) where
1782 Dsf: DataStoreFactory,
1783 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
1784{
1785 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache)
1786 .map(|builder| builder.with_gap_limits(GapLimits::new(10, 5, 3)))
1787 .build::<T>();
1788
1789 let account = st.test_account().cloned().unwrap();
1790 let account_id = account.id();
1791
1792 let value = Zatoshis::const_from_u64(100000);
1793
1794 st.add_a_single_note_checking_balance(value);
1796 let initial_balance = value;
1797 assert_eq!(
1798 st.get_spendable_balance(account_id, ConfirmationsPolicy::MIN),
1799 initial_balance
1800 );
1801
1802 let expected_step0_fee = (zip317::MARGINAL_FEE * 3u64).unwrap();
1803 let expected_step1_fee = zip317::MINIMUM_FEE;
1804 let expected_ephemeral_spend = (value - expected_step0_fee - expected_step1_fee).unwrap();
1805 let expected_ephemeral_balance = (value - expected_step0_fee).unwrap();
1806 let expected_step0_change = (value - expected_step0_fee).unwrap();
1807
1808 let total_sent = (expected_step0_fee + expected_step1_fee + expected_ephemeral_spend).unwrap();
1809
1810 assert_eq!(total_sent, value);
1813
1814 let tex_addr = Address::Tex([0x4; 20]);
1816
1817 let fee_rule = StandardFeeRule::Zip317;
1820
1821 let addy = tex_addr.to_zcash_address(st.network());
1824 let proposal = st
1825 .propose_send_max_transfer(
1826 account_id,
1827 &fee_rule,
1828 addy,
1829 None,
1830 MaxSpendMode::Everything,
1831 ConfirmationsPolicy::MIN,
1832 )
1833 .unwrap();
1834
1835 let steps: Vec<_> = proposal.steps().iter().cloned().collect();
1836 assert_eq!(steps.len(), 2);
1837
1838 assert_eq!(steps[0].balance().fee_required(), expected_step0_fee);
1839 assert_eq!(steps[1].balance().fee_required(), expected_step1_fee);
1840 assert_eq!(
1841 steps[0].balance().proposed_change(),
1842 [
1843 ChangeValue::ephemeral_transparent(
1850 (total_sent - expected_step0_fee).expect("value is non-zero")
1851 ),
1852 ]
1853 );
1854 assert_eq!(steps[1].balance().proposed_change(), []);
1855
1856 let txids = st.create_proposed_expecting(&proposal, 2);
1857
1858 for txid in txids.iter() {
1860 let (h, _) = st.generate_next_block_including(*txid);
1861 st.scan_cached_blocks(h, 1);
1862 }
1863
1864 let confirmed_sent: Vec<Vec<_>> = txids
1866 .iter()
1867 .map(|sent_txid| st.wallet().get_sent_outputs(sent_txid).unwrap())
1868 .collect();
1869
1870 let tx_data_requests = st.wallet().transaction_data_requests().unwrap();
1873 assert!(tx_data_requests.contains(&TransactionDataRequest::GetStatus(*txids.last())));
1874
1875 assert!(expected_step0_change > expected_ephemeral_spend);
1876 assert_eq!(confirmed_sent.len(), 2);
1877 assert_eq!(confirmed_sent[0].len(), 1);
1878 assert_eq!(confirmed_sent[0][0].value, expected_step0_change);
1879 let OutputOfSentTx {
1880 value: ephemeral_v, ..
1881 } = confirmed_sent[0][0].clone();
1882 assert_eq!(ephemeral_v, expected_ephemeral_balance);
1883
1884 assert_eq!(confirmed_sent[1].len(), 1);
1885 assert_matches!(
1886 &confirmed_sent[1][0],
1887 OutputOfSentTx { value: sent_v, external_recipient: sent_to_addr, ephemeral_address: None }
1888 if sent_v == &expected_ephemeral_spend && sent_to_addr == &Some(tex_addr));
1889
1890 let tx_history = st.wallet().get_tx_history().unwrap();
1892
1893 let tx_0 = tx_history
1894 .iter()
1895 .find(|tx| tx.txid() == *txids.first())
1896 .unwrap();
1897 let tx_1 = tx_history
1898 .iter()
1899 .find(|tx| tx.txid() == *txids.last())
1900 .unwrap();
1901
1902 assert_eq!(tx_0.account_id(), &account_id);
1903 assert!(!tx_0.expired_unmined());
1904 assert_eq!(tx_0.has_change(), expected_step0_change.is_zero());
1905 assert!(!tx_0.is_shielding());
1906 assert_eq!(
1907 tx_0.account_value_delta(),
1908 -ZatBalance::from(expected_step0_fee),
1909 );
1910
1911 assert_eq!(tx_1.account_id(), &account_id);
1912 assert!(!tx_1.expired_unmined());
1913 assert!(!tx_1.has_change());
1914 assert!(!tx_0.is_shielding());
1915 assert_eq!(
1916 tx_1.account_value_delta(),
1917 -ZatBalance::from(expected_ephemeral_balance),
1918 );
1919
1920 let ending_balance = st.get_spendable_balance(account_id, ConfirmationsPolicy::MIN);
1921 assert_eq!(initial_balance - total_sent, ending_balance.into());
1922}
1923
1924#[cfg(feature = "transparent-inputs")]
1927pub fn spend_everything_multi_step_many_notes_proposed_transfer<T: ShieldedPoolTester, Dsf>(
1928 ds_factory: Dsf,
1929 cache: impl TestCache,
1930) where
1931 Dsf: DataStoreFactory,
1932 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
1933{
1934 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache)
1935 .map(|builder| builder.with_gap_limits(GapLimits::new(10, 5, 3)))
1936 .build::<T>();
1937
1938 let number_of_notes = 3u64;
1939 let note_value = Zatoshis::const_from_u64(100000);
1940 let value = (note_value * number_of_notes).unwrap();
1941
1942 for _ in 0..number_of_notes {
1944 st.add_a_single_note_checking_balance(note_value);
1945 }
1946
1947 let initial_balance = value;
1948 let account = st.test_account().cloned().unwrap();
1949 let account_id = account.id();
1950 assert_eq!(
1951 st.get_spendable_balance(account_id, ConfirmationsPolicy::MIN),
1952 initial_balance
1953 );
1954
1955 let expected_step0_fee = (zip317::MARGINAL_FEE * 4u64).unwrap();
1956 let expected_step1_fee = zip317::MINIMUM_FEE;
1957 let expected_ephemeral_spend = (value - expected_step0_fee - expected_step1_fee).unwrap();
1958 let expected_ephemeral_balance = (value - expected_step0_fee).unwrap();
1959 let expected_step0_change = (value - expected_step0_fee).unwrap();
1960
1961 let total_sent = (expected_step0_fee + expected_step1_fee + expected_ephemeral_spend).unwrap();
1962
1963 assert_eq!(total_sent, value);
1966
1967 let tex_addr = Address::Tex([0x4; 20]);
1969 let fee_rule = StandardFeeRule::Zip317;
1970
1971 let addy = tex_addr.to_zcash_address(st.network());
1974 let proposal = st
1975 .propose_send_max_transfer(
1976 account_id,
1977 &fee_rule,
1978 addy,
1979 None,
1980 MaxSpendMode::Everything,
1981 ConfirmationsPolicy::MIN,
1982 )
1983 .unwrap();
1984
1985 let steps: Vec<_> = proposal.steps().iter().cloned().collect();
1986 assert_eq!(steps.len(), 2);
1987
1988 assert_eq!(steps[0].balance().fee_required(), expected_step0_fee);
1989 assert_eq!(steps[1].balance().fee_required(), expected_step1_fee);
1990 assert_eq!(
1991 steps[0].balance().proposed_change(),
1992 [ChangeValue::ephemeral_transparent(
1993 (total_sent - expected_step0_fee).expect("value is non-zero")
1994 ),]
1995 );
1996 assert_eq!(steps[1].balance().proposed_change(), []);
1997
1998 let txids = st.create_proposed_expecting(&proposal, 2);
1999
2000 for txid in txids.iter() {
2002 let (h, _) = st.generate_next_block_including(*txid);
2003 st.scan_cached_blocks(h, 1);
2004 }
2005
2006 let confirmed_sent: Vec<Vec<_>> = txids
2008 .iter()
2009 .map(|sent_txid| st.wallet().get_sent_outputs(sent_txid).unwrap())
2010 .collect();
2011
2012 let tx_data_requests = st.wallet().transaction_data_requests().unwrap();
2015 assert!(tx_data_requests.contains(&TransactionDataRequest::GetStatus(*txids.last())));
2016
2017 assert!(expected_step0_change > expected_ephemeral_spend);
2018 assert_eq!(confirmed_sent.len(), 2);
2019 assert_eq!(confirmed_sent[0].len(), 1);
2020 assert_eq!(confirmed_sent[0][0].value, expected_step0_change);
2021 let OutputOfSentTx {
2022 value: ephemeral_v, ..
2023 } = confirmed_sent[0][0].clone();
2024 assert_eq!(ephemeral_v, expected_ephemeral_balance);
2025
2026 assert_eq!(confirmed_sent[1].len(), 1);
2027 assert_matches!(
2028 &confirmed_sent[1][0],
2029 OutputOfSentTx { value: sent_v, external_recipient: sent_to_addr, ephemeral_address: None }
2030 if sent_v == &expected_ephemeral_spend && sent_to_addr == &Some(tex_addr));
2031
2032 let tx_history = st.wallet().get_tx_history().unwrap();
2034
2035 let tx_0 = tx_history
2036 .iter()
2037 .find(|tx| tx.txid() == *txids.first())
2038 .unwrap();
2039 let tx_1 = tx_history
2040 .iter()
2041 .find(|tx| tx.txid() == *txids.last())
2042 .unwrap();
2043
2044 assert_eq!(tx_0.account_id(), &account_id);
2045 assert!(!tx_0.expired_unmined());
2046 assert_eq!(tx_0.has_change(), expected_step0_change.is_zero());
2047 assert!(!tx_0.is_shielding());
2048 assert_eq!(
2049 tx_0.account_value_delta(),
2050 -ZatBalance::from(expected_step0_fee),
2051 );
2052
2053 assert_eq!(tx_1.account_id(), &account_id);
2054 assert!(!tx_1.expired_unmined());
2055 assert!(!tx_1.has_change());
2056 assert!(!tx_0.is_shielding());
2057 assert_eq!(
2058 tx_1.account_value_delta(),
2059 -ZatBalance::from(expected_ephemeral_balance),
2060 );
2061
2062 let ending_balance = st.get_spendable_balance(account_id, ConfirmationsPolicy::MIN);
2063 assert_eq!(initial_balance - total_sent, ending_balance.into());
2064}
2065
2066#[cfg(feature = "transparent-inputs")]
2070pub fn spend_everything_multi_step_with_marginal_notes_proposed_transfer<
2071 T: ShieldedPoolTester,
2072 Dsf,
2073>(
2074 ds_factory: Dsf,
2075 cache: impl TestCache,
2076) where
2077 Dsf: DataStoreFactory,
2078 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
2079{
2080 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache)
2081 .map(|builder| builder.with_gap_limits(GapLimits::new(10, 5, 3)))
2082 .build::<T>();
2083
2084 let number_of_notes = 10u64;
2085 let note_value = Zatoshis::const_from_u64(100000);
2086 let non_marginal_notes_value =
2087 (note_value * number_of_notes).expect("sum of notes should not fail.");
2088
2089 for _ in 0..number_of_notes {
2090 st.add_a_single_note_checking_balance(note_value);
2091 st.add_a_single_note_checking_balance(zip317::MARGINAL_FEE);
2092 }
2093
2094 let account = st.test_account().cloned().unwrap();
2095 let account_id = account.id();
2096 assert_eq!(
2097 st.get_spendable_balance(account_id, ConfirmationsPolicy::MIN),
2098 non_marginal_notes_value
2099 );
2100
2101 let expected_step0_fee = (zip317::MARGINAL_FEE * (number_of_notes + 1)).unwrap();
2102 let expected_step1_fee = zip317::MINIMUM_FEE;
2103 let expected_ephemeral_spend =
2104 (non_marginal_notes_value - expected_step0_fee - expected_step1_fee).unwrap();
2105 let expected_ephemeral_balance = (non_marginal_notes_value - expected_step0_fee).unwrap();
2106 let expected_step0_change = (non_marginal_notes_value - expected_step0_fee).unwrap();
2107
2108 let total_sent = (expected_step0_fee + expected_step1_fee + expected_ephemeral_spend).unwrap();
2109
2110 assert_eq!(total_sent, non_marginal_notes_value);
2113
2114 let tex_addr = Address::Tex([0x4; 20]);
2116
2117 let fee_rule = StandardFeeRule::Zip317;
2118
2119 let addy = tex_addr.to_zcash_address(st.network());
2122 let proposal = st
2123 .propose_send_max_transfer(
2124 account_id,
2125 &fee_rule,
2126 addy,
2127 None,
2128 MaxSpendMode::Everything,
2129 ConfirmationsPolicy::MIN,
2130 )
2131 .unwrap();
2132
2133 let steps: Vec<_> = proposal.steps().iter().cloned().collect();
2134 assert_eq!(steps.len(), 2);
2135
2136 assert_eq!(
2137 steps[0].shielded_inputs().unwrap().notes().len() as u64,
2138 number_of_notes
2139 );
2140 assert_eq!(steps[0].balance().fee_required(), expected_step0_fee);
2141 assert_eq!(steps[1].balance().fee_required(), expected_step1_fee);
2142 assert_eq!(
2143 steps[0].balance().proposed_change(),
2144 [ChangeValue::ephemeral_transparent(
2145 (total_sent - expected_step0_fee).expect("value is non-zero")
2146 ),]
2147 );
2148 assert_eq!(steps[1].balance().proposed_change(), []);
2149
2150 let txids = st.create_proposed_expecting(&proposal, 2);
2151
2152 for txid in txids.iter() {
2154 let (h, _) = st.generate_next_block_including(*txid);
2155 st.scan_cached_blocks(h, 1);
2156 }
2157
2158 let confirmed_sent: Vec<Vec<_>> = txids
2160 .iter()
2161 .map(|sent_txid| st.wallet().get_sent_outputs(sent_txid).unwrap())
2162 .collect();
2163
2164 let tx_data_requests = st.wallet().transaction_data_requests().unwrap();
2167 assert!(tx_data_requests.contains(&TransactionDataRequest::GetStatus(*txids.last())));
2168
2169 assert!(expected_step0_change > expected_ephemeral_spend);
2170 assert_eq!(confirmed_sent.len(), 2);
2171 assert_eq!(confirmed_sent[0].len(), 1);
2172 assert_eq!(confirmed_sent[0][0].value, expected_step0_change);
2173 let OutputOfSentTx {
2174 value: ephemeral_v, ..
2175 } = confirmed_sent[0][0].clone();
2176 assert_eq!(ephemeral_v, expected_ephemeral_balance);
2177
2178 assert_eq!(confirmed_sent[1].len(), 1);
2179 assert_matches!(
2180 &confirmed_sent[1][0],
2181 OutputOfSentTx { value: sent_v, external_recipient: sent_to_addr, ephemeral_address: None }
2182 if sent_v == &expected_ephemeral_spend && sent_to_addr == &Some(tex_addr));
2183
2184 let tx_history = st.wallet().get_tx_history().unwrap();
2186
2187 let tx_0 = tx_history
2188 .iter()
2189 .find(|tx| tx.txid() == *txids.first())
2190 .unwrap();
2191 let tx_1 = tx_history
2192 .iter()
2193 .find(|tx| tx.txid() == *txids.last())
2194 .unwrap();
2195
2196 assert_eq!(tx_0.account_id(), &account_id);
2197 assert!(!tx_0.expired_unmined());
2198 assert_eq!(tx_0.has_change(), expected_step0_change.is_zero());
2199 assert!(!tx_0.is_shielding());
2200 assert_eq!(
2201 tx_0.account_value_delta(),
2202 -ZatBalance::from(expected_step0_fee),
2203 );
2204
2205 assert_eq!(tx_1.account_id(), &account_id);
2206 assert!(!tx_1.expired_unmined());
2207 assert!(!tx_1.has_change());
2208 assert!(!tx_0.is_shielding());
2209 assert_eq!(
2210 tx_1.account_value_delta(),
2211 -ZatBalance::from(expected_ephemeral_balance),
2212 );
2213
2214 let ending_balance = st.get_spendable_balance(account_id, ConfirmationsPolicy::MIN);
2215 assert_eq!(ending_balance, Zatoshis::ZERO); }
2217
2218#[cfg(feature = "transparent-inputs")]
2227pub fn insufficient_funds_counts_transparent_inputs<T: ShieldedPoolTester>(
2228 dsf: impl DataStoreFactory,
2229 cache: impl TestCache,
2230) {
2231 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
2232
2233 let note_value = Zatoshis::const_from_u64(50_000);
2234 let (h, _, _) = st.add_a_single_note_checking_balance(note_value);
2235
2236 let account = st.test_account().cloned().unwrap();
2237 let uaddr = st
2238 .wallet()
2239 .get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
2240 .unwrap()
2241 .unwrap();
2242 let taddr = uaddr.transparent().unwrap();
2243
2244 let utxo_value = Zatoshis::const_from_u64(30_000);
2245 let utxo = WalletTransparentOutput::from_parts(
2246 OutPoint::fake(),
2247 TxOut::new(utxo_value, taddr.script().into()),
2248 Some(h),
2249 Some(account.id()),
2250 Some(TransparentKeyScope::EXTERNAL),
2251 None,
2252 )
2253 .unwrap();
2254 st.wallet_mut()
2255 .put_received_transparent_utxo(&utxo)
2256 .unwrap();
2257
2258 let to: Address = T::sk_default_address(&T::sk(&[0xf5; 32]));
2259 let amount = Zatoshis::const_from_u64(100_000);
2260 let request = TransactionRequest::new(vec![Payment::without_memo(
2261 to.to_zcash_address(st.network()),
2262 amount,
2263 )])
2264 .unwrap();
2265
2266 let input_selector = GreedyInputSelector::new();
2267 let change_strategy =
2268 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
2269
2270 let expected_required = (amount + (MARGINAL_FEE * 3u64).unwrap()).unwrap();
2274 let expected_available = (note_value + utxo_value).unwrap();
2275
2276 assert_matches!(
2277 st.propose_transfer_with_policy(
2278 account.id(),
2279 &input_selector,
2280 &change_strategy,
2281 request,
2282 ConfirmationsPolicy::MIN,
2283 &SpendPolicy::default().with_transparent(TransparentSpendPolicy::any_account_addr()),
2284 ),
2285 Err(Error::InsufficientFunds { available, required })
2286 if available == expected_available && required == expected_required
2287 );
2288}
2289
2290pub fn send_with_multiple_change_outputs<T: ShieldedPoolTester>(
2291 dsf: impl DataStoreFactory,
2292 cache: impl TestCache,
2293) {
2294 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
2295
2296 let value = Zatoshis::const_from_u64(650_0000);
2298 let (h, _, _) = st.add_a_single_note_checking_balance(value);
2299
2300 let to_extsk = T::sk(&[0xf5; 32]);
2301 let to: Address = T::sk_default_address(&to_extsk);
2302 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
2303 to.to_zcash_address(st.network()),
2304 Zatoshis::const_from_u64(100_0000),
2305 )])
2306 .unwrap();
2307
2308 let input_selector = GreedyInputSelector::new();
2309 let change_memo = "Test change memo".parse::<Memo>().unwrap();
2310 let change_strategy = fees::zip317::MultiOutputChangeStrategy::new(
2311 Zip317FeeRule::standard(),
2312 Some(change_memo.clone().into()),
2313 T::SHIELDED_PROTOCOL,
2314 DustOutputPolicy::default(),
2315 SplitPolicy::with_min_output_value(
2316 NonZeroUsize::new(2).unwrap(),
2317 Zatoshis::const_from_u64(100_0000),
2318 ),
2319 );
2320
2321 let account = st.test_account().cloned().unwrap();
2322 let proposal = st
2323 .propose_transfer(
2324 account.id(),
2325 &input_selector,
2326 &change_strategy,
2327 request.clone(),
2328 ConfirmationsPolicy::MIN,
2329 )
2330 .unwrap();
2331
2332 let step = &proposal.steps().head;
2333 assert_eq!(step.balance().proposed_change().len(), 2);
2334
2335 let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible, _>(
2336 account.usk(),
2337 OvkPolicy::Sender,
2338 &proposal,
2339 );
2340 assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1);
2341
2342 let sent_tx_id = create_proposed_result.unwrap()[0];
2343
2344 let tx = st
2346 .wallet()
2347 .get_transaction(sent_tx_id)
2348 .unwrap()
2349 .expect("Created transaction was stored.");
2350 let ufvks = [(account.id(), account.usk().to_unified_full_viewing_key())]
2351 .into_iter()
2352 .collect();
2353 let d_tx = decrypt_transaction(st.network(), None, Some(h), &tx, &ufvks);
2354 assert_eq!(T::decrypted_pool_outputs_count(&d_tx), 3);
2355
2356 let mut found_tx_change_memo = false;
2357 let mut found_tx_empty_memo = false;
2358 T::with_decrypted_pool_memos(&d_tx, |memo| {
2359 if Memo::try_from(memo).unwrap() == change_memo {
2360 found_tx_change_memo = true
2361 }
2362 if Memo::try_from(memo).unwrap() == Memo::Empty {
2363 found_tx_empty_memo = true
2364 }
2365 });
2366 assert!(found_tx_change_memo);
2367 assert!(found_tx_empty_memo);
2368
2369 let sent_note_ids = st
2371 .wallet()
2372 .get_sent_note_ids(&sent_tx_id, T::SHIELDED_PROTOCOL)
2373 .unwrap();
2374 assert_eq!(sent_note_ids.len(), 3);
2375
2376 let mut change_memo_count = 0;
2379 let mut found_sent_empty_memo = false;
2380 for sent_note_id in sent_note_ids {
2381 match st
2382 .wallet()
2383 .get_memo(sent_note_id)
2384 .expect("Note id is valid")
2385 .as_ref()
2386 {
2387 Some(m) if m == &change_memo => {
2388 change_memo_count += 1;
2389 }
2390 Some(m) if m == &Memo::Empty => {
2391 found_sent_empty_memo = true;
2392 }
2393 Some(other) => panic!("Unexpected memo value: {other:?}"),
2394 None => panic!("Memo should not be stored as NULL"),
2395 }
2396 }
2397 assert_eq!(change_memo_count, 2);
2398 assert!(found_sent_empty_memo);
2399
2400 let tx_history = st.wallet().get_tx_history().unwrap();
2401 assert_eq!(tx_history.len(), 2);
2402 {
2403 let tx_0 = &tx_history[0];
2404 assert_eq!(tx_0.total_spent(), Zatoshis::const_from_u64(0));
2405 assert_eq!(tx_0.total_received(), Zatoshis::const_from_u64(650_0000));
2406 }
2407
2408 {
2409 let tx_1 = &tx_history[1];
2410 assert_eq!(tx_1.total_spent(), Zatoshis::const_from_u64(650_0000));
2411 assert_eq!(tx_1.total_received(), Zatoshis::const_from_u64(548_5000));
2412 assert_eq!(tx_1.fee_paid(), Some(Zatoshis::const_from_u64(15000)));
2413 }
2414
2415 let network = *st.network();
2416 assert_matches!(
2417 decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, None),
2418 Ok(_)
2419 );
2420
2421 let (h, _) = st.generate_next_block_including(sent_tx_id);
2422 st.scan_cached_blocks(h, 1);
2423
2424 let change_strategy = fees::zip317::MultiOutputChangeStrategy::new(
2427 Zip317FeeRule::standard(),
2428 Some(change_memo.into()),
2429 T::SHIELDED_PROTOCOL,
2430 DustOutputPolicy::default(),
2431 SplitPolicy::with_min_output_value(
2432 NonZeroUsize::new(8).unwrap(),
2433 Zatoshis::const_from_u64(10_0000),
2434 ),
2435 );
2436
2437 let proposal = st
2438 .propose_transfer(
2439 account.id(),
2440 &input_selector,
2441 &change_strategy,
2442 request,
2443 ConfirmationsPolicy::MIN,
2444 )
2445 .unwrap();
2446
2447 let step = &proposal.steps().head;
2448 assert_eq!(step.balance().proposed_change().len(), 7);
2449}
2450
2451#[cfg(feature = "transparent-inputs")]
2452pub fn send_multi_step_proposed_transfer<T: ShieldedPoolTester, Dsf>(
2453 ds_factory: Dsf,
2454 cache: impl TestCache,
2455 is_reached_gap_limit: impl Fn(&<Dsf::DataStore as WalletRead>::Error, Dsf::AccountId, u32) -> bool,
2456) where
2457 Dsf: DataStoreFactory,
2458{
2459 let gap_limits = GapLimits::new(10, 5, 3);
2460 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache)
2461 .map(|builder| builder.with_gap_limits(gap_limits))
2462 .build::<T>();
2463
2464 let account = st.test_account().cloned().unwrap();
2465 let account_id = account.id();
2466 let dfvk = T::test_account_fvk(&st);
2467 let tex_addr = Address::Tex([0x4; 20]);
2468
2469 let add_funds = |st: &mut TestState<_, Dsf::DataStore, _>, value| {
2470 let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
2471 st.scan_cached_blocks(h, 1);
2472
2473 assert_eq!(
2474 st.wallet()
2475 .block_max_scanned()
2476 .unwrap()
2477 .unwrap()
2478 .block_height(),
2479 h
2480 );
2481 h
2482 };
2483
2484 let value = Zatoshis::const_from_u64(100000);
2485 let transfer_amount = Zatoshis::const_from_u64(50000);
2486
2487 let run_test = |st: &mut TestState<_, Dsf::DataStore, _>, expected_index, prior_balance| {
2488 add_funds(st, value);
2490 let initial_balance: Option<Zatoshis> = prior_balance + value;
2491 assert_eq!(
2492 st.get_spendable_balance(account_id, ConfirmationsPolicy::MIN),
2493 initial_balance.unwrap()
2494 );
2495
2496 let expected_step0_fee = (zip317::MARGINAL_FEE * 3u64).unwrap();
2497 let expected_step1_fee = zip317::MINIMUM_FEE;
2498 let expected_ephemeral = (transfer_amount + expected_step1_fee).unwrap();
2499 let expected_step0_change =
2500 (initial_balance - expected_ephemeral - expected_step0_fee).expect("sufficient funds");
2501 assert!(expected_step0_change.is_positive());
2502
2503 let total_sent = (expected_step0_fee + expected_step1_fee + transfer_amount).unwrap();
2504
2505 let change_memo = Some(Memo::from_str("change").expect("valid memo").encode());
2508
2509 let proposal = st
2512 .propose_standard_transfer::<Infallible>(
2513 account_id,
2514 StandardFeeRule::Zip317,
2515 ConfirmationsPolicy::MIN,
2516 &tex_addr,
2517 transfer_amount,
2518 None,
2519 change_memo.clone(),
2520 T::SHIELDED_PROTOCOL,
2521 )
2522 .unwrap();
2523
2524 let steps: Vec<_> = proposal.steps().iter().cloned().collect();
2525 assert_eq!(steps.len(), 2);
2526
2527 assert_eq!(steps[0].balance().fee_required(), expected_step0_fee);
2528 assert_eq!(steps[1].balance().fee_required(), expected_step1_fee);
2529 assert_eq!(
2530 steps[0].balance().proposed_change(),
2531 [
2532 ChangeValue::shielded(T::SHIELDED_PROTOCOL, expected_step0_change, change_memo),
2533 ChangeValue::ephemeral_transparent(expected_ephemeral),
2534 ]
2535 );
2536 assert_eq!(steps[1].balance().proposed_change(), []);
2537
2538 let exposed_at_tip = st
2540 .wallet()
2541 .get_ephemeral_transparent_receivers(account.account().id(), 1, false)
2542 .unwrap();
2543 assert_eq!(exposed_at_tip.len(), 0);
2544
2545 let create_proposed_result = st
2546 .create_proposed_transactions::<Infallible, _, Infallible, _>(
2547 account.usk(),
2548 OvkPolicy::Sender,
2549 &proposal,
2550 );
2551 assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 2);
2552 let txids = create_proposed_result.unwrap();
2553
2554 let exposed_at_tip = st
2556 .wallet()
2557 .get_ephemeral_transparent_receivers(account.account().id(), 1, false)
2558 .unwrap();
2559 assert_eq!(exposed_at_tip.len(), 1);
2560 let cur_height = st.wallet().chain_height().unwrap().unwrap();
2561 assert_matches!(
2562 exposed_at_tip.values().next().map(|m0| m0.exposure()),
2563 Some(Exposure::Exposed { at_height, .. }) if at_height == cur_height
2564 );
2565
2566 let exposed_at_tip = st
2568 .wallet()
2569 .get_ephemeral_transparent_receivers(account.account().id(), 1, true)
2570 .unwrap();
2571 assert!(exposed_at_tip.is_empty());
2572
2573 for txid in txids.iter() {
2575 let (h, _) = st.generate_next_block_including(*txid);
2576 st.scan_cached_blocks(h, 1);
2577 }
2578
2579 let confirmed_sent: Vec<Vec<_>> = txids
2581 .iter()
2582 .map(|sent_txid| st.wallet().get_sent_outputs(sent_txid).unwrap())
2583 .collect();
2584
2585 let tx_data_requests = st.wallet().transaction_data_requests().unwrap();
2588 assert!(tx_data_requests.contains(&TransactionDataRequest::GetStatus(*txids.last())));
2589
2590 assert!(expected_step0_change < expected_ephemeral);
2591 assert_eq!(confirmed_sent.len(), 2);
2592 assert_eq!(confirmed_sent[0].len(), 2);
2593 assert_eq!(confirmed_sent[0][0].value, expected_step0_change);
2594 let OutputOfSentTx {
2595 value: ephemeral_v,
2596 external_recipient: to_addr,
2597 ephemeral_address,
2598 } = confirmed_sent[0][1].clone();
2599 assert_eq!(ephemeral_v, expected_ephemeral);
2600 assert!(to_addr.is_some());
2601 assert_eq!(
2602 ephemeral_address,
2603 to_addr.map(|addr| (
2604 addr,
2605 NonHardenedChildIndex::const_from_index(expected_index)
2606 )),
2607 );
2608
2609 assert_eq!(confirmed_sent[1].len(), 1);
2610 assert_matches!(
2611 &confirmed_sent[1][0],
2612 OutputOfSentTx { value: sent_v, external_recipient: sent_to_addr, ephemeral_address: None }
2613 if sent_v == &transfer_amount && sent_to_addr.as_ref() == Some(&tex_addr));
2614
2615 let tx_history = st.wallet().get_tx_history().unwrap();
2617
2618 let tx_0 = tx_history
2619 .iter()
2620 .find(|tx| tx.txid() == *txids.first())
2621 .unwrap();
2622 let tx_1 = tx_history
2623 .iter()
2624 .find(|tx| tx.txid() == *txids.last())
2625 .unwrap();
2626
2627 assert_eq!(tx_0.account_id(), &account_id);
2628 assert!(!tx_0.expired_unmined());
2629 assert_eq!(tx_0.has_change(), expected_step0_change.is_positive());
2630 assert!(!tx_0.is_shielding());
2631 assert_eq!(
2632 tx_0.account_value_delta(),
2633 -ZatBalance::from(expected_step0_fee),
2634 );
2635
2636 assert_eq!(tx_1.account_id(), &account_id);
2637 assert!(!tx_1.expired_unmined());
2638 assert!(!tx_1.has_change());
2639 assert!(!tx_0.is_shielding());
2640 assert_eq!(
2641 tx_1.account_value_delta(),
2642 -ZatBalance::from(expected_ephemeral),
2643 );
2644
2645 let ending_balance = st.get_spendable_balance(account_id, ConfirmationsPolicy::MIN);
2646 assert_eq!(initial_balance - total_sent, ending_balance.into());
2647
2648 (ephemeral_address.unwrap().0, txids, ending_balance)
2649 };
2650
2651 let (ephemeral0, _, bal_0) = run_test(&mut st, 0, Zatoshis::ZERO);
2653 let (ephemeral1, _, _) = run_test(&mut st, 1, bal_0);
2654 assert_ne!(ephemeral0, ephemeral1);
2655
2656 add_funds(&mut st, value);
2657
2658 assert_matches!(
2659 ephemeral0,
2660 Address::Transparent(TransparentAddress::PublicKeyHash(_))
2661 );
2662
2663 let known_addrs = st
2666 .wallet()
2667 .get_known_ephemeral_addresses(account_id, None)
2668 .unwrap();
2669 assert_eq!(
2670 known_addrs.len(),
2671 usize::try_from(gap_limits.ephemeral() + 2).unwrap()
2672 );
2673
2674 let known_set: HashSet<_> = known_addrs.iter().map(|(addr, _)| addr).collect();
2676 assert_eq!(known_set.len(), known_addrs.len());
2677 for (i, (_, meta)) in known_addrs.iter().enumerate() {
2679 assert_eq!(
2680 meta.source(),
2681 &TransparentAddressSource::Derived {
2682 scope: TransparentKeyScope::EPHEMERAL,
2683 address_index: NonHardenedChildIndex::from_index(i.try_into().unwrap()).unwrap(),
2684 }
2685 );
2686 }
2687
2688 let (colliding_addr, _) = &known_addrs[usize::try_from(gap_limits.ephemeral() - 1).unwrap()];
2689 let utxo_value = (value - zip317::MINIMUM_FEE).unwrap();
2690 let to = Address::from(*colliding_addr);
2691 let proposal = st.propose_transfer_to(&to, utxo_value);
2692
2693 let txids = st
2696 .create_proposed_transactions::<Infallible, _, Infallible, _>(
2697 account.usk(),
2698 OvkPolicy::Sender,
2699 &proposal,
2700 )
2701 .unwrap();
2702
2703 let (h, _) = st.generate_next_block_including(txids.head);
2706 st.scan_cached_blocks(h, 1);
2707 st.wallet_mut()
2708 .set_transaction_status(txids.head, TransactionStatus::Mined(h))
2709 .unwrap();
2710
2711 let new_known_addrs = st
2714 .wallet()
2715 .get_known_ephemeral_addresses(account_id, None)
2716 .unwrap();
2717 assert_eq!(
2718 new_known_addrs.len(),
2719 usize::try_from(gap_limits.ephemeral() * 2).unwrap()
2720 );
2721
2722 assert!(
2725 new_known_addrs
2726 .iter()
2727 .map(|a| a.0)
2728 .zip(known_addrs.iter().map(|a| a.0))
2729 .all(|(a, b)| a == b),
2730 "new_known_addrs must have known_addrs as its prefix"
2731 );
2732
2733 let reservation_should_succeed = |st: &mut TestState<_, Dsf::DataStore, _>, n: u32| {
2734 let reserved = st
2735 .wallet_mut()
2736 .reserve_next_n_ephemeral_addresses(account_id, n.try_into().unwrap())
2737 .unwrap();
2738 assert_eq!(reserved.len(), usize::try_from(n).unwrap());
2739 reserved
2740 };
2741 let reservation_should_fail =
2742 |st: &mut TestState<_, Dsf::DataStore, _>, n: u32, expected_bad_index| {
2743 assert_matches!(st
2744 .wallet_mut()
2745 .reserve_next_n_ephemeral_addresses(account_id, n.try_into().unwrap()),
2746 Err(e) if is_reached_gap_limit(&e, account_id, expected_bad_index));
2747 };
2748
2749 assert_matches!(
2750 known_addrs[usize::try_from(gap_limits.ephemeral()).unwrap()]
2751 .1
2752 .exposure(),
2753 Exposure::Unknown
2754 );
2755
2756 let next_reserved = reservation_should_succeed(&mut st, 1);
2757
2758 let gap_position = 0;
2760 let expected = &known_addrs[usize::try_from(gap_limits.ephemeral()).unwrap()];
2761 let actual = &next_reserved[usize::try_from(gap_position).unwrap()];
2762 assert_eq!(actual.0, expected.0);
2763 assert_eq!(actual.1.source(), expected.1.source());
2764 assert_eq!(expected.1.exposure(), Exposure::Unknown);
2765 assert_eq!(
2766 actual.1.exposure(),
2767 Exposure::Exposed {
2768 at_height: st.latest_block_height.unwrap(),
2769 gap_metadata: crate::wallet::GapMetadata::InGap {
2770 gap_position,
2771 gap_limit: gap_limits.ephemeral(),
2772 }
2773 }
2774 );
2775
2776 reservation_should_fail(&mut st, gap_limits.ephemeral(), gap_limits.ephemeral() * 2);
2781 reservation_should_succeed(&mut st, gap_limits.ephemeral() - 1);
2782 reservation_should_fail(&mut st, 1, gap_limits.ephemeral() * 2);
2784}
2785
2786pub fn spend_all_funds_single_step_proposed_transfer<T: ShieldedPoolTester>(
2797 dsf: impl DataStoreFactory,
2798 cache: impl TestCache,
2799) {
2800 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
2801
2802 let value = Zatoshis::const_from_u64(60000);
2804 let (h, _, _) = st.add_a_single_note_checking_balance(value);
2805
2806 let spend_amount = Zatoshis::const_from_u64(50000);
2807 let to_extsk = T::sk(&[0xf5; 32]);
2808 let to: Address = T::sk_default_address(&to_extsk);
2809 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
2810 to.to_zcash_address(st.network()),
2811 spend_amount,
2812 )])
2813 .unwrap();
2814
2815 let fee_rule = StandardFeeRule::Zip317;
2816
2817 let change_memo = "Test change memo".parse::<Memo>().unwrap();
2818 let change_strategy = standard::SingleOutputChangeStrategy::new(
2819 fee_rule,
2820 Some(change_memo.clone().into()),
2821 T::SHIELDED_PROTOCOL,
2822 DustOutputPolicy::default(),
2823 );
2824 let input_selector = GreedyInputSelector::new();
2825
2826 let account = st.test_account().cloned().unwrap();
2827 let proposal = st
2828 .propose_transfer(
2829 account.id(),
2830 &input_selector,
2831 &change_strategy,
2832 request,
2833 ConfirmationsPolicy::MIN,
2834 )
2835 .unwrap();
2836
2837 let sent_tx_id = st.create_proposed_expecting(&proposal, 1)[0];
2838
2839 let tx = st
2841 .wallet()
2842 .get_transaction(sent_tx_id)
2843 .unwrap()
2844 .expect("Created transaction was stored.");
2845 let ufvks = [(account.id(), account.usk().to_unified_full_viewing_key())]
2846 .into_iter()
2847 .collect();
2848 let d_tx = decrypt_transaction(st.network(), None, Some(h), &tx, &ufvks);
2849 assert_eq!(T::decrypted_pool_outputs_count(&d_tx), 2);
2850
2851 let mut found_tx_change_memo = false;
2852 let mut found_tx_empty_memo = false;
2853 T::with_decrypted_pool_memos(&d_tx, |memo| {
2854 if Memo::try_from(memo).unwrap() == change_memo {
2855 found_tx_change_memo = true
2856 }
2857 if Memo::try_from(memo).unwrap() == Memo::Empty {
2858 found_tx_empty_memo = true
2859 }
2860 });
2861 assert!(found_tx_change_memo);
2862 assert!(found_tx_empty_memo);
2863
2864 let sent_note_ids = st
2866 .wallet()
2867 .get_sent_note_ids(&sent_tx_id, T::SHIELDED_PROTOCOL)
2868 .unwrap();
2869 assert_eq!(sent_note_ids.len(), 2);
2870
2871 let mut found_sent_change_memo = false;
2874 let mut found_sent_empty_memo = false;
2875 for sent_note_id in sent_note_ids {
2876 match st
2877 .wallet()
2878 .get_memo(sent_note_id)
2879 .expect("Note id is valid")
2880 .as_ref()
2881 {
2882 Some(m) if m == &change_memo => {
2883 found_sent_change_memo = true;
2884 }
2885 Some(m) if m == &Memo::Empty => {
2886 found_sent_empty_memo = true;
2887 }
2888 Some(other) => panic!("Unexpected memo value: {other:?}"),
2889 None => panic!("Memo should not be stored as NULL"),
2890 }
2891 }
2892 assert!(found_sent_change_memo);
2893 assert!(found_sent_empty_memo);
2894
2895 assert_matches!(
2897 st.wallet()
2898 .get_memo(NoteId::new(sent_tx_id, T::SHIELDED_PROTOCOL, 12345)),
2899 Ok(None)
2900 );
2901
2902 let tx_history = st.wallet().get_tx_history().unwrap();
2903 assert_eq!(tx_history.len(), 2);
2904 {
2905 let tx_0 = &tx_history[0];
2906 assert_eq!(tx_0.total_spent(), Zatoshis::const_from_u64(0));
2907 assert_eq!(tx_0.total_received(), Zatoshis::const_from_u64(60000));
2908 }
2909
2910 {
2911 let tx_1 = &tx_history[1];
2912 assert_eq!(tx_1.total_spent(), Zatoshis::const_from_u64(60000));
2913 assert_eq!(tx_1.total_received(), Zatoshis::ZERO);
2914 }
2915
2916 let network = *st.network();
2917 assert_matches!(
2918 decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, None),
2919 Ok(_)
2920 );
2921}
2922#[cfg(feature = "transparent-inputs")]
2936pub fn spend_all_funds_multi_step_proposed_transfer<T: ShieldedPoolTester, Dsf>(
2937 ds_factory: Dsf,
2938 cache: impl TestCache,
2939) where
2940 Dsf: DataStoreFactory,
2941 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
2942{
2943 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache)
2944 .map(|builder| builder.with_gap_limits(GapLimits::new(10, 5, 3)))
2945 .build::<T>();
2946
2947 let account = st.test_account().cloned().unwrap();
2948 let account_id = account.id();
2949 let dfvk = T::test_account_fvk(&st);
2950
2951 let value = Zatoshis::const_from_u64(100000);
2952 let transfer_amount = Zatoshis::const_from_u64(75000);
2953
2954 let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
2956 st.scan_cached_blocks(h, 1);
2957
2958 let initial_balance = value;
2959 assert_eq!(
2960 st.get_spendable_balance(account_id, ConfirmationsPolicy::MIN),
2961 initial_balance
2962 );
2963
2964 let expected_step0_fee = (zip317::MARGINAL_FEE * 3u64).unwrap();
2965 let expected_step1_fee = zip317::MINIMUM_FEE;
2966 let expected_ephemeral = (transfer_amount + expected_step1_fee).unwrap();
2967 let expected_step0_change =
2968 (initial_balance - expected_ephemeral - expected_step0_fee).expect("sufficient funds");
2969 assert!(expected_step0_change.is_zero());
2970
2971 let total_sent = (expected_step0_fee + expected_step1_fee + transfer_amount).unwrap();
2972
2973 let tex_addr = Address::Tex([0x4; 20]);
2975
2976 let change_memo: Option<MemoBytes> = None;
2977 let proposal = st.propose_transfer_to(&tex_addr, transfer_amount);
2980
2981 let steps: Vec<_> = proposal.steps().iter().cloned().collect();
2982 assert_eq!(steps.len(), 2);
2983
2984 assert_eq!(steps[0].balance().fee_required(), expected_step0_fee);
2985 assert_eq!(steps[1].balance().fee_required(), expected_step1_fee);
2986 assert_eq!(
2987 steps[0].balance().proposed_change(),
2988 [
2989 ChangeValue::shielded(T::SHIELDED_PROTOCOL, expected_step0_change, change_memo),
2990 ChangeValue::ephemeral_transparent(expected_ephemeral),
2991 ]
2992 );
2993 assert_eq!(steps[1].balance().proposed_change(), []);
2994
2995 let txids = st.create_proposed_expecting(&proposal, 2);
2996
2997 for txid in txids.iter() {
2999 let (h, _) = st.generate_next_block_including(*txid);
3000 st.scan_cached_blocks(h, 1);
3001 }
3002
3003 let confirmed_sent: Vec<Vec<_>> = txids
3005 .iter()
3006 .map(|sent_txid| st.wallet().get_sent_outputs(sent_txid).unwrap())
3007 .collect();
3008
3009 let tx_data_requests = st.wallet().transaction_data_requests().unwrap();
3012 assert!(tx_data_requests.contains(&TransactionDataRequest::GetStatus(*txids.last())));
3013
3014 assert!(expected_step0_change < expected_ephemeral);
3015 assert_eq!(confirmed_sent.len(), 2);
3016 assert_eq!(confirmed_sent[0].len(), 2);
3017 assert_eq!(confirmed_sent[0][0].value, expected_step0_change);
3018 let OutputOfSentTx {
3019 value: ephemeral_v,
3020 external_recipient: to_addr,
3021 ephemeral_address: _,
3022 } = confirmed_sent[0][1].clone();
3023 assert_eq!(ephemeral_v, expected_ephemeral);
3024 assert!(to_addr.is_some());
3025
3026 assert_eq!(confirmed_sent[1].len(), 1);
3027 assert_matches!(
3028 &confirmed_sent[1][0],
3029 OutputOfSentTx { value: sent_v, external_recipient: sent_to_addr, ephemeral_address: None }
3030 if sent_v == &transfer_amount && sent_to_addr == &Some(tex_addr));
3031
3032 let tx_history = st.wallet().get_tx_history().unwrap();
3034
3035 let tx_0 = tx_history
3036 .iter()
3037 .find(|tx| tx.txid() == *txids.first())
3038 .unwrap();
3039 let tx_1 = tx_history
3040 .iter()
3041 .find(|tx| tx.txid() == *txids.last())
3042 .unwrap();
3043
3044 assert_eq!(tx_0.account_id(), &account_id);
3045 assert!(!tx_0.expired_unmined());
3046 assert_eq!(tx_0.has_change(), expected_step0_change.is_zero());
3047 assert!(!tx_0.is_shielding());
3048 assert_eq!(
3049 tx_0.account_value_delta(),
3050 -ZatBalance::from(expected_step0_fee),
3051 );
3052
3053 assert_eq!(tx_1.account_id(), &account_id);
3054 assert!(!tx_1.expired_unmined());
3055 assert!(!tx_1.has_change());
3056 assert!(!tx_0.is_shielding());
3057 assert_eq!(
3058 tx_1.account_value_delta(),
3059 -ZatBalance::from(expected_ephemeral),
3060 );
3061
3062 let ending_balance = st.get_spendable_balance(account_id, ConfirmationsPolicy::MIN);
3063 assert_eq!(initial_balance - total_sent, ending_balance.into());
3064}
3065
3066#[cfg(feature = "transparent-inputs")]
3067pub fn proposal_fails_if_not_all_ephemeral_outputs_consumed<T: ShieldedPoolTester, Dsf>(
3068 ds_factory: Dsf,
3069 cache: impl TestCache,
3070) where
3071 Dsf: DataStoreFactory,
3072{
3073 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
3074
3075 let account = st.test_account().cloned().unwrap();
3076 let account_id = account.id();
3077 let dfvk = T::test_account_fvk(&st);
3078
3079 let add_funds = |st: &mut TestState<_, Dsf::DataStore, _>, value| {
3080 let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
3081 st.scan_cached_blocks(h, 1);
3082
3083 assert_eq!(
3084 st.wallet()
3085 .block_max_scanned()
3086 .unwrap()
3087 .unwrap()
3088 .block_height(),
3089 h
3090 );
3091 assert_eq!(
3092 st.get_spendable_balance(account_id, ConfirmationsPolicy::MIN),
3093 value
3094 );
3095 };
3096
3097 let value = Zatoshis::const_from_u64(100000);
3098 let transfer_amount = Zatoshis::const_from_u64(50000);
3099
3100 add_funds(&mut st, value);
3102
3103 let tex_addr = Address::Tex([0x4; 20]);
3105 let proposal = st.propose_transfer_to(&tex_addr, transfer_amount);
3106
3107 let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible, _>(
3111 account.usk(),
3112 OvkPolicy::Sender,
3113 &proposal,
3114 );
3115 assert_matches!(create_proposed_result, Ok(_));
3116
3117 let frobbed_proposal = Proposal::multi_step(
3120 *proposal.fee_rule(),
3121 proposal.min_target_height(),
3122 proposal.confirmations_policy(),
3123 NonEmpty::singleton(proposal.steps().first().clone()),
3124 )
3125 .unwrap();
3126
3127 let create_proposed_result = st.create_proposed_transactions::<Infallible, _, Infallible, _>(
3128 account.usk(),
3129 OvkPolicy::Sender,
3130 &frobbed_proposal,
3131 );
3132 assert_matches!(
3133 create_proposed_result,
3134 Err(Error::Proposal(ProposalError::EphemeralOutputLeftUnspent(so)))
3135 if so == StepOutput::new(0, StepOutputIndex::Change(1))
3136 );
3137}
3138
3139pub fn create_to_address_fails_on_incorrect_usk<T: ShieldedPoolTester, Dsf: DataStoreFactory>(
3140 ds_factory: Dsf,
3141) {
3142 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, ()).build::<T>();
3143 let dfvk = T::test_account_fvk(&st);
3144 let to = T::fvk_default_address(&dfvk);
3145
3146 let acct1 = zip32::AccountId::try_from(1).unwrap();
3148 let usk1 = UnifiedSpendingKey::from_seed(st.network(), &[1u8; 32], acct1).unwrap();
3149
3150 let input_selector = GreedyInputSelector::<Dsf::DataStore>::new();
3151 let change_strategy =
3152 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
3153
3154 let req = TransactionRequest::new(vec![Payment::without_memo(
3155 to.to_zcash_address(st.network()),
3156 Zatoshis::const_from_u64(1),
3157 )])
3158 .unwrap();
3159
3160 assert_matches!(
3162 st.spend(
3163 &input_selector,
3164 &change_strategy,
3165 &usk1,
3166 req,
3167 OvkPolicy::Sender,
3168 ConfirmationsPolicy::MIN,
3169 ),
3170 Err(data_api::error::Error::KeyNotRecognized)
3171 );
3172}
3173
3174pub fn proposal_fails_with_no_blocks<T: ShieldedPoolTester, Dsf>(ds_factory: Dsf)
3175where
3176 Dsf: DataStoreFactory,
3177 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
3178{
3179 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, ()).build::<T>();
3180
3181 let account_id = st.test_account().unwrap().id();
3182 let dfvk = T::test_account_fvk(&st);
3183 let to = T::fvk_default_address(&dfvk);
3184
3185 assert_eq!(st.get_wallet_summary(ConfirmationsPolicy::MIN), None);
3187
3188 assert_matches!(
3190 st.propose_standard_transfer::<Infallible>(
3191 account_id,
3192 StandardFeeRule::Zip317,
3193 ConfirmationsPolicy::MIN,
3194 &to,
3195 Zatoshis::const_from_u64(1),
3196 None,
3197 None,
3198 T::SHIELDED_PROTOCOL,
3199 ),
3200 Err(data_api::error::Error::ScanRequired)
3201 );
3202}
3203
3204pub fn spend_fails_on_unverified_notes<T: ShieldedPoolTester>(
3205 ds_factory: impl DataStoreFactory,
3206 cache: impl TestCache,
3207) {
3208 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
3209
3210 let account = st.test_account().cloned().unwrap();
3211 let account_id = account.id();
3212 let dfvk = T::test_account_fvk(&st);
3213
3214 let value = Zatoshis::const_from_u64(50000);
3216 st.add_a_single_note_checking_balance(value);
3217
3218 assert_eq!(
3220 st.get_pending_shielded_balance(account_id, ConfirmationsPolicy::default()),
3221 value
3222 );
3223 assert_eq!(
3224 st.get_spendable_balance(account_id, ConfirmationsPolicy::default()),
3225 Zatoshis::ZERO
3226 );
3227
3228 let no_recovery = Some(Ratio::new(0, 0));
3232
3233 let summary = st.get_wallet_summary(ConfirmationsPolicy::MIN);
3235 assert_eq!(
3236 summary.as_ref().and_then(|s| s.progress().recovery()),
3237 no_recovery,
3238 );
3239 assert_eq!(summary.map(|s| s.progress().scan()), Some(Ratio::new(1, 1)));
3240
3241 let (h2, _, _) = st.add_a_single_note_checking_balance(value);
3243
3244 let total = (value + value).unwrap();
3246 assert_eq!(
3247 st.get_spendable_balance(
3248 account_id,
3249 ConfirmationsPolicy::new_symmetrical_unchecked(
3250 2,
3251 #[cfg(feature = "transparent-inputs")]
3252 false
3253 )
3254 ),
3255 value
3256 );
3257 assert_eq!(
3258 st.get_pending_shielded_balance(
3259 account_id,
3260 ConfirmationsPolicy::new_symmetrical_unchecked(
3261 2,
3262 #[cfg(feature = "transparent-inputs")]
3263 false
3264 )
3265 ),
3266 value
3267 );
3268 assert_eq!(st.get_total_balance(account_id), total);
3269
3270 let summary = st.get_wallet_summary(ConfirmationsPolicy::MIN);
3272 assert_eq!(
3273 summary.as_ref().and_then(|s| s.progress().recovery()),
3274 no_recovery
3275 );
3276 assert_eq!(summary.map(|s| s.progress().scan()), Some(Ratio::new(2, 2)));
3277
3278 let extsk2 = T::sk(&[0xf5; 32]);
3280 let to = T::sk_default_address(&extsk2);
3281 st.expect_insufficient_funds_with(
3282 &to,
3283 Zatoshis::const_from_u64(70000),
3284 ConfirmationsPolicy::new_symmetrical_unchecked(
3285 2,
3286 #[cfg(feature = "transparent-inputs")]
3287 false,
3288 ),
3289 Zatoshis::const_from_u64(50000),
3290 Zatoshis::const_from_u64(80000),
3291 );
3292
3293 for _ in 2..10 {
3296 st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
3297 }
3298 st.scan_cached_blocks(h2 + 1, 8);
3299
3300 assert_eq!(st.get_total_balance(account_id), (value * 10u64).unwrap());
3302
3303 st.expect_insufficient_funds_with(
3305 &to,
3306 Zatoshis::const_from_u64(70000),
3307 ConfirmationsPolicy::default(),
3308 Zatoshis::const_from_u64(50000),
3309 Zatoshis::const_from_u64(80000),
3310 );
3311
3312 let (h11, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
3314 st.scan_cached_blocks(h11, 1);
3315
3316 assert_eq!(st.get_total_balance(account_id), (value * 11u64).unwrap());
3318 assert_eq!(
3320 st.get_spendable_balance(account_id, ConfirmationsPolicy::default()),
3321 (value * 2u64).unwrap()
3322 );
3323 assert_eq!(
3324 st.get_pending_shielded_balance(account_id, ConfirmationsPolicy::default()),
3325 (value * 9u64).unwrap()
3326 );
3327
3328 let amount_sent = Zatoshis::from_u64(70000).unwrap();
3330 let proposal = st
3331 .propose_standard_transfer::<Infallible>(
3332 account_id,
3333 StandardFeeRule::Zip317,
3334 ConfirmationsPolicy::default(),
3335 &to,
3336 amount_sent,
3337 None,
3338 None,
3339 T::SHIELDED_PROTOCOL,
3340 )
3341 .unwrap();
3342
3343 let txid = st
3345 .create_proposed_transactions::<Infallible, _, Infallible, _>(
3346 account.usk(),
3347 OvkPolicy::Sender,
3348 &proposal,
3349 )
3350 .unwrap()[0];
3351
3352 let (h, _) = st.generate_next_block_including(txid);
3353 st.scan_cached_blocks(h, 1);
3354
3355 assert_eq!(
3357 st.get_total_balance(account_id),
3358 ((value * 11u64).unwrap() - (amount_sent + Zatoshis::from_u64(10000).unwrap()).unwrap())
3359 .unwrap()
3360 );
3361}
3362
3363pub fn ovk_policy_prevents_recovery_from_chain<T: ShieldedPoolTester, Dsf>(
3364 ds_factory: Dsf,
3365 cache: impl TestCache,
3366) where
3367 Dsf: DataStoreFactory,
3368{
3369 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
3370
3371 let account = st.test_account().cloned().unwrap();
3372 let account_id = account.id();
3373 let dfvk = T::test_account_fvk(&st);
3374
3375 let value = Zatoshis::const_from_u64(50000);
3377 let (h1, _, _) = st.add_a_single_note_checking_balance(value);
3378
3379 let extsk2 = T::sk(&[0xf5; 32]);
3380 let addr2 = T::sk_default_address(&extsk2);
3381
3382 let fee_rule = StandardFeeRule::Zip317;
3383
3384 #[allow(clippy::type_complexity)]
3385 let send_and_recover_with_policy = |st: &mut TestState<_, Dsf::DataStore, _>,
3386 ovk_policy|
3387 -> Result<
3388 Option<(Note, Address, MemoBytes)>,
3389 TransferErrT<
3390 Dsf::DataStore,
3391 GreedyInputSelector<Dsf::DataStore>,
3392 SingleOutputChangeStrategy<Dsf::DataStore>,
3393 >,
3394 > {
3395 let proposal = st.propose_standard_transfer(
3396 account_id,
3397 fee_rule,
3398 ConfirmationsPolicy::MIN,
3399 &addr2,
3400 Zatoshis::const_from_u64(15000),
3401 None,
3402 None,
3403 T::SHIELDED_PROTOCOL,
3404 )?;
3405
3406 let txid = st.create_proposed_transactions(account.usk(), ovk_policy, &proposal)?[0];
3408
3409 let tx = st
3411 .wallet()
3412 .get_transaction(txid)
3413 .map_err(Error::DataSource)?
3414 .unwrap();
3415
3416 Ok(T::try_output_recovery(st.network(), h1, &tx, &dfvk))
3417 };
3418
3419 assert_matches!(
3422 send_and_recover_with_policy(&mut st, OvkPolicy::Sender),
3423 Ok(Some((_, recovered_to, _))) if recovered_to == addr2
3424 );
3425
3426 for i in 1..=42 {
3429 st.generate_next_block(
3430 &T::sk_to_fvk(&T::sk(&[i as u8; 32])),
3431 AddressType::DefaultExternal,
3432 value,
3433 );
3434 }
3435 st.scan_cached_blocks(h1 + 1, 42);
3436
3437 assert_matches!(
3440 send_and_recover_with_policy(&mut st, OvkPolicy::Discard),
3441 Ok(None)
3442 );
3443}
3444
3445pub fn spend_succeeds_to_t_addr_zero_change<T: ShieldedPoolTester>(
3446 ds_factory: impl DataStoreFactory,
3447 cache: impl TestCache,
3448) {
3449 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
3450
3451 let value = Zatoshis::const_from_u64(70000);
3453 st.add_a_single_note_checking_balance(value);
3454
3455 let to = TransparentAddress::PublicKeyHash([7; 20]).into();
3457 let account = st.test_account().cloned().unwrap();
3458 let amount_sent = Zatoshis::const_from_u64(50000);
3459 let proposal = st.propose_transfer_to(&to, amount_sent);
3460
3461 assert_matches!(
3463 st.create_proposed_transactions::<Infallible, _, Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal),
3464 Ok(txids) if txids.len() == 1
3465 );
3466}
3467
3468pub fn change_note_spends_succeed<T: ShieldedPoolTester>(
3469 ds_factory: impl DataStoreFactory,
3470 cache: impl TestCache,
3471) {
3472 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
3473
3474 let value = Zatoshis::const_from_u64(70000);
3476 st.add_a_single_note_checking_balance(
3477 TestNoteConfig::from(value).with_address_type(AddressType::Internal),
3478 );
3479
3480 let account = st.test_account().cloned().unwrap();
3482 let account_id = account.id();
3483 assert_eq!(
3484 st.get_pending_shielded_balance(account_id, ConfirmationsPolicy::default()),
3485 value
3486 );
3487 assert_eq!(
3488 st.get_spendable_balance(account_id, ConfirmationsPolicy::default()),
3489 Zatoshis::ZERO
3490 );
3491
3492 let change_note_scope = st
3493 .wallet()
3494 .get_notes(T::SHIELDED_PROTOCOL)
3495 .unwrap()
3496 .iter()
3497 .find_map(|note| (note.note().value() == value).then_some(note.spending_key_scope()));
3498 assert_matches!(change_note_scope, Some(Scope::Internal));
3499
3500 let to = TransparentAddress::PublicKeyHash([7; 20]).into();
3502 let amount_sent = Zatoshis::const_from_u64(50000);
3503 let proposal = st.propose_transfer_to(&to, amount_sent);
3504
3505 assert_matches!(
3507 st.create_proposed_transactions::<Infallible, _, Infallible, _>(account.usk(), OvkPolicy::Sender, &proposal),
3508 Ok(txids) if txids.len() == 1
3509 );
3510}
3511
3512pub fn account_deletion<T: ShieldedPoolTester, DSF>(ds_factory: DSF, cache: impl TestCache)
3513where
3514 DSF: DataStoreFactory,
3515 <DSF as DataStoreFactory>::DataStore: Reset,
3516{
3517 let mut st = TestBuilder::new()
3518 .with_data_store_factory(ds_factory)
3519 .with_block_cache(cache)
3520 .build();
3521
3522 let seed = Secret::new([0u8; 32].to_vec());
3524 let birthday = AccountBirthday::from_sapling_activation(st.network(), BlockHash([0; 32]));
3525 let (account1, usk) = st
3526 .wallet_mut()
3527 .create_account("account1", &seed, &birthday, None)
3528 .unwrap();
3529 let dfvk = T::sk_to_fvk(T::usk_to_sk(&usk));
3530
3531 let (account2, usk2) = st
3532 .wallet_mut()
3533 .create_account("account2", &seed, &birthday, None)
3534 .unwrap();
3535 let dfvk2 = T::sk_to_fvk(T::usk_to_sk(&usk2));
3536
3537 let value = Zatoshis::from_u64(100000).unwrap();
3539 let (h, b0_result, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
3540 st.scan_cached_blocks(h, 1);
3541 let txid0 = *b0_result
3542 .txids()
3543 .first()
3544 .expect("A transaction was created.");
3545
3546 assert_eq!(st.get_total_balance(account1), value);
3548 assert_eq!(
3549 st.get_spendable_balance(account1, ConfirmationsPolicy::MIN),
3550 value
3551 );
3552 assert_eq!(st.get_total_balance(account2), Zatoshis::ZERO);
3553
3554 let bal_2 = Zatoshis::from_u64(50000).unwrap();
3555 let addr2 = T::fvk_default_address(&dfvk2);
3556 let req = TransactionRequest::new(vec![
3557 Payment::without_memo(addr2.to_zcash_address(st.network()), bal_2),
3559 ])
3560 .unwrap();
3561
3562 let change_strategy =
3563 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
3564 let input_selector = GreedyInputSelector::new();
3565
3566 let txid1 = st
3567 .spend(
3568 &input_selector,
3569 &change_strategy,
3570 &usk,
3571 req,
3572 OvkPolicy::Sender,
3573 ConfirmationsPolicy::MIN,
3574 )
3575 .unwrap()[0];
3576
3577 let bal_1 = (value - (bal_2 + MINIMUM_FEE).unwrap()).unwrap();
3578 assert_eq!(st.get_total_balance(account1), bal_1);
3579
3580 let (h, _) = st.generate_next_block_including(txid1);
3581 st.scan_cached_blocks(h, 1);
3582
3583 assert_eq!(st.get_total_balance(account2), bal_2);
3584 assert_eq!(st.get_total_balance(account1), bal_1);
3585
3586 assert_matches!(st.wallet_mut().get_tx_height(txid0), Ok(Some(_)));
3589
3590 assert_matches!(st.wallet_mut().delete_account(account1), Ok(_));
3592
3593 assert_matches!(st.wallet_mut().get_tx_height(txid0), Ok(None));
3595
3596 assert_matches!(st.wallet_mut().get_transaction(txid1), Ok(Some(_)));
3598
3599 let summary = st
3600 .wallet()
3601 .get_wallet_summary(ConfirmationsPolicy::MIN)
3602 .unwrap()
3603 .unwrap();
3604 assert!(summary.account_balances().get(&account1).is_none());
3605 assert_eq!(
3606 summary.account_balances().get(&account2).unwrap().total(),
3607 bal_2
3608 );
3609 assert_eq!(
3610 summary
3611 .account_balances()
3612 .get(&account2)
3613 .unwrap()
3614 .spendable_value(),
3615 bal_2
3616 );
3617
3618 let (account3, usk3) = st
3620 .wallet_mut()
3621 .create_account("account3", &seed, &birthday, None)
3622 .unwrap();
3623 let dfvk3 = T::sk_to_fvk(T::usk_to_sk(&usk3));
3624
3625 st.scan_cached_blocks(birthday.height(), 2);
3627
3628 let bal_3 = Zatoshis::from_u64(20000).unwrap();
3629 let addr3 = T::fvk_default_address(&dfvk3);
3630 let req = TransactionRequest::new(vec![
3631 Payment::without_memo(addr3.to_zcash_address(st.network()), bal_3),
3633 ])
3634 .unwrap();
3635
3636 let txid2 = st
3637 .spend(
3638 &input_selector,
3639 &change_strategy,
3640 &usk2,
3641 req,
3642 OvkPolicy::Sender,
3643 ConfirmationsPolicy::MIN,
3644 )
3645 .unwrap()[0];
3646
3647 let bal_2_final = (bal_2 - (bal_3 + MINIMUM_FEE).unwrap()).unwrap();
3648 assert_eq!(st.get_total_balance(account2), bal_2_final);
3649
3650 let (h, _) = st.generate_next_block_including(txid2);
3651 st.scan_cached_blocks(h, 1);
3652
3653 assert_eq!(st.get_total_balance(account2), bal_2_final);
3654 assert_eq!(st.get_total_balance(account3), bal_3);
3655
3656 assert_matches!(st.wallet_mut().get_tx_height(txid2), Ok(Some(_)));
3659
3660 assert_matches!(st.wallet_mut().delete_account(account3), Ok(_));
3662
3663 assert_matches!(st.wallet_mut().get_transaction(txid2), Ok(Some(_)));
3665
3666 let summary = st
3667 .wallet()
3668 .get_wallet_summary(ConfirmationsPolicy::default())
3669 .unwrap()
3670 .unwrap();
3671 assert!(summary.account_balances().get(&account3).is_none());
3672 assert_eq!(
3673 summary.account_balances().get(&account2).unwrap().total(),
3674 bal_2_final
3675 );
3676}
3677
3678pub fn account_deletion_with_internal_transfer<T: ShieldedPoolTester, DSF>(
3691 ds_factory: DSF,
3692 cache: impl TestCache,
3693) where
3694 DSF: DataStoreFactory,
3695 <DSF as DataStoreFactory>::DataStore: Reset,
3696{
3697 let mut st = TestBuilder::new()
3698 .with_data_store_factory(ds_factory)
3699 .with_block_cache(cache)
3700 .build();
3701
3702 let seed = Secret::new([0u8; 32].to_vec());
3704 let birthday = AccountBirthday::from_sapling_activation(st.network(), BlockHash([0; 32]));
3705 let (account1, usk1) = st
3706 .wallet_mut()
3707 .create_account("account1", &seed, &birthday, None)
3708 .unwrap();
3709 let dfvk1 = T::sk_to_fvk(T::usk_to_sk(&usk1));
3710
3711 let (account2, usk2) = st
3712 .wallet_mut()
3713 .create_account("account2", &seed, &birthday, None)
3714 .unwrap();
3715 let dfvk2 = T::sk_to_fvk(T::usk_to_sk(&usk2));
3716
3717 let value = Zatoshis::from_u64(100000).unwrap();
3719 let (h, _, _) = st.generate_next_block(&dfvk1, AddressType::DefaultExternal, value);
3720 st.scan_cached_blocks(h, 1);
3721
3722 assert_eq!(st.get_total_balance(account1), value);
3723 assert_eq!(st.get_total_balance(account2), Zatoshis::ZERO);
3724
3725 let bal_2 = Zatoshis::from_u64(50000).unwrap();
3727 let addr2 = T::fvk_default_address(&dfvk2);
3728 let req = TransactionRequest::new(vec![Payment::without_memo(
3729 addr2.to_zcash_address(st.network()),
3730 bal_2,
3731 )])
3732 .unwrap();
3733
3734 let change_strategy =
3735 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
3736 let input_selector = GreedyInputSelector::new();
3737
3738 let txid = st
3739 .spend(
3740 &input_selector,
3741 &change_strategy,
3742 &usk1,
3743 req,
3744 OvkPolicy::Sender,
3745 ConfirmationsPolicy::MIN,
3746 )
3747 .unwrap()[0];
3748
3749 let (h, _) = st.generate_next_block_including(txid);
3750 st.scan_cached_blocks(h, 1);
3751
3752 assert_eq!(st.get_total_balance(account2), bal_2);
3753
3754 let tx = st.wallet().get_transaction(txid).unwrap().unwrap();
3760 let params = *st.network();
3761 decrypt_and_store_transaction(¶ms, st.wallet_mut(), &tx, Some(h)).unwrap();
3762
3763 assert_matches!(st.wallet_mut().delete_account(account2), Ok(_));
3767
3768 let summary = st
3770 .wallet()
3771 .get_wallet_summary(ConfirmationsPolicy::MIN)
3772 .unwrap()
3773 .unwrap();
3774 assert!(summary.account_balances().get(&account2).is_none());
3775 assert!(summary.account_balances().contains_key(&account1));
3776}
3777
3778pub fn account_rename<T: ShieldedPoolTester, DSF>(ds_factory: DSF, cache: impl TestCache)
3783where
3784 DSF: DataStoreFactory,
3785 <DSF as DataStoreFactory>::DataStore: Reset,
3786{
3787 const ACCOUNT1_NAME: &str = "account1";
3789 const ACCOUNT2_NAME: &str = "account2";
3791 const RENAMED_NAME: &str = "renamed";
3793
3794 let mut st = TestBuilder::new()
3795 .with_data_store_factory(ds_factory)
3796 .with_block_cache(cache)
3797 .build();
3798
3799 let seed = Secret::new([0u8; 32].to_vec());
3801 let birthday = AccountBirthday::from_sapling_activation(st.network(), BlockHash([0; 32]));
3802 let (account1, _) = st
3803 .wallet_mut()
3804 .create_account(ACCOUNT1_NAME, &seed, &birthday, None)
3805 .unwrap();
3806 let (account2, _) = st
3807 .wallet_mut()
3808 .create_account(ACCOUNT2_NAME, &seed, &birthday, None)
3809 .unwrap();
3810
3811 assert_matches!(
3813 st.wallet_mut().set_account_name(account1, RENAMED_NAME),
3814 Ok(())
3815 );
3816 let renamed1 = st.wallet().get_account(account1).unwrap().unwrap();
3817 let untouched2 = st.wallet().get_account(account2).unwrap().unwrap();
3818 assert_eq!(renamed1.name(), Some(RENAMED_NAME));
3819 assert_eq!(untouched2.name(), Some(ACCOUNT2_NAME));
3820
3821 assert_matches!(
3823 st.wallet_mut().set_account_name(account2, RENAMED_NAME),
3824 Ok(())
3825 );
3826 let renamed1 = st.wallet().get_account(account1).unwrap().unwrap();
3827 let renamed2 = st.wallet().get_account(account2).unwrap().unwrap();
3828 assert_eq!(renamed1.name(), Some(RENAMED_NAME));
3829 assert_eq!(renamed2.name(), Some(RENAMED_NAME));
3830}
3831
3832pub fn external_address_change_spends_detected_in_restore_from_seed<T: ShieldedPoolTester, Dsf>(
3833 ds_factory: Dsf,
3834 cache: impl TestCache,
3835) where
3836 Dsf: DataStoreFactory,
3837 <Dsf as DataStoreFactory>::DataStore: Reset,
3838{
3839 let mut st = TestBuilder::new()
3840 .with_data_store_factory(ds_factory)
3841 .with_block_cache(cache)
3842 .build();
3843
3844 let seed = Secret::new([0u8; 32].to_vec());
3846 let birthday = AccountBirthday::from_sapling_activation(st.network(), BlockHash([0; 32]));
3847 let (account1, usk) = st
3848 .wallet_mut()
3849 .create_account("account1", &seed, &birthday, None)
3850 .unwrap();
3851 let dfvk = T::sk_to_fvk(T::usk_to_sk(&usk));
3852
3853 let (account2, usk2) = st
3854 .wallet_mut()
3855 .create_account("account2", &seed, &birthday, None)
3856 .unwrap();
3857 let dfvk2 = T::sk_to_fvk(T::usk_to_sk(&usk2));
3858
3859 let value = Zatoshis::from_u64(100000).unwrap();
3861 let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
3862 st.scan_cached_blocks(h, 1);
3863
3864 assert_eq!(st.get_total_balance(account1), value);
3866 assert_eq!(
3867 st.get_spendable_balance(account1, ConfirmationsPolicy::MIN),
3868 value
3869 );
3870 assert_eq!(st.get_total_balance(account2), Zatoshis::ZERO);
3871
3872 let amount_sent = Zatoshis::from_u64(20000).unwrap();
3873 let amount_legacy_change = Zatoshis::from_u64(30000).unwrap();
3874 let addr = T::fvk_default_address(&dfvk);
3875 let addr2 = T::fvk_default_address(&dfvk2);
3876 let req = TransactionRequest::new(vec![
3877 Payment::without_memo(addr2.to_zcash_address(st.network()), amount_sent),
3879 Payment::without_memo(addr.to_zcash_address(st.network()), amount_legacy_change),
3881 ])
3882 .unwrap();
3883
3884 let change_strategy =
3885 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
3886 let input_selector = GreedyInputSelector::new();
3887
3888 let txid = st
3889 .spend(
3890 &input_selector,
3891 &change_strategy,
3892 &usk,
3893 req,
3894 OvkPolicy::Sender,
3895 ConfirmationsPolicy::MIN,
3896 )
3897 .unwrap()[0];
3898
3899 let amount_left = (value - (amount_sent + MINIMUM_FEE + MARGINAL_FEE).unwrap()).unwrap();
3900 let pending_change = (amount_left - amount_legacy_change).unwrap();
3901
3902 assert_eq!(
3904 st.get_pending_change(account1, ConfirmationsPolicy::MIN),
3905 pending_change
3906 );
3907 assert_eq!(st.get_total_balance(account1), pending_change);
3909
3910 let (h, _) = st.generate_next_block_including(txid);
3911 st.scan_cached_blocks(h, 1);
3912
3913 assert_eq!(st.get_total_balance(account2), amount_sent,);
3914 assert_eq!(st.get_total_balance(account1), amount_left);
3915
3916 st.reset();
3917
3918 let (account1, restored_usk) = st
3920 .wallet_mut()
3921 .create_account("account1_restored", &seed, &birthday, None)
3922 .unwrap();
3923 assert!(T::fvks_equal(
3924 &T::sk_to_fvk(T::usk_to_sk(&restored_usk)),
3925 &dfvk,
3926 ));
3927
3928 let (account2, restored_usk2) = st
3929 .wallet_mut()
3930 .create_account("account2_restored", &seed, &birthday, None)
3931 .unwrap();
3932 assert!(T::fvks_equal(
3933 &T::sk_to_fvk(T::usk_to_sk(&restored_usk2)),
3934 &dfvk2,
3935 ));
3936
3937 st.scan_cached_blocks(st.sapling_activation_height(), 2);
3938
3939 assert_eq!(st.get_total_balance(account2), amount_sent);
3940 assert_eq!(st.get_total_balance(account1), amount_left);
3941}
3942
3943#[allow(dead_code)]
3944pub fn zip317_spend<T: ShieldedPoolTester, Dsf: DataStoreFactory>(
3945 ds_factory: Dsf,
3946 cache: impl TestCache,
3947) {
3948 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
3949
3950 let account = st.test_account().cloned().unwrap();
3951 let account_id = account.id();
3952 let dfvk = T::test_account_fvk(&st);
3953
3954 st.add_notes_checking_balance([Some(Zatoshis::const_from_u64(50000))]);
3956
3957 for _ in 1..=10 {
3959 st.add_notes_checking_balance([Some(Zatoshis::const_from_u64(1000))]);
3960 }
3961
3962 let total = Zatoshis::const_from_u64(60000);
3964 assert_eq!(st.get_total_balance(account_id), total);
3965 assert_eq!(
3966 st.get_spendable_balance(account_id, ConfirmationsPolicy::MIN),
3967 total
3968 );
3969
3970 let input_selector = GreedyInputSelector::<Dsf::DataStore>::new();
3971 let change_strategy =
3972 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
3973
3974 let req = TransactionRequest::new(vec![Payment::without_memo(
3976 T::fvk_default_address(&dfvk).to_zcash_address(st.network()),
3977 Zatoshis::const_from_u64(50000),
3978 )])
3979 .unwrap();
3980
3981 assert_matches!(
3982 st.spend(
3983 &input_selector,
3984 &change_strategy,
3985 account.usk(),
3986 req,
3987 OvkPolicy::Sender,
3988 ConfirmationsPolicy::MIN,
3989 ),
3990 Err(Error::InsufficientFunds { available, required })
3991 if available == Zatoshis::const_from_u64(51000)
3992 && required == Zatoshis::const_from_u64(60000)
3993 );
3994
3995 let req = TransactionRequest::new(vec![Payment::without_memo(
3998 T::fvk_default_address(&dfvk).to_zcash_address(st.network()),
3999 Zatoshis::const_from_u64(41000),
4000 )])
4001 .unwrap();
4002
4003 let txid = st
4004 .spend(
4005 &input_selector,
4006 &change_strategy,
4007 account.usk(),
4008 req,
4009 OvkPolicy::Sender,
4010 ConfirmationsPolicy::MIN,
4011 )
4012 .unwrap()[0];
4013
4014 let (h, _) = st.generate_next_block_including(txid);
4015 st.scan_cached_blocks(h, 1);
4016
4017 assert_eq!(
4021 st.get_total_balance(account_id),
4022 (total - Zatoshis::const_from_u64(10000)).unwrap()
4023 );
4024}
4025
4026#[cfg(feature = "transparent-inputs")]
4027pub fn shield_transparent<T: ShieldedPoolTester, Dsf>(ds_factory: Dsf, cache: impl TestCache)
4028where
4029 Dsf: DataStoreFactory,
4030 <<Dsf as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
4031{
4032 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
4033
4034 let account = st.test_account().cloned().unwrap();
4035 let dfvk = T::test_account_fvk(&st);
4036
4037 let uaddr = st
4038 .wallet()
4039 .get_last_generated_address_matching(account.id(), UnifiedAddressRequest::AllAvailableKeys)
4040 .unwrap()
4041 .unwrap();
4042 let taddr = uaddr.transparent().unwrap();
4043
4044 let (h, _, _) = st.generate_next_block(
4046 &dfvk,
4047 AddressType::Internal,
4048 Zatoshis::const_from_u64(50000),
4049 );
4050 st.scan_cached_blocks(h, 1);
4051
4052 let spent_outpoint = OutPoint::fake();
4053 let utxo = WalletTransparentOutput::from_parts(
4054 spent_outpoint.clone(),
4055 TxOut::new(Zatoshis::const_from_u64(100000), taddr.script().into()),
4056 Some(h),
4057 Some(account.id()),
4058 Some(TransparentKeyScope::EXTERNAL),
4059 None,
4060 )
4061 .unwrap();
4062
4063 let res0 = st.wallet_mut().put_received_transparent_utxo(&utxo);
4064 assert_matches!(res0, Ok(_));
4065
4066 let input_selector = GreedyInputSelector::new();
4067 let change_strategy =
4068 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
4069
4070 let txids = st
4071 .shield_transparent_funds(
4072 &input_selector,
4073 &change_strategy,
4074 Zatoshis::from_u64(10000).unwrap(),
4075 account.usk(),
4076 &[*taddr],
4077 account.id(),
4078 ConfirmationsPolicy::MIN,
4079 )
4080 .unwrap();
4081 assert_eq!(txids.len(), 1);
4082 let shielding_txid = *txids.first();
4083
4084 assert!(
4085 !st.wallet()
4086 .transaction_data_requests()
4087 .unwrap()
4088 .contains(&TransactionDataRequest::GetStatus(shielding_txid)),
4089 "a wallet-owned shielded output makes the transaction observable by scanning",
4090 );
4091
4092 let tx_summary = st.get_tx_from_history(shielding_txid).unwrap().unwrap();
4093 assert_eq!(tx_summary.spent_note_count(), 1);
4094 assert!(tx_summary.has_change());
4095 assert_eq!(tx_summary.received_note_count(), 0);
4096 assert_eq!(tx_summary.sent_note_count(), 0);
4097 assert!(tx_summary.is_shielding());
4098
4099 let (h, _) = st.generate_next_block_including(shielding_txid);
4101 let scan_result = st.scan_cached_blocks(h, 1);
4102
4103 let tx_summary = st.get_tx_from_history(shielding_txid).unwrap().unwrap();
4105 assert_eq!(tx_summary.spent_note_count(), 1);
4106 assert!(tx_summary.has_change());
4107 assert_eq!(tx_summary.received_note_count(), 0);
4108 assert_eq!(tx_summary.sent_note_count(), 0);
4109 assert!(tx_summary.is_shielding());
4110
4111 let requests = st.wallet().transaction_data_requests().unwrap();
4114 assert!(
4115 !requests
4116 .iter()
4117 .any(|req| req == &TransactionDataRequest::Enhancement(*spent_outpoint.txid()))
4118 );
4119
4120 let tx = st
4123 .wallet()
4124 .get_transaction(*txids.first())
4125 .unwrap()
4126 .unwrap();
4127 let params = *st.network();
4128 decrypt_and_store_transaction(¶ms, st.wallet_mut(), &tx, Some(h)).unwrap();
4129
4130 let requests = st.wallet().transaction_data_requests().unwrap();
4132 assert!(
4133 requests
4134 .iter()
4135 .any(|req| req == &TransactionDataRequest::Enhancement(*spent_outpoint.txid()))
4136 );
4137
4138 for _ in 0..BLOSSOM_DEFAULT_TX_EXPIRY_DELTA {
4142 st.generate_next_block(
4143 &dfvk,
4144 AddressType::DefaultExternal,
4145 Zatoshis::const_from_u64(10000),
4146 );
4147 }
4148 st.scan_cached_blocks(
4149 scan_result.scanned_range().end,
4150 usize::try_from(BLOSSOM_DEFAULT_TX_EXPIRY_DELTA).unwrap(),
4151 );
4152
4153 st.wallet_mut()
4155 .set_transaction_status(
4156 *spent_outpoint.txid(),
4157 data_api::TransactionStatus::TxidNotRecognized,
4158 )
4159 .unwrap();
4160
4161 let requests = st.wallet().transaction_data_requests().unwrap();
4163 assert!(
4164 !requests
4165 .iter()
4166 .any(|req| req == &TransactionDataRequest::Enhancement(*spent_outpoint.txid()))
4167 );
4168}
4169
4170#[allow(dead_code)]
4172pub fn birthday_in_anchor_shard<T: ShieldedPoolTester>(
4173 ds_factory: impl DataStoreFactory,
4174 cache: impl TestCache,
4175) {
4176 let frontier_tree_size: u32 = (0x1 << 16) + 1234;
4184 let mut st = TestBuilder::new()
4185 .with_data_store_factory(ds_factory)
4186 .with_block_cache(cache)
4187 .with_initial_chain_state(|rng, network| {
4188 let birthday_height = network.activation_height(NetworkUpgrade::Nu5).unwrap() + 1000;
4189
4190 let (prior_sapling_roots, sapling_initial_tree) =
4193 Frontier::random_with_prior_subtree_roots(
4194 rng,
4195 frontier_tree_size.into(),
4196 NonZeroU8::new(16).unwrap(),
4197 );
4198 let prior_sapling_roots = prior_sapling_roots
4200 .into_iter()
4201 .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 500, root))
4202 .collect::<Vec<_>>();
4203
4204 #[cfg(feature = "orchard")]
4205 let (prior_orchard_roots, orchard_initial_tree) =
4206 Frontier::random_with_prior_subtree_roots(
4207 rng,
4208 frontier_tree_size.into(),
4209 NonZeroU8::new(16).unwrap(),
4210 );
4211 #[cfg(feature = "orchard")]
4213 let prior_orchard_roots = prior_orchard_roots
4214 .into_iter()
4215 .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 500, root))
4216 .collect::<Vec<_>>();
4217
4218 #[cfg(feature = "orchard")]
4220 let ironwood_initial_tree = Frontier::empty();
4221
4222 InitialChainState {
4223 chain_state: ChainState::new(
4224 birthday_height - 1,
4225 BlockHash([5; 32]),
4226 sapling_initial_tree,
4227 #[cfg(feature = "orchard")]
4228 orchard_initial_tree,
4229 #[cfg(feature = "orchard")]
4230 ironwood_initial_tree,
4231 ),
4232 prior_sapling_roots,
4233 #[cfg(feature = "orchard")]
4234 prior_orchard_roots,
4235 }
4236 })
4237 .with_account_having_current_birthday()
4238 .build();
4239
4240 let not_our_value = Zatoshis::const_from_u64(10000);
4242 let not_our_key = T::random_fvk(st.rng_mut());
4243 let (initial_height, _, _) =
4244 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
4245 for _ in 1..9 {
4246 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
4247 }
4248
4249 let (received_tx_height, _, _) = st.generate_next_block(
4251 &T::test_account_fvk(&st),
4252 AddressType::DefaultExternal,
4253 Zatoshis::const_from_u64(500000),
4254 );
4255
4256 for _ in 0..15 {
4258 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value);
4259 }
4260
4261 st.scan_cached_blocks(initial_height + 5, 20);
4264
4265 let account = st.test_account().unwrap();
4267 let account_id = account.id();
4268 let spendable = T::select_spendable_notes(
4269 &st,
4270 account_id,
4271 TargetValue::AtLeast(Zatoshis::const_from_u64(300000)),
4272 TargetHeight::from(received_tx_height + 10),
4273 ConfirmationsPolicy::default(),
4274 &[],
4275 )
4276 .unwrap();
4277
4278 assert_eq!(spendable.len(), 0);
4279
4280 st.scan_cached_blocks(initial_height, 5);
4282
4283 let spendable = T::select_spendable_notes(
4285 &st,
4286 account_id,
4287 TargetValue::AtLeast(Zatoshis::const_from_u64(300000)),
4288 TargetHeight::from(received_tx_height + 10),
4289 ConfirmationsPolicy::default(),
4290 &[],
4291 )
4292 .unwrap();
4293
4294 assert_eq!(spendable.len(), 1);
4295}
4296
4297pub fn checkpoint_gaps<T: ShieldedPoolTester, Dsf: DataStoreFactory>(
4298 ds_factory: Dsf,
4299 cache: impl TestCache,
4300) {
4301 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
4302
4303 st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(500000));
4305
4306 let account = st.test_account().cloned().unwrap();
4309 let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32]));
4310 let not_our_value = Zatoshis::const_from_u64(10000);
4311 let sapling_end_size = st.latest_cached_block().unwrap().sapling_end_size();
4312 let orchard_end_size = st.latest_cached_block().unwrap().orchard_end_size();
4313 let ironwood_end_size = st.latest_cached_block().unwrap().ironwood_end_size();
4314 st.generate_block_at(
4315 account.birthday().height() + 10,
4316 BlockHash([0; 32]),
4317 &[FakeCompactOutput::new(
4318 ¬_our_key,
4319 AddressType::DefaultExternal,
4320 not_our_value,
4321 )],
4322 sapling_end_size,
4323 orchard_end_size,
4324 ironwood_end_size,
4325 false,
4326 );
4327
4328 st.scan_cached_blocks(account.birthday().height() + 10, 1);
4330
4331 let spendable = T::select_spendable_notes(
4333 &st,
4334 account.id(),
4335 TargetValue::AtLeast(Zatoshis::const_from_u64(300000)),
4336 TargetHeight::from(account.birthday().height() + 5),
4337 ConfirmationsPolicy::new_unchecked(
4338 1,
4339 5,
4340 #[cfg(feature = "transparent-inputs")]
4341 false,
4342 ),
4343 &[],
4344 )
4345 .unwrap();
4346 assert_eq!(spendable.len(), 1);
4347
4348 let input_selector = GreedyInputSelector::<Dsf::DataStore>::new();
4349 let change_strategy =
4350 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
4351
4352 let to = T::fvk_default_address(¬_our_key);
4353 let req = TransactionRequest::new(vec![Payment::without_memo(
4354 to.to_zcash_address(st.network()),
4355 Zatoshis::const_from_u64(10000),
4356 )])
4357 .unwrap();
4358
4359 assert_matches!(
4361 st.spend(
4362 &input_selector,
4363 &change_strategy,
4364 account.usk(),
4365 req,
4366 OvkPolicy::Sender,
4367 ConfirmationsPolicy::new_symmetrical_unchecked(
4368 5,
4369 #[cfg(feature = "transparent-inputs")]
4370 false
4371 ),
4372 ),
4373 Ok(_)
4374 );
4375}
4376
4377#[allow(clippy::type_complexity)]
4381fn tree_anchor_state<S, const DEPTH: u8, const SHARD_HEIGHT: u8>(
4382 tree: &mut ShardTree<S, DEPTH, SHARD_HEIGHT>,
4383 note_position: Position,
4384 anchor_height: BlockHeight,
4385) -> Result<(bool, BTreeSet<BlockHeight>, BTreeSet<BlockHeight>), ShardTreeError<S::Error>>
4386where
4387 S: ShardStore<CheckpointId = BlockHeight>,
4388 S::H: Hashable + Clone + PartialEq,
4389{
4390 let witness_computable = tree
4391 .witness_at_checkpoint_id(note_position, &anchor_height)?
4392 .is_some();
4393 let retained = tree
4394 .store()
4395 .retained_checkpoints()
4396 .map_err(ShardTreeError::Storage)?;
4397 let checkpoint_count = tree
4398 .store()
4399 .checkpoint_count()
4400 .map_err(ShardTreeError::Storage)?;
4401 let mut survivors = BTreeSet::new();
4402 tree.store()
4403 .for_each_checkpoint(checkpoint_count, |cid, _| {
4404 survivors.insert(*cid);
4405 Ok(())
4406 })
4407 .map_err(ShardTreeError::Storage)?;
4408 Ok((witness_computable, survivors, retained))
4409}
4410
4411pub fn anchor_checkpoints_retained_across_deep_scan<
4435 T: ShieldedPoolTester,
4436 Dsf: DataStoreFactory,
4437>(
4438 ds_factory: Dsf,
4439 cache: impl TestCache,
4440 interval: AnchorRetentionInterval,
4441) {
4442 let interval_blocks = interval.block_count().get();
4443
4444 let activation = BlockHeight::from_u32(100_000);
4447 let ironwood_active_network = LocalNetwork {
4448 nu6: Some(activation),
4449 nu6_1: Some(activation),
4450 nu6_2: Some(activation),
4451 nu6_3: Some(activation),
4452 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
4453 };
4454
4455 let mut st = TestDsl::from(
4456 TestBuilder::new()
4457 .with_network(ironwood_active_network)
4458 .with_data_store_factory(ds_factory)
4459 .with_block_cache(cache)
4460 .with_anchor_retention_interval(interval)
4461 .with_account_from_sapling_activation(BlockHash([0; 32])),
4462 )
4463 .build::<T>();
4464
4465 let (received_height, _, _) =
4467 st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(500_000));
4468 let received = u32::from(received_height);
4469 let floor = u32::from(activation);
4470
4471 let mut anchor = u32::from(
4474 anchor_grid(st.network(), interval).boundary_at_or_above(BlockHeight::from(floor)),
4475 );
4476 while anchor <= received {
4477 anchor += interval_blocks;
4478 }
4479
4480 let tip = anchor + PRUNING_DEPTH + 10;
4483
4484 let not_our_fvk = T::sk_to_fvk(&T::sk(&[0xf5; 32]));
4487 let filler_count = tip - received;
4488 for _ in 0..filler_count {
4489 st.generate_next_block(
4490 ¬_our_fvk,
4491 AddressType::DefaultExternal,
4492 Zatoshis::const_from_u64(10_000),
4493 );
4494 }
4495 st.scan_cached_blocks(received_height + 1, filler_count as usize);
4496
4497 let account_id = st.get_account().id();
4499 let spendable = T::select_spendable_notes(
4500 &st,
4501 account_id,
4502 TargetValue::AtLeast(Zatoshis::const_from_u64(1)),
4503 TargetHeight::from(BlockHeight::from(tip + 1)),
4504 ConfirmationsPolicy::MIN,
4505 &[],
4506 )
4507 .unwrap();
4508 let note_position = spendable
4509 .first()
4510 .expect("the received note is spendable")
4511 .note_commitment_tree_position();
4512
4513 let anchor_height = BlockHeight::from(anchor);
4514 let (witness_computable, survivors, retained) = match T::SHIELDED_PROTOCOL {
4515 ShieldedPool::Sapling => st
4516 .wallet_mut()
4517 .with_sapling_tree_mut(|tree| tree_anchor_state(tree, note_position, anchor_height))
4518 .unwrap(),
4519 #[cfg(feature = "orchard")]
4520 ShieldedPool::Orchard => st
4521 .wallet_mut()
4522 .with_orchard_tree_mut(|tree| tree_anchor_state(tree, note_position, anchor_height))
4523 .unwrap(),
4524 other => {
4525 unreachable!("anchor retention test covers only Sapling and Orchard, got {other:?}")
4526 }
4527 };
4528
4529 assert!(
4532 witness_computable,
4533 "a witness for the received note as of the retained anchor at height {anchor} \
4534 must still be constructible",
4535 );
4536
4537 let expected_anchors: BTreeSet<BlockHeight> = (floor..=tip)
4539 .map(BlockHeight::from)
4540 .filter(|h| anchor_grid(st.network(), interval).is_boundary(*h))
4541 .collect();
4542 assert_eq!(
4543 retained, expected_anchors,
4544 "only the interval-aligned anchors at/above the NU6.3 floor should be retained",
4545 );
4546
4547 assert!(
4550 survivors.contains(&anchor_height),
4551 "the buried anchor checkpoint must survive",
4552 );
4553 assert!(
4554 !survivors.contains(&BlockHeight::from(anchor + 1)),
4555 "the ordinary checkpoint just above the buried anchor must have been pruned",
4556 );
4557
4558 for h in (tip - PRUNING_DEPTH + 1)..=tip {
4561 assert!(
4562 survivors.contains(&BlockHeight::from(h)),
4563 "checkpoint at tip-window height {h} must be retained",
4564 );
4565 }
4566}
4567
4568#[cfg(feature = "orchard")]
4579pub fn empty_boundary_blocks_are_checkpointed_and_retained<
4580 T: ShieldedPoolTester,
4581 Dsf: DataStoreFactory,
4582>(
4583 ds_factory: Dsf,
4584 cache: impl TestCache,
4585 interval: AnchorRetentionInterval,
4586) {
4587 let interval_blocks = interval.block_count().get();
4588
4589 let activation = BlockHeight::from_u32(100_000);
4592 let ironwood_active_network = LocalNetwork {
4593 nu6: Some(activation),
4594 nu6_1: Some(activation),
4595 nu6_2: Some(activation),
4596 nu6_3: Some(activation),
4597 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
4598 };
4599
4600 let mut st = TestDsl::from(
4601 TestBuilder::new()
4602 .with_network(ironwood_active_network)
4603 .with_data_store_factory(ds_factory)
4604 .with_block_cache(cache)
4605 .with_anchor_retention_interval(interval)
4606 .with_account_from_sapling_activation(BlockHash([0; 32])),
4607 )
4608 .build::<T>();
4609
4610 let (received_height, _, _) =
4612 st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(500_000));
4613 let received = u32::from(received_height);
4614
4615 let first_boundary = {
4618 let mut b = u32::from(anchor_grid(st.network(), interval).boundary_at_or_above(activation));
4619 while b <= received {
4620 b += interval_blocks;
4621 }
4622 b
4623 };
4624 let boundaries = [first_boundary, first_boundary + interval_blocks];
4625 let tip = boundaries[1] + 2;
4626
4627 let not_our_fvk = T::sk_to_fvk(&T::sk(&[0xf5; 32]));
4631 for height in (received + 1)..=tip {
4632 let generated =
4633 if anchor_grid(st.network(), interval).is_boundary(BlockHeight::from_u32(height)) {
4634 st.generate_empty_block().0
4635 } else {
4636 st.generate_next_block(
4637 ¬_our_fvk,
4638 AddressType::DefaultExternal,
4639 Zatoshis::const_from_u64(10_000),
4640 )
4641 .0
4642 };
4643 assert_eq!(u32::from(generated), height, "cache height must track");
4644 }
4645 st.scan_cached_blocks(received_height + 1, (tip - received) as usize);
4647
4648 let account_id = st.get_account().id();
4650 let spendable = T::select_spendable_notes(
4651 &st,
4652 account_id,
4653 TargetValue::AtLeast(Zatoshis::const_from_u64(1)),
4654 TargetHeight::from(BlockHeight::from_u32(tip + 1)),
4655 ConfirmationsPolicy::MIN,
4656 &[],
4657 )
4658 .unwrap();
4659 let note_position = spendable
4660 .first()
4661 .expect("the received note is spendable")
4662 .note_commitment_tree_position();
4663
4664 for boundary in boundaries {
4665 let anchor_height = BlockHeight::from_u32(boundary);
4666 let (witness_computable, survivors, retained) = match T::SHIELDED_PROTOCOL {
4667 ShieldedPool::Sapling => st
4668 .wallet_mut()
4669 .with_sapling_tree_mut(|tree| tree_anchor_state(tree, note_position, anchor_height))
4670 .unwrap(),
4671 ShieldedPool::Orchard => st
4672 .wallet_mut()
4673 .with_orchard_tree_mut(|tree| tree_anchor_state(tree, note_position, anchor_height))
4674 .unwrap(),
4675 other => {
4676 unreachable!("this test covers only Sapling and Orchard, got {other:?}")
4677 }
4678 };
4679
4680 assert!(
4681 survivors.contains(&anchor_height),
4682 "the empty boundary block at height {boundary} must be checkpointed",
4683 );
4684 assert!(
4685 retained.contains(&anchor_height),
4686 "the checkpoint at the empty boundary height {boundary} must be retained",
4687 );
4688 assert!(
4691 witness_computable,
4692 "a witness for the received note as of the empty boundary at height {boundary} \
4693 must be constructible",
4694 );
4695 }
4696
4697 let ironwood_retained = st
4700 .wallet_mut()
4701 .with_ironwood_tree_mut(|tree| {
4702 tree.store()
4703 .retained_checkpoints()
4704 .map_err(ShardTreeError::Storage)
4705 })
4706 .unwrap()
4707 .expect("the data store maintains an Ironwood tree");
4708 for boundary in boundaries {
4709 assert!(
4710 ironwood_retained.contains(&BlockHeight::from_u32(boundary)),
4711 "the Ironwood tree must retain the empty boundary at height {boundary}",
4712 );
4713 }
4714}
4715
4716#[cfg(feature = "orchard")]
4717pub fn pool_crossing_required<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
4718 ds_factory: impl DataStoreFactory,
4719 cache: impl TestCache,
4720) {
4721 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<P0>();
4725
4726 let account = st.test_account().cloned().unwrap();
4727
4728 let p0_fvk = P0::test_account_fvk(&st);
4729
4730 let p1_fvk = P1::test_account_fvk(&st);
4731 let p1_to = P1::fvk_default_address(&p1_fvk);
4732
4733 let note_value = Zatoshis::const_from_u64(350000);
4734 st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value);
4735 st.scan_cached_blocks(account.birthday().height(), 2);
4736
4737 let initial_balance = note_value;
4738 assert_eq!(st.get_total_balance(account.id()), initial_balance);
4739 assert_eq!(
4740 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
4741 initial_balance
4742 );
4743
4744 let transfer_amount = Zatoshis::const_from_u64(200000);
4745 let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo(
4746 p1_to.to_zcash_address(st.network()),
4747 transfer_amount,
4748 )])
4749 .unwrap();
4750
4751 let input_selector = GreedyInputSelector::new();
4752 let change_strategy =
4753 single_output_change_strategy(StandardFeeRule::Zip317, None, P1::SHIELDED_PROTOCOL);
4754 let proposal0 = st
4755 .propose_transfer(
4756 account.id(),
4757 &input_selector,
4758 &change_strategy,
4759 p0_to_p1,
4760 ConfirmationsPolicy::MIN,
4761 )
4762 .unwrap();
4763
4764 let _min_target_height = proposal0.min_target_height();
4765 assert_eq!(proposal0.steps().len(), 1);
4766 let step0 = &proposal0.steps().head;
4767
4768 let expected_fee = Zatoshis::const_from_u64(20000);
4770 assert_eq!(step0.balance().fee_required(), expected_fee);
4771
4772 let expected_change = (note_value - transfer_amount - expected_fee).unwrap();
4773 let proposed_change = step0.balance().proposed_change();
4774 assert_eq!(proposed_change.len(), 1);
4775 let change_output = proposed_change.first().unwrap();
4776 assert_eq!(
4778 change_output.output_pool(),
4779 PoolType::Shielded(std::cmp::max(ShieldedPool::Sapling, ShieldedPool::Orchard))
4780 );
4781 assert_eq!(change_output.value(), expected_change);
4782
4783 let txids = st.create_proposed_expecting(&proposal0, 1);
4784
4785 let (h, _) = st.generate_next_block_including(txids[0]);
4786 st.scan_cached_blocks(h, 1);
4787
4788 assert_eq!(
4789 st.get_total_balance(account.id()),
4790 (initial_balance - expected_fee).unwrap()
4791 );
4792 assert_eq!(
4793 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
4794 (initial_balance - expected_fee).unwrap()
4795 );
4796}
4797
4798#[cfg(feature = "orchard")]
4799pub fn fully_funded_fully_private<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
4800 ds_factory: impl DataStoreFactory,
4801 cache: impl TestCache,
4802) {
4803 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<P0>();
4805
4806 let account = st.test_account().cloned().unwrap();
4807
4808 let p0_fvk = P0::test_account_fvk(&st);
4809
4810 let p1_fvk = P1::test_account_fvk(&st);
4811 let p1_to = P1::fvk_default_address(&p1_fvk);
4812
4813 let note_value = Zatoshis::const_from_u64(350000);
4814 st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value);
4815 st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value);
4816 st.scan_cached_blocks(account.birthday().height(), 2);
4817
4818 let initial_balance = (note_value * 2u64).unwrap();
4819 assert_eq!(st.get_total_balance(account.id()), initial_balance);
4820 assert_eq!(
4821 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
4822 initial_balance
4823 );
4824
4825 let transfer_amount = Zatoshis::const_from_u64(200000);
4826 let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo(
4827 p1_to.to_zcash_address(st.network()),
4828 transfer_amount,
4829 )])
4830 .unwrap();
4831
4832 let input_selector = GreedyInputSelector::new();
4833 let change_strategy =
4836 single_output_change_strategy(StandardFeeRule::Zip317, None, P0::SHIELDED_PROTOCOL);
4837 let proposal0 = st
4838 .propose_transfer(
4839 account.id(),
4840 &input_selector,
4841 &change_strategy,
4842 p0_to_p1,
4843 ConfirmationsPolicy::MIN,
4844 )
4845 .unwrap();
4846
4847 let _min_target_height = proposal0.min_target_height();
4848 assert_eq!(proposal0.steps().len(), 1);
4849 let step0 = &proposal0.steps().head;
4850
4851 let expected_fee = Zatoshis::const_from_u64(10000);
4854 assert_eq!(step0.balance().fee_required(), expected_fee);
4855
4856 let expected_change = (note_value - transfer_amount - expected_fee).unwrap();
4857 let proposed_change = step0.balance().proposed_change();
4858 assert_eq!(proposed_change.len(), 1);
4859 let change_output = proposed_change.first().unwrap();
4860 assert_eq!(
4863 change_output.output_pool(),
4864 PoolType::Shielded(P1::SHIELDED_PROTOCOL)
4865 );
4866 assert_eq!(change_output.value(), expected_change);
4867
4868 let txids = st.create_proposed_expecting(&proposal0, 1);
4869
4870 let (h, _) = st.generate_next_block_including(txids[0]);
4871 st.scan_cached_blocks(h, 1);
4872
4873 assert_eq!(
4874 st.get_total_balance(account.id()),
4875 (initial_balance - expected_fee).unwrap()
4876 );
4877 assert_eq!(
4878 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
4879 (initial_balance - expected_fee).unwrap()
4880 );
4881}
4882
4883#[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
4884pub fn fully_funded_send_to_t<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
4885 ds_factory: impl DataStoreFactory,
4886 cache: impl TestCache,
4887) {
4888 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<P0>();
4890
4891 let account = st.test_account().cloned().unwrap();
4892
4893 let p0_fvk = P0::test_account_fvk(&st);
4894 let p1_fvk = P1::test_account_fvk(&st);
4895 let (p1_to, _) = account.usk().default_transparent_address();
4896
4897 let note_value = Zatoshis::const_from_u64(350000);
4898 st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value);
4899 st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value);
4900 st.scan_cached_blocks(account.birthday().height(), 2);
4901
4902 let initial_balance = (note_value * 2u64).unwrap();
4903 assert_eq!(st.get_total_balance(account.id()), initial_balance);
4904 assert_eq!(
4905 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
4906 initial_balance
4907 );
4908
4909 let transfer_amount = Zatoshis::const_from_u64(200000);
4910 let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo(
4911 Address::Transparent(p1_to).to_zcash_address(st.network()),
4912 transfer_amount,
4913 )])
4914 .unwrap();
4915
4916 let input_selector = GreedyInputSelector::new();
4917 let change_strategy =
4920 single_output_change_strategy(StandardFeeRule::Zip317, None, P0::SHIELDED_PROTOCOL);
4921 let proposal0 = st
4922 .propose_transfer(
4923 account.id(),
4924 &input_selector,
4925 &change_strategy,
4926 p0_to_p1,
4927 ConfirmationsPolicy::MIN,
4928 )
4929 .unwrap();
4930
4931 let _min_target_height = proposal0.min_target_height();
4932 assert_eq!(proposal0.steps().len(), 1);
4933 let step0 = &proposal0.steps().head;
4934
4935 let expected_fee = Zatoshis::const_from_u64(15000);
4937 assert_eq!(step0.balance().fee_required(), expected_fee);
4938
4939 let expected_change = (note_value - transfer_amount - expected_fee).unwrap();
4940 let proposed_change = step0.balance().proposed_change();
4941 assert_eq!(proposed_change.len(), 1);
4942 let change_output = proposed_change.first().unwrap();
4943 assert_eq!(change_output.output_pool(), PoolType::SAPLING);
4947 assert_eq!(change_output.value(), expected_change);
4948
4949 let txids = st.create_proposed_expecting(&proposal0, 1);
4950
4951 let (h, _) = st.generate_next_block_including(txids[0]);
4952 st.scan_cached_blocks(h, 1);
4953
4954 assert_eq!(
4957 st.get_total_balance(account.id()),
4958 (initial_balance - expected_fee).unwrap()
4959 );
4960 assert_eq!(
4963 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
4964 (initial_balance - transfer_amount - expected_fee).unwrap()
4965 );
4966}
4967
4968#[cfg(feature = "orchard")]
4969pub fn multi_pool_checkpoint<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
4970 ds_factory: impl DataStoreFactory,
4971 cache: impl TestCache,
4972) {
4973 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<P0>();
4975
4976 let account = st.test_account().cloned().unwrap();
4977 let acct_id = account.id();
4978
4979 let p0_fvk = P0::test_account_fvk(&st);
4980 let p1_fvk = P1::test_account_fvk(&st);
4981
4982 let note_value = Zatoshis::const_from_u64(500000);
4985 let (start_height, _, _) =
4986 st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value);
4987 st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value);
4988 st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value);
4989 let scanned = st.scan_cached_blocks(start_height, 3);
4990
4991 let next_to_scan = scanned.scanned_range().end;
4992
4993 let initial_balance = (note_value * 3u64).unwrap();
4994 assert_eq!(st.get_total_balance(acct_id), initial_balance);
4995 assert_eq!(
4996 st.get_spendable_balance(acct_id, ConfirmationsPolicy::MIN),
4997 initial_balance
4998 );
4999
5000 for _ in 0..10 {
5002 st.generate_empty_block();
5003 }
5004
5005 let scanned = st.scan_cached_blocks(next_to_scan, 5);
5007 let next_to_scan = scanned.scanned_range().end;
5008
5009 assert_eq!(st.get_total_balance(acct_id), initial_balance);
5011 assert_eq!(
5012 st.get_spendable_balance(acct_id, ConfirmationsPolicy::MIN),
5013 initial_balance
5014 );
5015
5016 let input_selector = GreedyInputSelector::new();
5018 let change_strategy =
5019 single_output_change_strategy(StandardFeeRule::Zip317, None, P1::SHIELDED_PROTOCOL);
5020
5021 let transfer_amount = Zatoshis::const_from_u64(200000);
5023 let p0_transfer = TransactionRequest::new(vec![Payment::without_memo(
5024 P0::random_address(st.rng_mut()).to_zcash_address(st.network()),
5025 transfer_amount,
5026 )])
5027 .unwrap();
5028 let res = st
5029 .spend(
5030 &input_selector,
5031 &change_strategy,
5032 account.usk(),
5033 p0_transfer,
5034 OvkPolicy::Sender,
5035 ConfirmationsPolicy::MIN,
5036 )
5037 .unwrap();
5038 st.generate_next_block_including(*res.first());
5039
5040 let expected_fee = Zatoshis::const_from_u64(10000);
5041 let expected_change = (note_value - transfer_amount - expected_fee).unwrap();
5042 assert_eq!(
5043 st.get_total_balance(acct_id),
5044 ((note_value * 2u64).unwrap() + expected_change).unwrap()
5045 );
5046 assert_eq!(
5047 st.get_pending_change(acct_id, ConfirmationsPolicy::MIN),
5048 expected_change
5049 );
5050
5051 let both_transfer = TransactionRequest::new(vec![
5053 Payment::without_memo(
5054 P0::random_address(st.rng_mut()).to_zcash_address(st.network()),
5055 transfer_amount,
5056 ),
5057 Payment::without_memo(
5058 P1::random_address(st.rng_mut()).to_zcash_address(st.network()),
5059 transfer_amount,
5060 ),
5061 ])
5062 .unwrap();
5063 let res = st
5064 .spend(
5065 &input_selector,
5066 &change_strategy,
5067 account.usk(),
5068 both_transfer,
5069 OvkPolicy::Sender,
5070 ConfirmationsPolicy::MIN,
5071 )
5072 .unwrap();
5073 st.generate_next_block_including(*res.first());
5074
5075 for _ in 0..5 {
5077 st.generate_empty_block();
5078 }
5079
5080 let (max_height, _, _) =
5082 st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value);
5083
5084 st.scan_cached_blocks(
5086 next_to_scan,
5087 usize::try_from(u32::from(max_height) - u32::from(next_to_scan) + 1).unwrap(),
5088 );
5089
5090 let expected_final = (initial_balance + note_value
5091 - (transfer_amount * 3u64).unwrap()
5092 - (expected_fee * 3u64).unwrap())
5093 .unwrap();
5094 assert_eq!(st.get_total_balance(acct_id), expected_final);
5095
5096 let expected_checkpoints_p0: Vec<(BlockHeight, Option<Position>)> = [
5097 (99999, None),
5098 (100000, Some(0)),
5099 (100001, Some(1)),
5100 (100002, Some(1)),
5101 (100007, Some(1)), (100013, Some(3)),
5103 (100014, Some(5)),
5104 (100020, Some(6)),
5105 ]
5106 .into_iter()
5107 .map(|(h, pos)| (BlockHeight::from(h), pos.map(Position::from)))
5108 .collect();
5109
5110 let expected_checkpoints_p1: Vec<(BlockHeight, Option<Position>)> = [
5111 (99999, None),
5112 (100000, None),
5113 (100001, None),
5114 (100002, Some(0)),
5115 (100007, Some(0)), (100013, Some(0)),
5117 (100014, Some(2)),
5118 (100020, Some(2)),
5119 ]
5120 .into_iter()
5121 .map(|(h, pos)| (BlockHeight::from(h), pos.map(Position::from)))
5122 .collect();
5123
5124 let p0_checkpoints = st
5125 .wallet()
5126 .get_checkpoint_history(&P0::SHIELDED_PROTOCOL)
5127 .unwrap();
5128 assert_eq!(p0_checkpoints.to_vec(), expected_checkpoints_p0);
5129
5130 let p1_checkpoints = st
5131 .wallet()
5132 .get_checkpoint_history(&P1::SHIELDED_PROTOCOL)
5133 .unwrap();
5134 assert_eq!(p1_checkpoints.to_vec(), expected_checkpoints_p1);
5135}
5136
5137#[cfg(feature = "orchard")]
5138pub fn multi_pool_checkpoints_with_pruning<P0: ShieldedPoolTester, P1: ShieldedPoolTester>(
5139 ds_factory: impl DataStoreFactory,
5140 cache: impl TestCache,
5141) {
5142 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<P0>();
5144
5145 let account = st.test_account().cloned().unwrap();
5146
5147 let p0_fvk = P0::random_fvk(st.rng_mut());
5148 let p1_fvk = P1::random_fvk(st.rng_mut());
5149
5150 let note_value = Zatoshis::const_from_u64(10000);
5151 for _ in 0..10 {
5153 for _ in 0..10 {
5154 st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value);
5155 }
5156 for _ in 0..10 {
5157 st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value);
5158 }
5159 }
5160 st.scan_cached_blocks(account.birthday().height(), 200);
5161 for _ in 0..100 {
5162 st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value);
5163 st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value);
5164 }
5165 st.scan_cached_blocks(account.birthday().height() + 200, 200);
5166}
5167
5168pub fn valid_chain_states<T: ShieldedPoolTester>(
5169 ds_factory: impl DataStoreFactory,
5170 cache: impl TestCache,
5171) {
5172 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
5173
5174 let dfvk = T::test_account_fvk(&st);
5175
5176 assert_matches!(st.wallet().chain_height(), Ok(None));
5178
5179 let (h1, _, _) = st.generate_next_block(
5181 &dfvk,
5182 AddressType::DefaultExternal,
5183 Zatoshis::const_from_u64(5),
5184 );
5185
5186 st.scan_cached_blocks(h1, 1);
5188
5189 let (h2, _, _) = st.generate_next_block(
5191 &dfvk,
5192 AddressType::DefaultExternal,
5193 Zatoshis::const_from_u64(7),
5194 );
5195
5196 st.scan_cached_blocks(h2, 1);
5198}
5199
5200#[allow(dead_code)]
5202pub fn invalid_chain_cache_disconnected<T: ShieldedPoolTester>(
5203 ds_factory: impl DataStoreFactory,
5204 cache: impl TestCache,
5205) {
5206 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
5207
5208 let dfvk = T::test_account_fvk(&st);
5209
5210 let (h, _, _) = st.generate_next_block(
5212 &dfvk,
5213 AddressType::DefaultExternal,
5214 Zatoshis::const_from_u64(5),
5215 );
5216 let (last_contiguous_height, _, _) = st.generate_next_block(
5217 &dfvk,
5218 AddressType::DefaultExternal,
5219 Zatoshis::const_from_u64(7),
5220 );
5221
5222 st.scan_cached_blocks(h, 2);
5224
5225 let disconnect_height = last_contiguous_height + 1;
5227 st.generate_block_at(
5228 disconnect_height,
5229 BlockHash([1; 32]),
5230 &[FakeCompactOutput::new(
5231 &dfvk,
5232 AddressType::DefaultExternal,
5233 Zatoshis::const_from_u64(8),
5234 )],
5235 2,
5236 2,
5237 0,
5238 true,
5239 );
5240 st.generate_next_block(
5241 &dfvk,
5242 AddressType::DefaultExternal,
5243 Zatoshis::const_from_u64(3),
5244 );
5245
5246 assert_matches!(
5248 st.try_scan_cached_blocks(
5249 disconnect_height,
5250 2
5251 ),
5252 Err(chain::error::Error::Scan(ScanError::PrevHashMismatch { at_height }))
5253 if at_height == disconnect_height
5254 );
5255}
5256
5257pub fn data_db_truncation<T: ShieldedPoolTester, Dsf>(ds_factory: Dsf, cache: impl TestCache)
5258where
5259 Dsf: DataStoreFactory,
5260 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
5261{
5262 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
5263
5264 let account = st.test_account().cloned().unwrap();
5265 let dfvk = T::test_account_fvk(&st);
5266
5267 assert_eq!(st.get_wallet_summary(ConfirmationsPolicy::MIN), None);
5269
5270 let value = Zatoshis::const_from_u64(50000);
5272 let value2 = Zatoshis::const_from_u64(70000);
5273 let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
5274 st.generate_next_block(&dfvk, AddressType::DefaultExternal, value2);
5275
5276 st.scan_cached_blocks(h, 2);
5278
5279 assert_eq!(
5281 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
5282 (value + value2).unwrap()
5283 );
5284
5285 st.wallet_mut().truncate_to_height(h + 1).unwrap();
5287
5288 assert_eq!(
5290 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
5291 (value + value2).unwrap()
5292 );
5293
5294 st.wallet_mut().truncate_to_height(h).unwrap();
5296
5297 assert_eq!(
5300 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
5301 value
5302 );
5303 assert_eq!(
5304 st.get_pending_shielded_balance(account.id(), ConfirmationsPolicy::MIN),
5305 value2
5306 );
5307
5308 st.scan_cached_blocks(h, 2);
5310
5311 assert_eq!(
5313 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
5314 (value + value2).unwrap()
5315 );
5316}
5317
5318pub fn truncate_to_chain_state<T: ShieldedPoolTester, Dsf>(ds_factory: Dsf, cache: impl TestCache)
5319where
5320 Dsf: DataStoreFactory,
5321 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
5322{
5323 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
5333
5334 let sapling_activation = st
5335 .network()
5336 .activation_height(consensus::NetworkUpgrade::Sapling)
5337 .unwrap();
5338
5339 let account_fvk = T::test_account_fvk(&st);
5346 let seed = [1u8; 32];
5347 let other_sk = T::sk(&seed);
5348 let other_fvk = T::sk_to_fvk(&other_sk);
5349
5350 let initial_block_count = 8u32;
5351 st.generate_next_block(
5352 &account_fvk,
5353 AddressType::DefaultExternal,
5354 Zatoshis::const_from_u64(10000),
5355 );
5356 for _ in 1..initial_block_count {
5357 st.generate_next_block(
5358 &other_fvk,
5359 AddressType::DefaultExternal,
5360 Zatoshis::const_from_u64(10000),
5361 );
5362 }
5363 let scan_start = sapling_activation;
5364 st.scan_cached_blocks(scan_start, initial_block_count as usize);
5365
5366 let capture_height = sapling_activation + initial_block_count - 1;
5369 let captured_chain_state = st
5370 .latest_cached_block()
5371 .expect("should have cached blocks")
5372 .chain_state()
5373 .clone();
5374 assert_eq!(captured_chain_state.block_height(), capture_height);
5375
5376 let extra_blocks = PRUNING_DEPTH + 10;
5379 for _ in 0..extra_blocks {
5380 st.generate_next_block(
5381 &other_fvk,
5382 AddressType::DefaultExternal,
5383 Zatoshis::const_from_u64(5000),
5384 );
5385 }
5386 st.scan_cached_blocks(capture_height + 1, extra_blocks as usize);
5387
5388 let tip = st
5389 .wallet()
5390 .chain_height()
5391 .unwrap()
5392 .expect("chain tip should be set");
5393 assert!(
5394 tip >= capture_height + PRUNING_DEPTH,
5395 "tip should be beyond pruning depth from capture height"
5396 );
5397
5398 let truncation_result = st.wallet_mut().truncate_to_height(capture_height);
5402 assert!(
5403 truncation_result.is_err(),
5404 "truncate_to_height should fail when checkpoint has been pruned"
5405 );
5406
5407 st.wallet_mut()
5410 .truncate_to_chain_state(captured_chain_state.clone())
5411 .expect("truncate_to_chain_state should succeed");
5412
5413 let new_tip = st
5416 .wallet()
5417 .chain_height()
5418 .unwrap()
5419 .expect("chain tip should still be set after truncation");
5420 assert_eq!(new_tip, capture_height);
5421
5422 let hash_at_capture = st
5424 .wallet()
5425 .get_block_hash(capture_height)
5426 .unwrap()
5427 .expect("block hash should exist at capture height");
5428 assert_eq!(hash_at_capture, captured_chain_state.block_hash());
5429
5430 assert_eq!(
5432 st.wallet().get_block_hash(capture_height + 1).unwrap(),
5433 None,
5434 "blocks above capture height should be removed"
5435 );
5436}
5437
5438pub fn truncate_to_chain_state_below_birthday<T: ShieldedPoolTester, Dsf>(
5439 ds_factory: Dsf,
5440 cache: impl TestCache,
5441) where
5442 Dsf: DataStoreFactory,
5443 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
5444{
5445 let mut st = TestBuilder::new()
5451 .with_data_store_factory(ds_factory)
5452 .with_block_cache(cache)
5453 .with_initial_chain_state(|rng, network| {
5454 let birthday_height = network.activation_height(NetworkUpgrade::Sapling).unwrap() + 200;
5455
5456 let (prior_sapling_roots, sapling_initial_tree) =
5457 Frontier::random_with_prior_subtree_roots(rng, 1u64, NonZeroU8::new(16).unwrap());
5458 let prior_sapling_roots = prior_sapling_roots
5459 .into_iter()
5460 .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 100, root))
5461 .collect::<Vec<_>>();
5462
5463 #[cfg(feature = "orchard")]
5464 let (prior_orchard_roots, orchard_initial_tree) =
5465 Frontier::random_with_prior_subtree_roots(rng, 1u64, NonZeroU8::new(16).unwrap());
5466 #[cfg(feature = "orchard")]
5467 let prior_orchard_roots = prior_orchard_roots
5468 .into_iter()
5469 .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 100, root))
5470 .collect::<Vec<_>>();
5471
5472 #[cfg(feature = "orchard")]
5474 let ironwood_initial_tree = Frontier::empty();
5475
5476 InitialChainState {
5477 chain_state: ChainState::new(
5478 birthday_height - 1,
5479 BlockHash([5; 32]),
5480 sapling_initial_tree,
5481 #[cfg(feature = "orchard")]
5482 orchard_initial_tree,
5483 #[cfg(feature = "orchard")]
5484 ironwood_initial_tree,
5485 ),
5486 prior_sapling_roots,
5487 #[cfg(feature = "orchard")]
5488 prior_orchard_roots,
5489 }
5490 })
5491 .with_account_having_current_birthday()
5492 .build();
5493
5494 let other_fvk = T::random_fvk(st.rng_mut());
5496 let birthday_height = st.test_account().unwrap().birthday().height();
5497
5498 for _ in 0..5 {
5499 st.generate_next_block(
5500 &other_fvk,
5501 AddressType::DefaultExternal,
5502 Zatoshis::const_from_u64(10000),
5503 );
5504 }
5505 st.scan_cached_blocks(birthday_height, 5);
5506
5507 let extra_blocks = PRUNING_DEPTH + 10;
5510 for _ in 0..extra_blocks {
5511 st.generate_next_block(
5512 &other_fvk,
5513 AddressType::DefaultExternal,
5514 Zatoshis::const_from_u64(5000),
5515 );
5516 }
5517 st.scan_cached_blocks(birthday_height + 5, extra_blocks as usize);
5518
5519 let prior_chain_state = st
5523 .test_account()
5524 .unwrap()
5525 .birthday()
5526 .prior_chain_state()
5527 .clone();
5528
5529 let _target_height = prior_chain_state.block_height();
5533 st.wallet_mut()
5534 .truncate_to_chain_state(prior_chain_state)
5535 .expect("truncate_to_chain_state below birthday should succeed");
5536
5537 assert_eq!(
5539 st.wallet().get_block_hash(birthday_height).unwrap(),
5540 None,
5541 "blocks at birthday height should be removed after truncating below birthday"
5542 );
5543}
5544
5545pub fn truncate_to_chain_state_above_scanned<T: ShieldedPoolTester, Dsf>(
5546 ds_factory: Dsf,
5547 cache: impl TestCache,
5548) where
5549 Dsf: DataStoreFactory,
5550 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
5551{
5552 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
5559
5560 let birthday_height = st.test_account().unwrap().birthday().height();
5561
5562 let other_fvk = T::random_fvk(st.rng_mut());
5565 let initial_blocks = 5u32;
5566 for _ in 0..initial_blocks {
5567 st.generate_next_block(
5568 &other_fvk,
5569 AddressType::DefaultExternal,
5570 Zatoshis::const_from_u64(10000),
5571 );
5572 }
5573 st.scan_cached_blocks(birthday_height, initial_blocks as usize);
5574
5575 let extra_blocks = PRUNING_DEPTH + 10;
5576 for _ in 0..extra_blocks {
5577 st.generate_next_block(
5578 &other_fvk,
5579 AddressType::DefaultExternal,
5580 Zatoshis::const_from_u64(5000),
5581 );
5582 }
5583 st.scan_cached_blocks(birthday_height + initial_blocks, extra_blocks as usize);
5584 let max_scanned = birthday_height + initial_blocks + extra_blocks - 1;
5585
5586 T::put_subtree_roots(
5590 &mut st,
5591 0,
5592 &[CommitmentTreeRoot::from_parts(
5593 birthday_height,
5594 T::empty_tree_leaf(),
5595 )],
5596 )
5597 .unwrap();
5598
5599 let chain_tip = max_scanned + 500;
5601 st.wallet_mut().update_chain_tip(chain_tip).unwrap();
5602
5603 let target_height = max_scanned + 50;
5608 let shard_2_tree_size: u64 = (0x2 << 16) + 2;
5609 let (_, shard2_sapling_frontier) = Frontier::random_with_prior_subtree_roots(
5610 st.rng_mut(),
5611 shard_2_tree_size,
5612 NonZeroU8::new(16).unwrap(),
5613 );
5614 #[cfg(feature = "orchard")]
5615 let (_, shard2_orchard_frontier) = Frontier::random_with_prior_subtree_roots(
5616 st.rng_mut(),
5617 shard_2_tree_size,
5618 NonZeroU8::new(16).unwrap(),
5619 );
5620 #[cfg(feature = "orchard")]
5622 let shard2_ironwood_frontier = Frontier::empty();
5623
5624 let target_chain_state = ChainState::new(
5625 target_height,
5626 BlockHash([7; 32]),
5627 shard2_sapling_frontier,
5628 #[cfg(feature = "orchard")]
5629 shard2_orchard_frontier,
5630 #[cfg(feature = "orchard")]
5631 shard2_ironwood_frontier,
5632 );
5633
5634 let pre_truncation_tip = st
5636 .wallet()
5637 .chain_height()
5638 .unwrap()
5639 .expect("chain tip should be set");
5640 assert!(pre_truncation_tip > target_height);
5641
5642 st.wallet_mut()
5647 .truncate_to_chain_state(target_chain_state)
5648 .expect("truncate_to_chain_state above max scanned should succeed");
5649
5650 let post_truncation_tip = st
5652 .wallet()
5653 .chain_height()
5654 .unwrap()
5655 .expect("chain tip should still be set after truncation");
5656 assert_eq!(
5657 post_truncation_tip, target_height,
5658 "scan queue should be trimmed to target height, not extend to the old chain tip"
5659 );
5660
5661 assert!(
5663 st.wallet().get_block_hash(max_scanned).unwrap().is_some(),
5664 "blocks at max_scanned should be preserved"
5665 );
5666}
5667
5668pub fn rewind_to_chain_state_deep<T: ShieldedPoolTester, Dsf>(
5669 ds_factory: Dsf,
5670 cache: impl TestCache,
5671) where
5672 Dsf: DataStoreFactory,
5673 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
5674{
5675 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
5687
5688 let sapling_activation = st
5689 .network()
5690 .activation_height(consensus::NetworkUpgrade::Sapling)
5691 .unwrap();
5692
5693 let seed = [1u8; 32];
5696 let other_fvk = T::sk_to_fvk(&T::sk(&seed));
5697
5698 let initial_block_count = 8u32;
5699 for _ in 0..initial_block_count {
5700 st.generate_next_block(
5701 &other_fvk,
5702 AddressType::DefaultExternal,
5703 Zatoshis::const_from_u64(10000),
5704 );
5705 }
5706 st.scan_cached_blocks(sapling_activation, initial_block_count as usize);
5707
5708 let rewind_target = sapling_activation + initial_block_count - 1;
5710
5711 let extra_blocks = PRUNING_DEPTH + 10;
5714 for _ in 0..extra_blocks {
5715 st.generate_next_block(
5716 &other_fvk,
5717 AddressType::DefaultExternal,
5718 Zatoshis::const_from_u64(5000),
5719 );
5720 }
5721 st.scan_cached_blocks(rewind_target + 1, extra_blocks as usize);
5722
5723 let pre_rewind_tip = st
5724 .wallet()
5725 .chain_height()
5726 .unwrap()
5727 .expect("chain tip should be set");
5728 assert!(
5729 pre_rewind_tip > rewind_target + PRUNING_DEPTH,
5730 "tip should be strictly beyond pruning depth from the rewind target"
5731 );
5732
5733 let prune_boundary = pre_rewind_tip - (PRUNING_DEPTH - 1);
5736 let boundary_hash_before = st
5737 .wallet()
5738 .get_block_hash(prune_boundary)
5739 .unwrap()
5740 .expect("block at prune boundary should be present before rewind");
5741
5742 st.wallet_mut()
5744 .rewind_to_chain_state(
5745 ChainState::empty(rewind_target, BlockHash([0; 32])),
5746 HashSet::new(),
5747 )
5748 .expect("rewind_to_chain_state should succeed for a deep target");
5749
5750 let new_tip = st
5754 .wallet()
5755 .chain_height()
5756 .unwrap()
5757 .expect("chain tip should still be set after rewind");
5758 assert_eq!(new_tip, pre_rewind_tip);
5759
5760 let wallet = st.wallet();
5765 assert_eq!(
5766 wallet.get_block_hash(prune_boundary).unwrap(),
5767 Some(boundary_hash_before),
5768 "block hash at (tip - (PRUNING_DEPTH - 1)) should be preserved unchanged by a deep rewind"
5769 );
5770 assert!(
5771 wallet.get_block_hash(prune_boundary + 1).unwrap().is_none(),
5772 "block entries above (tip - (PRUNING_DEPTH - 1)) must be removed by a deep rewind"
5773 );
5774 assert!(
5775 wallet.get_block_hash(pre_rewind_tip).unwrap().is_none(),
5776 "block entries up to the pre-rewind tip must be removed by a deep rewind"
5777 );
5778 assert_eq!(
5779 wallet
5780 .block_max_scanned()
5781 .unwrap()
5782 .map(|m| m.block_height()),
5783 Some(prune_boundary),
5784 "block_max_scanned should equal (tip - (PRUNING_DEPTH - 1)) after a deep rewind"
5785 );
5786}
5787
5788pub fn rewind_to_chain_state_shallow<T: ShieldedPoolTester, Dsf>(
5789 ds_factory: Dsf,
5790 cache: impl TestCache,
5791) where
5792 Dsf: DataStoreFactory,
5793 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
5794{
5795 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
5806
5807 let sapling_activation = st
5808 .network()
5809 .activation_height(consensus::NetworkUpgrade::Sapling)
5810 .unwrap();
5811
5812 let seed = [1u8; 32];
5813 let other_fvk = T::sk_to_fvk(&T::sk(&seed));
5814
5815 let initial_block_count = 8u32;
5816 for _ in 0..initial_block_count {
5817 st.generate_next_block(
5818 &other_fvk,
5819 AddressType::DefaultExternal,
5820 Zatoshis::const_from_u64(10000),
5821 );
5822 }
5823 st.scan_cached_blocks(sapling_activation, initial_block_count as usize);
5824
5825 let rewind_target = sapling_activation + initial_block_count - 1;
5826
5827 let extra_blocks = PRUNING_DEPTH - 1;
5831 for _ in 0..extra_blocks {
5832 st.generate_next_block(
5833 &other_fvk,
5834 AddressType::DefaultExternal,
5835 Zatoshis::const_from_u64(5000),
5836 );
5837 }
5838 st.scan_cached_blocks(rewind_target + 1, extra_blocks as usize);
5839
5840 let tip = st
5841 .wallet()
5842 .chain_height()
5843 .unwrap()
5844 .expect("chain tip should be set");
5845 assert_eq!(
5846 tip,
5847 rewind_target + (PRUNING_DEPTH - 1),
5848 "tip should be exactly at the shallow boundary from the rewind target"
5849 );
5850
5851 let target_hash_before = st
5852 .wallet()
5853 .get_block_hash(rewind_target)
5854 .unwrap()
5855 .expect("block at the rewind target should be present before rewind");
5856
5857 st.wallet_mut()
5858 .rewind_to_chain_state(
5859 ChainState::empty(rewind_target, BlockHash([0; 32])),
5860 HashSet::new(),
5861 )
5862 .expect("rewind_to_chain_state should succeed for a shallow target");
5863
5864 let new_tip = st
5868 .wallet()
5869 .chain_height()
5870 .unwrap()
5871 .expect("chain tip should still be set after rewind");
5872 assert_eq!(new_tip, tip);
5873
5874 let wallet = st.wallet();
5878 assert_eq!(
5879 wallet.get_block_hash(rewind_target).unwrap(),
5880 Some(target_hash_before),
5881 "block hash at the rewind target should be preserved unchanged"
5882 );
5883 assert!(
5884 wallet.get_block_hash(rewind_target + 1).unwrap().is_none(),
5885 "block entries above the rewind target should be removed by a shallow rewind"
5886 );
5887 assert_eq!(
5888 wallet
5889 .block_max_scanned()
5890 .unwrap()
5891 .map(|m| m.block_height()),
5892 Some(rewind_target),
5893 "block_max_scanned should equal the rewind target after a shallow rewind"
5894 );
5895}
5896
5897pub fn rewind_after_non_contiguous_scan<T: ShieldedPoolTester, Dsf>(
5898 ds_factory: Dsf,
5899 cache: impl TestCache,
5900) where
5901 Dsf: DataStoreFactory,
5902 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
5903{
5904 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
5913
5914 let sapling_activation = st
5915 .network()
5916 .activation_height(consensus::NetworkUpgrade::Sapling)
5917 .unwrap();
5918
5919 let seed = [1u8; 32];
5923 let other_fvk = T::sk_to_fvk(&T::sk(&seed));
5924 let filler_value = Zatoshis::const_from_u64(10_000);
5925
5926 let low_count: u32 = 10;
5927 let gap_size: u32 = PRUNING_DEPTH + 5; let high_count: u32 = 10;
5929 let total_generated = low_count + gap_size + high_count;
5930
5931 for _ in 0..total_generated {
5932 st.generate_next_block(&other_fvk, AddressType::DefaultExternal, filler_value);
5933 }
5934
5935 let low_start = sapling_activation;
5936 let low_end_inclusive = low_start + low_count - 1;
5937 let high_start = low_end_inclusive + gap_size + 1;
5938
5939 st.scan_cached_blocks(low_start, low_count as usize);
5941
5942 st.scan_cached_blocks(high_start, high_count as usize);
5947
5948 let max_scanned_height = st
5949 .wallet()
5950 .block_max_scanned()
5951 .unwrap()
5952 .map(|m| m.block_height())
5953 .expect("block_max_scanned should report the high-range tip");
5954 let high_end_inclusive = high_start + high_count - 1;
5955 assert_eq!(max_scanned_height, high_end_inclusive);
5956 let pd_floor = max_scanned_height - (PRUNING_DEPTH - 1);
5957 assert!(
5958 pd_floor > low_end_inclusive && pd_floor < high_start,
5959 "test invariant: PD floor must lie in the unscanned gap (got {pd_floor}, \
5960 gap is ({low_end_inclusive}, {high_start}))"
5961 );
5962
5963 st.wallet_mut()
5967 .rewind_to_chain_state(
5968 ChainState::empty(low_end_inclusive, BlockHash([0; 32])),
5969 HashSet::new(),
5970 )
5971 .expect("rewind_to_chain_state should succeed across a non-contiguous scan");
5972}
5973
5974pub fn stabilized_note_spendable_after_deep_rewind<T, Dsf>(ds_factory: Dsf, cache: impl TestCache)
5977where
5978 T: ShieldedPoolTester,
5979 Dsf: DataStoreFactory,
5980 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
5981{
5982 const SHARD_HEIGHT: u32 = 16;
6013 const SHARD_POSITIONS: u32 = 1 << SHARD_HEIGHT; let initial_tree_size: u32 = 2 * SHARD_POSITIONS - 1;
6017
6018 let mut st = TestBuilder::new()
6019 .with_data_store_factory(ds_factory)
6020 .with_block_cache(cache)
6021 .with_initial_chain_state(|rng, network| {
6022 let birthday_height = network.activation_height(NetworkUpgrade::Nu5).unwrap() + 1000;
6030
6031 let (prior_sapling_roots, sapling_initial_tree) =
6032 Frontier::random_with_prior_subtree_roots(
6033 rng,
6034 initial_tree_size.into(),
6035 NonZeroU8::new(SHARD_HEIGHT as u8).unwrap(),
6036 );
6037 let prior_sapling_roots = prior_sapling_roots
6039 .into_iter()
6040 .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 500, root))
6041 .collect::<Vec<_>>();
6042
6043 #[cfg(feature = "orchard")]
6044 let (prior_orchard_roots, orchard_initial_tree) =
6045 Frontier::random_with_prior_subtree_roots(
6046 rng,
6047 initial_tree_size.into(),
6048 NonZeroU8::new(SHARD_HEIGHT as u8).unwrap(),
6049 );
6050 #[cfg(feature = "orchard")]
6051 let prior_orchard_roots = prior_orchard_roots
6052 .into_iter()
6053 .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 500, root))
6054 .collect::<Vec<_>>();
6055
6056 #[cfg(feature = "orchard")]
6058 let ironwood_initial_tree = Frontier::empty();
6059
6060 InitialChainState {
6061 chain_state: ChainState::new(
6062 birthday_height - 1,
6063 BlockHash([5; 32]),
6064 sapling_initial_tree,
6065 #[cfg(feature = "orchard")]
6066 orchard_initial_tree,
6067 #[cfg(feature = "orchard")]
6068 ironwood_initial_tree,
6069 ),
6070 prior_sapling_roots,
6071 #[cfg(feature = "orchard")]
6072 prior_orchard_roots,
6073 }
6074 })
6075 .with_account_having_current_birthday()
6076 .build();
6077
6078 let dfvk = T::test_account_fvk(&st);
6079 let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32]));
6080 let filler_value = Zatoshis::const_from_u64(1000);
6081
6082 let note_values = [
6087 Zatoshis::const_from_u64(100_000),
6088 Zatoshis::const_from_u64(200_000),
6089 Zatoshis::const_from_u64(150_000),
6090 ];
6091 let total_note_value = note_values.iter().sum::<Option<Zatoshis>>().unwrap();
6092 let note_tree_positions: [u32; 3] = [
6094 2 * SHARD_POSITIONS, 2 * SHARD_POSITIONS + SHARD_POSITIONS / 2, 3 * SHARD_POSITIONS - 1, ];
6098
6099 let scan_block_size: u32 = SHARD_POSITIONS + 1; let first_scanned_position: u32 = initial_tree_size; let mut outputs = Vec::with_capacity(scan_block_size as usize);
6102 let mut next_wallet_ix = 0;
6103 for offset in 0..scan_block_size {
6104 let tree_pos = first_scanned_position + offset;
6105 if next_wallet_ix < note_tree_positions.len()
6106 && tree_pos == note_tree_positions[next_wallet_ix]
6107 {
6108 outputs.push(FakeCompactOutput::new(
6109 dfvk.clone(),
6110 AddressType::DefaultExternal,
6111 note_values[next_wallet_ix],
6112 ));
6113 next_wallet_ix += 1;
6114 } else {
6115 outputs.push(FakeCompactOutput::new(
6116 not_our_key.clone(),
6117 AddressType::DefaultExternal,
6118 filler_value,
6119 ));
6120 }
6121 }
6122 let (note_height, _, _) = st.generate_next_block_multi(&outputs);
6123 st.scan_cached_blocks(note_height, 1);
6124
6125 let birthday_height = st
6129 .wallet()
6130 .get_wallet_birthday()
6131 .unwrap()
6132 .expect("account birthday should be set");
6133 let rewind_target = birthday_height - 100;
6134
6135 let shard_2_root = T::shard_root(&mut st, 2).unwrap();
6140 T::put_subtree_roots(
6141 &mut st,
6142 2,
6143 &[CommitmentTreeRoot::from_parts(note_height, shard_2_root)],
6144 )
6145 .unwrap();
6146
6147 let extra_blocks = PRUNING_DEPTH + 10;
6151 for _ in 0..extra_blocks {
6152 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, filler_value);
6153 }
6154 st.scan_cached_blocks(note_height + 1, extra_blocks as usize);
6155
6156 let account = st.test_account().unwrap().clone();
6157
6158 st.wallet_mut()
6164 .rewind_to_chain_state(
6165 ChainState::empty(rewind_target, BlockHash([0; 32])),
6166 HashSet::from([account.id()]),
6167 )
6168 .expect("rewind_to_chain_state should succeed");
6169
6170 assert_eq!(
6173 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
6174 total_note_value,
6175 "all stabilized notes should remain spendable after deep rewind"
6176 );
6177
6178 let to_extsk = T::sk(&[0xcc; 32]);
6180 let to: Address = T::sk_default_address(&to_extsk);
6181 let send_value = Zatoshis::const_from_u64(10_000);
6182 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
6183 to.to_zcash_address(st.network()),
6184 send_value,
6185 )])
6186 .unwrap();
6187 let change_strategy =
6188 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
6189 let input_selector = GreedyInputSelector::new();
6190 let proposal = st
6191 .propose_transfer(
6192 account.id(),
6193 &input_selector,
6194 &change_strategy,
6195 request,
6196 ConfirmationsPolicy::MIN,
6197 )
6198 .expect("proposal should succeed with stabilized note after deep rewind");
6199 let txids = st
6200 .create_proposed_transactions::<std::convert::Infallible, _, std::convert::Infallible, _>(
6201 account.usk(),
6202 OvkPolicy::Sender,
6203 &proposal,
6204 )
6205 .expect("spend construction should succeed");
6206 assert_eq!(
6207 txids.len(),
6208 1,
6209 "the spend should produce exactly one transaction"
6210 );
6211}
6212
6213pub fn newly_discovered_notes_become_stabilized<T, Dsf>(ds_factory: Dsf, cache: impl TestCache)
6219where
6220 T: ShieldedPoolTester,
6221 Dsf: DataStoreFactory,
6222 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
6223{
6224 const SHARD_HEIGHT: u32 = 16;
6260 const SHARD_POSITIONS: u32 = 1 << SHARD_HEIGHT; const TEST_SEED: [u8; 32] = [0u8; 32];
6268
6269 let initial_tree_size: u32 = 2 * SHARD_POSITIONS - 1;
6275
6276 let mut st = TestBuilder::new()
6277 .with_data_store_factory(ds_factory)
6278 .with_block_cache(cache)
6279 .with_initial_chain_state(|rng, network| {
6280 let birthday_height = network.activation_height(NetworkUpgrade::Nu5).unwrap() + 1000;
6288
6289 let (prior_sapling_roots, sapling_initial_tree) =
6290 Frontier::random_with_prior_subtree_roots(
6291 rng,
6292 initial_tree_size.into(),
6293 NonZeroU8::new(SHARD_HEIGHT as u8).unwrap(),
6294 );
6295 let prior_sapling_roots = prior_sapling_roots
6301 .into_iter()
6302 .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 500, root))
6303 .collect::<Vec<_>>();
6304
6305 #[cfg(feature = "orchard")]
6306 let (prior_orchard_roots, orchard_initial_tree) =
6307 Frontier::random_with_prior_subtree_roots(
6308 rng,
6309 initial_tree_size.into(),
6310 NonZeroU8::new(SHARD_HEIGHT as u8).unwrap(),
6311 );
6312 #[cfg(feature = "orchard")]
6313 let prior_orchard_roots = prior_orchard_roots
6314 .into_iter()
6315 .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 500, root))
6316 .collect::<Vec<_>>();
6317
6318 #[cfg(feature = "orchard")]
6320 let ironwood_initial_tree = Frontier::empty();
6321
6322 InitialChainState {
6323 chain_state: ChainState::new(
6324 birthday_height - 1,
6325 BlockHash([5; 32]),
6326 sapling_initial_tree,
6327 #[cfg(feature = "orchard")]
6328 orchard_initial_tree,
6329 #[cfg(feature = "orchard")]
6330 ironwood_initial_tree,
6331 ),
6332 prior_sapling_roots,
6333 #[cfg(feature = "orchard")]
6334 prior_orchard_roots,
6335 }
6336 })
6337 .with_account_having_current_birthday()
6338 .build();
6339
6340 let dfvk_a = T::test_account_fvk(&st);
6341 let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32]));
6342 let filler_value = Zatoshis::const_from_u64(1000);
6343
6344 let zip32_index_b = zip32::AccountId::ZERO.next().unwrap();
6348 let usk_b = UnifiedSpendingKey::from_seed(st.network(), &TEST_SEED, zip32_index_b)
6349 .expect("account B USK derivation from seed should succeed");
6350 let fvk_b = T::sk_to_fvk(T::usk_to_sk(&usk_b));
6351
6352 let a_positions: [u32; 3] = [
6356 2 * SHARD_POSITIONS, 2 * SHARD_POSITIONS + SHARD_POSITIONS / 2, 3 * SHARD_POSITIONS - 1, ];
6360 let b_positions: [u32; 3] = [
6361 2 * SHARD_POSITIONS + 1, 2 * SHARD_POSITIONS + SHARD_POSITIONS / 2 + 1, 3 * SHARD_POSITIONS - 2, ];
6365 let a_values = [
6366 Zatoshis::const_from_u64(100_000),
6367 Zatoshis::const_from_u64(200_000),
6368 Zatoshis::const_from_u64(150_000),
6369 ];
6370 let b_values = [
6371 Zatoshis::const_from_u64(70_000),
6372 Zatoshis::const_from_u64(80_000),
6373 Zatoshis::const_from_u64(90_000),
6374 ];
6375 let total_a = a_values.iter().sum::<Option<Zatoshis>>().unwrap();
6376 let total_b = b_values.iter().sum::<Option<Zatoshis>>().unwrap();
6377
6378 let scan_block_size: u32 = SHARD_POSITIONS + 1;
6381 let first_scanned_position: u32 = initial_tree_size;
6382 let mut outputs = Vec::with_capacity(scan_block_size as usize);
6383 for offset in 0..scan_block_size {
6384 let tree_pos = first_scanned_position + offset;
6385 let output = if let Some(ix) = a_positions.iter().position(|&p| p == tree_pos) {
6386 FakeCompactOutput::new(dfvk_a.clone(), AddressType::DefaultExternal, a_values[ix])
6387 } else if let Some(ix) = b_positions.iter().position(|&p| p == tree_pos) {
6388 FakeCompactOutput::new(fvk_b.clone(), AddressType::DefaultExternal, b_values[ix])
6389 } else {
6390 FakeCompactOutput::new(
6391 not_our_key.clone(),
6392 AddressType::DefaultExternal,
6393 filler_value,
6394 )
6395 };
6396 outputs.push(output);
6397 }
6398 let (note_height, _, _) = st.generate_next_block_multi(&outputs);
6399
6400 let extra_blocks = PRUNING_DEPTH + 10;
6403 for _ in 0..extra_blocks {
6404 st.generate_next_block(¬_our_key, AddressType::DefaultExternal, filler_value);
6405 }
6406
6407 st.scan_cached_blocks(note_height, 1);
6414 let shard_2_root = T::shard_root(&mut st, 2).unwrap();
6415 T::put_subtree_roots(
6416 &mut st,
6417 2,
6418 &[CommitmentTreeRoot::from_parts(note_height, shard_2_root)],
6419 )
6420 .unwrap();
6421 st.scan_cached_blocks(note_height + 1, extra_blocks as usize);
6422
6423 let account_a = st.test_account().unwrap().clone();
6424
6425 assert_eq!(
6428 st.get_spendable_balance(account_a.id(), ConfirmationsPolicy::MIN),
6429 total_a,
6430 "A's three notes must be spendable after the initial scan + stabilization",
6431 );
6432
6433 let b_birthday = account_a.birthday().clone();
6437 let seed = Secret::new(TEST_SEED.to_vec());
6438 let (account_b, _usk_b) = st
6439 .wallet_mut()
6440 .import_account_hd("account B", &seed, zip32_index_b, &b_birthday, None)
6441 .expect("account B import should succeed");
6442
6443 st.scan_cached_blocks(note_height, (1 + extra_blocks) as usize);
6450
6451 assert_eq!(
6452 st.get_spendable_balance(account_b.id(), ConfirmationsPolicy::MIN),
6453 total_b,
6454 "B's three newly-discovered notes must be spendable after the re-scan",
6455 );
6456 assert_eq!(
6457 st.get_spendable_balance(account_a.id(), ConfirmationsPolicy::MIN),
6458 total_a,
6459 "A's notes must remain spendable across B's import and re-scan",
6460 );
6461
6462 let rewind_target = account_a.birthday().height() - 100;
6469 st.wallet_mut()
6470 .rewind_to_chain_state(
6471 ChainState::empty(rewind_target, BlockHash([0; 32])),
6472 HashSet::from([account_a.id(), account_b.id()]),
6473 )
6474 .expect("rewind_to_chain_state should succeed");
6475
6476 assert_eq!(
6477 st.get_spendable_balance(account_a.id(), ConfirmationsPolicy::MIN),
6478 total_a,
6479 "A's notes must survive the deep rewind, confirming they were and \
6480 remain witness_stabilized",
6481 );
6482 assert_eq!(
6483 st.get_spendable_balance(account_b.id(), ConfirmationsPolicy::MIN),
6484 total_b,
6485 "B's three re-scan-discovered notes must survive the deep rewind, \
6486 confirming that mark_stabilized_notes fired on the freshly-inserted \
6487 B rows during the re-scan",
6488 );
6489}
6490
6491pub fn reorg_to_checkpoint<T: ShieldedPoolTester, Dsf, C>(ds_factory: Dsf, cache: C)
6492where
6493 Dsf: DataStoreFactory,
6494 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
6495 C: TestCache,
6496{
6497 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
6498
6499 let account = st.test_account().cloned().unwrap();
6500
6501 let p0_fvk = T::random_fvk(st.rng_mut());
6503 let gen_random_block = |st: &mut TestState<C, Dsf::DataStore, LocalNetwork>,
6504 output_count: usize| {
6505 let fake_outputs =
6506 std::iter::repeat_with(|| FakeCompactOutput::random(st.rng_mut(), p0_fvk.clone()))
6507 .take(output_count)
6508 .collect::<Vec<_>>();
6509 st.generate_next_block_multi(&fake_outputs[..]);
6510 output_count
6511 };
6512
6513 for _ in 0..10 {
6515 gen_random_block(&mut st, 4);
6516 }
6517
6518 let reorg_height = account.birthday().height() + 4;
6520 let reorg_position = Position::from(19);
6521
6522 st.scan_cached_blocks(account.birthday().height(), 5);
6525 assert_eq!(
6526 st.wallet()
6527 .block_max_scanned()
6528 .unwrap()
6529 .unwrap()
6530 .block_height(),
6531 reorg_height
6532 );
6533
6534 let checkpoints = st
6537 .wallet()
6538 .get_checkpoint_history(&T::SHIELDED_PROTOCOL)
6539 .unwrap();
6540 assert_eq!(checkpoints.len(), 6);
6541 assert_eq!(
6542 checkpoints.last(),
6543 Some(&(reorg_height, Some(reorg_position)))
6544 );
6545
6546 st.scan_cached_blocks(reorg_height + 1, 1);
6548 assert_eq!(
6549 st.wallet()
6550 .block_max_scanned()
6551 .unwrap()
6552 .unwrap()
6553 .block_height(),
6554 reorg_height + 1
6555 );
6556 let checkpoints = st
6557 .wallet()
6558 .get_checkpoint_history(&T::SHIELDED_PROTOCOL)
6559 .unwrap();
6560 assert_eq!(checkpoints.len(), 7);
6561 assert_eq!(
6562 checkpoints.last(),
6563 Some(&(reorg_height + 1, Some(reorg_position + 4)))
6564 );
6565
6566 st.truncate_to_height_retaining_cache(reorg_height);
6572
6573 let checkpoints = st
6581 .wallet()
6582 .get_checkpoint_history(&T::SHIELDED_PROTOCOL)
6583 .unwrap();
6584 assert_eq!(checkpoints.len(), 6);
6585 assert_eq!(
6586 checkpoints.last(),
6587 Some(&(reorg_height, Some(reorg_position)))
6588 );
6589
6590 st.scan_cached_blocks(reorg_height + 2, 1);
6592
6593 let checkpoints = st
6599 .wallet()
6600 .get_checkpoint_history(&T::SHIELDED_PROTOCOL)
6601 .unwrap();
6602 assert_eq!(checkpoints.len(), 8);
6605 assert_eq!(
6606 checkpoints.last(),
6607 Some(&(reorg_height + 2, Some(reorg_position + 8)))
6608 );
6609
6610 st.truncate_to_height(reorg_height);
6613
6614 for _ in 0..10 {
6616 let output_count = st.rng_mut().random_range(2..10);
6617 gen_random_block(&mut st, output_count);
6618 }
6619
6620 st.scan_cached_blocks(reorg_height + 1, 1);
6625}
6626
6627pub fn scan_cached_blocks_allows_blocks_out_of_order<T: ShieldedPoolTester>(
6628 ds_factory: impl DataStoreFactory,
6629 cache: impl TestCache,
6630) {
6631 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
6632
6633 let account = st.test_account().cloned().unwrap();
6634 let dfvk = T::test_account_fvk(&st);
6635
6636 let value = Zatoshis::const_from_u64(50000);
6637 let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
6638 st.scan_cached_blocks(h1, 1);
6639 assert_eq!(st.get_total_balance(account.id()), value);
6640
6641 let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
6643 let (h3, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
6644
6645 st.scan_cached_blocks(h3, 1);
6647
6648 st.scan_cached_blocks(h2, 1);
6650 assert_eq!(
6651 st.get_total_balance(account.id()),
6652 Zatoshis::const_from_u64(150_000)
6653 );
6654
6655 let req = TransactionRequest::new(vec![Payment::without_memo(
6657 T::fvk_default_address(&dfvk).to_zcash_address(st.network()),
6658 Zatoshis::const_from_u64(110_000),
6659 )])
6660 .unwrap();
6661
6662 let input_selector = GreedyInputSelector::new();
6663 let change_strategy =
6664 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
6665
6666 assert_matches!(
6667 st.spend(
6668 &input_selector,
6669 &change_strategy,
6670 account.usk(),
6671 req,
6672 OvkPolicy::Sender,
6673 ConfirmationsPolicy::MIN,
6674 ),
6675 Ok(_)
6676 );
6677}
6678
6679pub fn scan_cached_blocks_finds_received_notes<T: ShieldedPoolTester, Dsf>(
6680 ds_factory: Dsf,
6681 cache: impl TestCache,
6682) where
6683 Dsf: DataStoreFactory,
6684 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
6685{
6686 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
6687
6688 let account = st.test_account().cloned().unwrap();
6689 let dfvk = T::test_account_fvk(&st);
6690
6691 assert_eq!(st.get_wallet_summary(ConfirmationsPolicy::MIN), None);
6693
6694 let value = Zatoshis::const_from_u64(50000);
6696 let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
6697
6698 let summary = st.scan_cached_blocks(h1, 1);
6700 assert_eq!(summary.scanned_range().start, h1);
6701 assert_eq!(summary.scanned_range().end, h1 + 1);
6702 assert_eq!(T::received_note_count(&summary), 1);
6703
6704 assert_eq!(st.get_total_balance(account.id()), value);
6706
6707 let value2 = Zatoshis::const_from_u64(70000);
6709 let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value2);
6710
6711 let summary = st.scan_cached_blocks(h2, 1);
6713 assert_eq!(summary.scanned_range().start, h2);
6714 assert_eq!(summary.scanned_range().end, h2 + 1);
6715 assert_eq!(T::received_note_count(&summary), 1);
6716
6717 assert_eq!(
6719 st.get_total_balance(account.id()),
6720 (value + value2).unwrap()
6721 );
6722}
6723
6724pub fn scan_cached_blocks_finds_change_notes<T: ShieldedPoolTester, Dsf>(
6725 ds_factory: Dsf,
6726 cache: impl TestCache,
6727) where
6728 Dsf: DataStoreFactory,
6729 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
6730{
6731 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
6732
6733 let account = st.test_account().cloned().unwrap();
6734 let dfvk = T::test_account_fvk(&st);
6735
6736 assert_eq!(st.get_wallet_summary(ConfirmationsPolicy::MIN), None);
6738
6739 let value = Zatoshis::const_from_u64(50000);
6741 let (_, _, nf) = st.add_a_single_note_checking_balance(value);
6742
6743 let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32]));
6745 let to2 = T::fvk_default_address(¬_our_key);
6746 let value2 = Zatoshis::const_from_u64(20000);
6747 let (spent_height, _) = st.generate_next_block_spending(&dfvk, (nf, value), to2, value2);
6748
6749 st.scan_cached_blocks(spent_height, 1);
6751
6752 assert_eq!(
6754 st.get_total_balance(account.id()),
6755 (value - value2).unwrap()
6756 );
6757}
6758
6759pub fn scan_cached_blocks_detects_spends_out_of_order<T: ShieldedPoolTester, Dsf>(
6760 ds_factory: Dsf,
6761 cache: impl TestCache,
6762) where
6763 Dsf: DataStoreFactory,
6764 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
6765{
6766 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
6767
6768 let account = st.test_account().cloned().unwrap();
6769 let dfvk = T::test_account_fvk(&st);
6770
6771 assert_eq!(st.get_wallet_summary(ConfirmationsPolicy::MIN), None);
6773
6774 let value = Zatoshis::const_from_u64(50000);
6776 let (received_height, _, nf) =
6777 st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
6778
6779 let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32]));
6781 let to2 = T::fvk_default_address(¬_our_key);
6782 let value2 = Zatoshis::const_from_u64(20000);
6783 let (spent_height, _) = st.generate_next_block_spending(&dfvk, (nf, value), to2, value2);
6784
6785 st.scan_cached_blocks(spent_height, 1);
6787
6788 assert_eq!(
6790 st.get_total_balance(account.id()),
6791 (value - value2).unwrap()
6792 );
6793
6794 st.scan_cached_blocks(received_height, 1);
6796
6797 assert_eq!(
6799 st.get_total_balance(account.id()),
6800 (value - value2).unwrap()
6801 );
6802}
6803
6804pub fn oldest_note_is_selected_first<T: ShieldedPoolTester, Dsf>(
6810 ds_factory: Dsf,
6811 cache: impl TestCache,
6812) where
6813 Dsf: DataStoreFactory,
6814{
6815 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
6816 let account = st.test_account().cloned().unwrap();
6817 let dfvk = T::test_account_fvk(&st);
6818
6819 let older_value = Zatoshis::const_from_u64(500_000);
6822 let newer_value = Zatoshis::const_from_u64(300_000);
6823 let (older_height, _, _) =
6824 st.generate_next_block(&dfvk, AddressType::DefaultExternal, older_value);
6825 let (newer_height, _, _) =
6826 st.generate_next_block(&dfvk, AddressType::DefaultExternal, newer_value);
6827
6828 st.scan_cached_blocks(newer_height, 1);
6831 st.scan_cached_blocks(older_height, 1);
6832
6833 let selected = T::select_spendable_notes(
6835 &st,
6836 account.id(),
6837 TargetValue::AtLeast(Zatoshis::const_from_u64(200_000)),
6838 TargetHeight::from(newer_height + 1),
6839 ConfirmationsPolicy::MIN,
6840 &[],
6841 )
6842 .unwrap();
6843 assert_eq!(selected.len(), 1, "a single note covers the target");
6844 assert_eq!(
6845 T::note_value(selected.first().expect("nonempty").note()),
6846 older_value,
6847 "the oldest sufficient note must be drawn first, not the first-discovered"
6848 );
6849}
6850
6851pub fn metadata_queries_exclude_unwanted_notes<T: ShieldedPoolTester, Dsf, TC>(
6852 ds_factory: Dsf,
6853 cache: TC,
6854) where
6855 Dsf: DataStoreFactory,
6856 <Dsf as DataStoreFactory>::AccountId: std::fmt::Debug,
6857 TC: TestCache,
6858{
6859 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
6860
6861 let note_values = (1..=10)
6863 .map(|i| Zatoshis::const_from_u64(i * 100_0000))
6864 .collect::<Vec<_>>();
6865 let h0 = st
6866 .add_notes_checking_balance(note_values.clone().into_iter().map(Some))
6867 .first_block_height()
6868 .unwrap();
6869
6870 let target_height = TargetHeight::from(h0 + 10);
6871 let account = st.test_account().cloned().unwrap();
6872 let test_meta = |st: &TestState<TC, Dsf::DataStore, LocalNetwork>, query, expected_count| {
6873 let metadata = st
6874 .wallet()
6875 .get_account_metadata(
6876 account.id(),
6877 &query,
6878 target_height,
6879 &[],
6880 LockFilter::Unfiltered,
6881 )
6882 .unwrap();
6883
6884 assert_eq!(metadata.note_count(T::SHIELDED_PROTOCOL), expected_count);
6885 };
6886
6887 test_meta(
6888 &st,
6889 NoteFilter::ExceedsMinValue(Zatoshis::const_from_u64(1000_0000)),
6890 Some(0),
6891 );
6892 test_meta(
6893 &st,
6894 NoteFilter::ExceedsMinValue(Zatoshis::const_from_u64(500_0000)),
6895 Some(5),
6896 );
6897 test_meta(
6898 &st,
6899 NoteFilter::ExceedsBalancePercentage(BoundedU8::new_const(10)),
6900 Some(5),
6901 );
6902
6903 test_meta(
6905 &st,
6906 NoteFilter::ExceedsPriorSendPercentile(BoundedU8::new_const(50)),
6907 None,
6908 );
6909
6910 let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32]));
6915 let to = T::fvk_default_address(¬_our_key).to_zcash_address(st.network());
6916 let nz2 = NonZeroU64::new(2).unwrap();
6917
6918 for value in ¬e_values {
6919 let txids = st
6920 .create_standard_transaction(&account, to.clone(), *value / nz2)
6921 .unwrap();
6922 st.generate_next_block_including(txids.head);
6923 }
6924 st.scan_cached_blocks(h0 + 10, 10);
6925
6926 test_meta(
6930 &st,
6931 NoteFilter::ExceedsPriorSendPercentile(BoundedU8::new_const(50)),
6932 Some(5),
6933 );
6934}
6935
6936#[cfg(feature = "pczt")]
6937pub fn pczt_single_step<P0: ShieldedPoolTester, P1: ShieldedPoolTester, Dsf>(
6938 ds_factory: Dsf,
6939 cache: impl TestCache,
6940 pin_expiry_above_target: Option<u32>,
6941) where
6942 Dsf: DataStoreFactory,
6943 <Dsf as DataStoreFactory>::AccountId: serde::Serialize + serde::de::DeserializeOwned,
6944{
6945 let mut st = TestBuilder::new()
6946 .with_data_store_factory(ds_factory)
6947 .with_block_cache(cache)
6948 .with_initial_chain_state(|_, network| {
6949 let birthday_height = std::cmp::max(
6951 network.activation_height(NetworkUpgrade::Nu5).unwrap(),
6952 network.activation_height(NetworkUpgrade::Canopy).unwrap() + ZIP212_GRACE_PERIOD,
6953 );
6954
6955 #[cfg(feature = "orchard")]
6957 let ironwood_initial_tree = Frontier::empty();
6958
6959 InitialChainState {
6960 chain_state: ChainState::new(
6961 birthday_height - 1,
6962 BlockHash([5; 32]),
6963 Frontier::empty(),
6964 #[cfg(feature = "orchard")]
6965 Frontier::empty(),
6966 #[cfg(feature = "orchard")]
6967 ironwood_initial_tree,
6968 ),
6969 prior_sapling_roots: vec![],
6970 #[cfg(feature = "orchard")]
6971 prior_orchard_roots: vec![],
6972 }
6973 })
6974 .with_account_having_current_birthday()
6975 .build();
6976
6977 let account = st.test_account().cloned().unwrap();
6978
6979 let p0_fvk = P0::test_account_fvk(&st);
6980
6981 let p1_fvk = P1::test_account_fvk(&st);
6982 let p1_to = P1::fvk_default_address(&p1_fvk);
6983
6984 let note_value = Zatoshis::const_from_u64(350000);
6986 st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value);
6987 st.scan_cached_blocks(account.birthday().height(), 1);
6988
6989 assert_eq!(st.get_total_balance(account.id()), note_value);
6990 assert_eq!(
6991 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
6992 note_value
6993 );
6994
6995 let transfer_amount = Zatoshis::const_from_u64(200000);
6996 let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo(
6997 p1_to.to_zcash_address(st.network()),
6998 transfer_amount,
6999 )])
7000 .unwrap();
7001
7002 let input_selector = GreedyInputSelector::new();
7003 let change_strategy =
7004 single_output_change_strategy(StandardFeeRule::Zip317, None, P0::SHIELDED_PROTOCOL);
7005 let proposal0 = st
7006 .propose_transfer(
7007 account.id(),
7008 &input_selector,
7009 &change_strategy,
7010 p0_to_p1,
7011 ConfirmationsPolicy::MIN,
7012 )
7013 .unwrap();
7014
7015 let min_target_height = proposal0.min_target_height();
7016 assert_eq!(proposal0.steps().len(), 1);
7017
7018 let expiry_height =
7019 pin_expiry_above_target.map(|delta| BlockHeight::from(min_target_height) + delta);
7020
7021 if expiry_height.is_some() {
7022 assert_matches!(
7025 st.create_pczt_from_proposal::<Infallible, _, Infallible>(
7026 account.id(),
7027 OvkPolicy::Sender,
7028 &proposal0,
7029 Some(min_target_height.saturating_sub(1)),
7030 ),
7031 Err(Error::ExpiryHeightBelowTargetHeight { .. })
7032 );
7033 }
7034
7035 let create_proposed_result = st.create_pczt_from_proposal::<Infallible, _, Infallible>(
7036 account.id(),
7037 OvkPolicy::Sender,
7038 &proposal0,
7039 expiry_height,
7040 );
7041 assert_matches!(&create_proposed_result, Ok(_));
7042 let pczt_created = create_proposed_result.unwrap();
7043 let pczt_branch_id =
7044 consensus::BranchId::try_from(*pczt_created.global().consensus_branch_id())
7045 .expect("the PCZT carries a valid consensus branch ID");
7046
7047 fn assert_signable_orchard_bundle(bundle: &orchard::pczt::Bundle) {
7054 for action in bundle.actions() {
7055 assert!(
7056 action.spend().spend_auth_sig().is_some()
7057 || action.spend().dummy_sk().is_some()
7058 || action.spend().zip32_derivation().is_some(),
7059 "unsignable Orchard-protocol spend: no signature, dummy key, or derivation",
7060 );
7061 }
7062 }
7063 pczt::roles::verifier::Verifier::new(pczt_created.clone())
7064 .with_orchard::<Infallible, _>(|bundle| {
7065 assert_signable_orchard_bundle(bundle);
7066 Ok(())
7067 })
7068 .unwrap()
7069 .with_ironwood::<Infallible, _>(|bundle| {
7070 assert_signable_orchard_bundle(bundle);
7071 Ok(())
7072 })
7073 .unwrap()
7074 .with_sapling::<Infallible, _>(|bundle| {
7075 for spend in bundle.spends() {
7076 assert!(
7077 spend.spend_auth_sig().is_some()
7078 || spend.proof_generation_key().is_some()
7079 || spend.zip32_derivation().is_some(),
7080 "unsignable Sapling spend: no signature, dummy proving material, or derivation",
7081 );
7082 }
7083 Ok(())
7084 })
7085 .unwrap();
7086
7087 assert_matches!(
7089 st.extract_and_store_transaction_from_pczt(pczt_created.clone()),
7090 Err(Error::Pczt(data_api::error::PcztError::Extraction(_)))
7091 );
7092
7093 let pczt_updated = P0::add_proof_generation_keys(pczt_created, account.usk()).unwrap();
7095
7096 let sapling_prover = LocalTxProver::bundled();
7102 let orchard_pk = zcash_primitives::transaction::builder::cached_orchard_proving_key(
7103 zcash_primitives::transaction::components::orchard::bundle_version_for_branch(
7104 pczt_branch_id,
7105 ::orchard::ValuePool::Orchard,
7106 )
7107 .expect("the PCZT's consensus branch supports the Orchard pool")
7108 .circuit_version(),
7109 );
7110 let pczt_proven = Prover::new(pczt_updated)
7111 .create_orchard_proof(UnwrapErr(SysRng), orchard_pk)
7112 .unwrap()
7113 .create_sapling_proofs(UnwrapErr(SysRng), &sapling_prover, &sapling_prover)
7114 .unwrap()
7115 .finish();
7116
7117 let original_sighash = Signer::new(pczt_proven.clone()).unwrap().shielded_sighash();
7122 let original_len = pczt_proven.clone().serialize().unwrap().len();
7123 let signer_view = redact_pczt_for_signer(&pczt_proven, SignerView::Compact);
7124 assert_eq!(
7125 *signer_view.global().tx_version(),
7126 zcash_protocol::constants::V5_TX_VERSION,
7127 );
7128 assert_eq!(
7129 signer_view.sapling().anchor(),
7130 pczt_proven.sapling().anchor()
7131 );
7132 assert_eq!(
7133 signer_view.orchard().anchor(),
7134 pczt_proven.orchard().anchor()
7135 );
7136 if !signer_view.orchard().actions().is_empty() {
7137 assert!(pczt::v1::Pczt::try_from(signer_view.clone()).is_err());
7138 assert!(
7139 signer_view
7140 .orchard()
7141 .actions()
7142 .iter()
7143 .all(|action| action.cv_net().is_none() && action.output().cmx().is_none())
7144 );
7145 }
7146
7147 let signer_view_bytes = signer_view.serialize().unwrap();
7148 assert!(signer_view_bytes.len() < original_len);
7149 let signer_view = pczt::Pczt::parse(&signer_view_bytes).unwrap();
7150 assert_eq!(
7151 Signer::new(signer_view.clone()).unwrap().shielded_sighash(),
7152 original_sighash,
7153 );
7154
7155 let full_view = redact_pczt_for_signer(&pczt_proven, SignerView::Full);
7160 assert_eq!(
7161 Signer::new(full_view.clone()).unwrap().shielded_sighash(),
7162 original_sighash,
7163 );
7164 assert_eq!(full_view.orchard().anchor(), pczt_proven.orchard().anchor());
7165 assert_eq!(full_view.sapling().anchor(), pczt_proven.sapling().anchor());
7166 assert_eq!(
7167 full_view.orchard().zkproof().is_some(),
7168 pczt_proven.orchard().zkproof().is_some(),
7169 );
7170 assert!(full_view.orchard().actions().iter().all(|action| {
7171 action.spend().witness().is_none() && action.spend().dummy_sk().is_none()
7172 }));
7173 assert!(
7174 full_view
7175 .sapling()
7176 .spends()
7177 .iter()
7178 .all(|spend| spend.witness().is_none())
7179 );
7180 assert!(
7183 !full_view
7184 .global()
7185 .proprietary()
7186 .contains_key("zcash_client_backend:proposal_info")
7187 );
7188 for bundle in [full_view.orchard(), full_view.ironwood()] {
7189 assert!(bundle.actions().iter().all(|action| {
7190 !action
7191 .output()
7192 .proprietary()
7193 .contains_key("zcash_client_backend:output_info")
7194 }));
7195 }
7196 assert!(full_view.sapling().outputs().iter().all(|output| {
7197 !output
7198 .proprietary()
7199 .contains_key("zcash_client_backend:output_info")
7200 }));
7201 assert!(full_view.transparent().outputs().iter().all(|output| {
7202 !output
7203 .proprietary()
7204 .contains_key("zcash_client_backend:output_info")
7205 }));
7206 if *full_view.global().tx_version() == zcash_protocol::constants::V5_TX_VERSION {
7207 let full_view_bytes = full_view.clone().serialize().unwrap();
7208 assert!(pczt::v1::Pczt::try_from(full_view).is_ok());
7209 assert_eq!(
7210 u32::from_le_bytes(full_view_bytes[4..8].try_into().unwrap()),
7211 1,
7212 "the full signer view of a v5 PCZT serializes in the v1 encoding",
7213 );
7214 }
7215
7216 let mut signer = Signer::new(signer_view).unwrap();
7219 P0::apply_signatures_to_pczt(&mut signer, account.usk()).unwrap();
7220 let pczt_authorized = Combiner::new(vec![pczt_proven, signer.finish()])
7221 .combine()
7222 .unwrap();
7223
7224 let extract_and_store_result = st.extract_and_store_transaction_from_pczt(pczt_authorized);
7226 assert_matches!(&extract_and_store_result, Ok(_));
7227 let txid = extract_and_store_result.unwrap();
7228
7229 if let Some(expiry_height) = expiry_height {
7230 let tx = st.wallet().get_transaction(txid).unwrap().unwrap();
7231 assert_eq!(tx.expiry_height(), expiry_height);
7232 }
7233
7234 let (h, _) = st.generate_next_block_including(txid);
7235 st.scan_cached_blocks(h, 1);
7236}
7237
7238#[cfg(feature = "transparent-inputs")]
7244pub fn wallet_recovery_computes_fees<T: ShieldedPoolTester, DsF: DataStoreFactory>(
7245 ds_factory: DsF,
7246 cache: impl TestCache,
7247 mut intervene: impl FnMut(&mut DsF::DataStore, TxId) -> Result<(), DsF::DsError>,
7248) {
7249 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
7250
7251 let seed = Secret::new(st.test_seed().unwrap().expose_secret().clone());
7252 let source_account = st.test_account().cloned().unwrap();
7253 let (dest_account_id, dest_usk) = st
7254 .wallet_mut()
7255 .create_account("dest", &seed, source_account.birthday(), None)
7256 .unwrap();
7257
7258 let (to, _) = dest_usk.default_transparent_address();
7259
7260 let note_value = Zatoshis::const_from_u64(350000);
7262 let _summary = st.add_notes_checking_balance([Some(note_value), Some(note_value)]);
7263
7264 let input_selector = GreedyInputSelector::new();
7267 let change_strategy =
7268 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
7269 let transfer_amount = Zatoshis::const_from_u64(200000);
7270 let request = TransactionRequest::new(vec![Payment::without_memo(
7271 Address::from(to).to_zcash_address(st.network()),
7272 transfer_amount,
7273 )])
7274 .unwrap();
7275
7276 let mut send_transparent = || {
7277 let p0 = st
7278 .propose_transfer(
7279 source_account.id(),
7280 &input_selector,
7281 &change_strategy,
7282 request.clone(),
7283 ConfirmationsPolicy::MIN,
7284 )
7285 .unwrap();
7286 let result0 = st
7287 .create_proposed_transactions::<Infallible, _, Infallible, _>(
7288 source_account.usk(),
7289 OvkPolicy::Sender,
7290 &p0,
7291 )
7292 .unwrap();
7293 assert_eq!(result0.len(), 1);
7294 let txid = result0[0];
7295 let (h, _) = st.generate_next_block_including(txid);
7296 st.scan_cached_blocks(h, 1);
7297
7298 let tx = st.wallet().get_transaction(txid).unwrap().unwrap();
7300 let t_bundle = tx.transparent_bundle().unwrap();
7301 assert_eq!(t_bundle.vout.len(), 1);
7302
7303 let outpoint = OutPoint::new(*txid.as_ref(), 0);
7304 let utxo = WalletTransparentOutput::from_parts(
7305 outpoint,
7306 t_bundle.vout[0].clone(),
7307 Some(h),
7308 Some(dest_account_id),
7309 Some(TransparentKeyScope::EXTERNAL),
7310 None,
7311 )
7312 .unwrap();
7313 st.wallet_mut()
7314 .put_received_transparent_utxo(&utxo)
7315 .unwrap();
7316
7317 (txid, h)
7318 };
7319
7320 send_transparent();
7321 let (input_tx_1_txid, input_tx_1_height) = send_transparent();
7322
7323 assert_eq!(
7324 st.get_total_balance(dest_account_id),
7325 (transfer_amount + transfer_amount).unwrap()
7326 );
7327
7328 let p1 = st
7330 .propose_shielding(
7331 &input_selector,
7332 &change_strategy,
7333 Zatoshis::const_from_u64(10000),
7334 &[to],
7335 dest_account_id,
7336 ConfirmationsPolicy::MIN,
7337 CoinbaseFilter::AllTransparentOutputs,
7338 )
7339 .unwrap();
7340 let result1 = st
7341 .create_proposed_transactions::<Infallible, _, Infallible, _>(
7342 &dest_usk,
7343 OvkPolicy::Sender,
7344 &p1,
7345 )
7346 .unwrap();
7347 assert_eq!(result1.len(), 1);
7348 let txid = result1[0];
7349 let (h, _) = st.generate_next_block_including(txid);
7350 st.scan_cached_blocks(h, 1);
7351
7352 let shielding_tx = st.get_tx_from_history(txid).unwrap().unwrap();
7356 assert_matches!(shielding_tx.fee_paid, Some(_));
7357 let created_fee = shielding_tx.fee_paid.unwrap();
7358
7359 intervene(st.wallet_mut(), txid).unwrap();
7360
7361 let shielding_tx = st.get_tx_from_history(txid).unwrap().unwrap();
7363 assert_matches!(shielding_tx.fee_paid, None);
7364
7365 let tx = st.wallet().get_transaction(txid).unwrap().unwrap();
7368 let network = *st.network();
7369 decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, Some(h)).unwrap();
7370
7371 let shielding_tx = st.get_tx_from_history(txid).unwrap().unwrap();
7373 assert_eq!(shielding_tx.fee_paid, Some(created_fee));
7374
7375 intervene(st.wallet_mut(), txid).unwrap();
7378
7379 let shielding_tx = st.get_tx_from_history(txid).unwrap().unwrap();
7380 assert_matches!(shielding_tx.fee_paid, None);
7381
7382 let tx = st
7385 .wallet()
7386 .get_transaction(input_tx_1_txid)
7387 .unwrap()
7388 .unwrap();
7389 let network = *st.network();
7390 decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, Some(input_tx_1_height)).unwrap();
7391
7392 let shielding_tx = st.get_tx_from_history(txid).unwrap().unwrap();
7394 assert_eq!(shielding_tx.fee_paid, Some(created_fee));
7395}
7396
7397pub fn receive_two_notes_with_same_value<T: ShieldedPoolTester>(
7400 dsf: impl DataStoreFactory,
7401 cache: impl TestCache,
7402) {
7403 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
7404
7405 let value = Zatoshis::const_from_u64(60000);
7407 let h = st
7408 .add_notes_checking_balance([[value, value]])
7409 .block_height()
7410 .unwrap();
7411
7412 let account = st.test_account().cloned().unwrap();
7414 let total_value = (value + value).unwrap();
7415 assert_eq!(
7416 st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
7417 total_value
7418 );
7419
7420 let target_height = (h + 1).into();
7421
7422 let unspent_notes = T::select_unspent_notes(&st, account.id(), target_height, &[]).unwrap();
7424 assert_eq!(unspent_notes.len(), 2);
7425 for note in unspent_notes {
7426 assert_eq!(T::note_value(note.note()), value);
7427 }
7428
7429 let spendable_notes = T::select_spendable_notes(
7431 &st,
7432 account.id(),
7433 TargetValue::AllFunds(MaxSpendMode::MaxSpendable),
7434 target_height,
7435 ConfirmationsPolicy::MIN,
7436 &[],
7437 )
7438 .unwrap();
7439 assert_eq!(spendable_notes.len(), 2);
7440 for note in spendable_notes {
7441 assert_eq!(T::note_value(note.note()), value);
7442 }
7443}
7444
7445#[cfg(feature = "pczt")]
7446fn build_transparent_coinbase_tx(
7447 network: &LocalNetwork,
7448 target_height: TargetHeight,
7449 value: Zatoshis,
7450 recipient: TransparentAddress,
7451 miner_data: Option<PushValue>,
7452) -> zcash_primitives::transaction::builder::BuildResult {
7453 let build_config = BuildConfig::Coinbase { miner_data };
7454 let mut builder = Builder::new(*network, BlockHeight::from(target_height), build_config);
7455
7456 builder.add_transparent_output(&recipient, value).unwrap();
7458
7459 builder
7461 .build(
7462 &TransparentSigningSet::new(),
7464 &[],
7466 &[],
7468 UnwrapErr(SysRng),
7469 &LocalTxProver::bundled(),
7470 &LocalTxProver::bundled(),
7471 &StandardFeeRule::Zip317,
7473 )
7474 .unwrap()
7475}
7476
7477#[cfg(all(feature = "pczt", feature = "transparent-inputs"))]
7478pub fn immature_coinbase_outputs_are_excluded_from_note_selection<T: ShieldedPoolTester>(
7480 dsf: impl DataStoreFactory,
7481 cache: impl TestCache,
7482) {
7483 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
7484
7485 let (t_addr, _) = st.get_account().usk().default_transparent_address();
7487
7488 let coinbase_value = Zatoshis::const_from_u64(50000);
7489
7490 let coinbase_height = st.sapling_activation_height();
7492
7493 let coinbase_build_result = build_transparent_coinbase_tx(
7495 st.network(),
7496 TargetHeight::from(coinbase_height),
7497 coinbase_value,
7498 t_addr,
7499 None,
7500 );
7501 let coinbase_tx = coinbase_build_result.transaction();
7502 let (h, _) = st.generate_next_block_from_tx(0, coinbase_tx);
7503 st.scan_cached_blocks(h, 1);
7504
7505 let params = *st.network();
7506 decrypt_and_store_transaction(¶ms, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
7507
7508 for i in 1..=99 {
7509 let latest_block_height = st.add_empty_blocks(1);
7510
7511 let spendable_utxos = st
7513 .wallet()
7514 .get_spendable_transparent_outputs(
7515 &t_addr,
7516 TargetHeight::from(h + i),
7517 ConfirmationsPolicy::default(),
7518 CoinbaseFilter::AllTransparentOutputs,
7519 LockFilter::Policy(&LockedInputPolicy::Exclude),
7520 )
7521 .unwrap();
7522 let confirmations = latest_block_height - h;
7523 assert!(
7524 spendable_utxos.is_empty(),
7525 "{i}: Immature coinbase output is spendable at blockheight {latest_block_height} \
7526 with {confirmations} confirmations \
7527 (should only be spendable at 100):\n \
7528 {spendable_utxos:#?}"
7529 );
7530 }
7531
7532 let latest_height = st.add_empty_blocks(1);
7534 let confirmations = latest_height - h;
7535 let target_height = TargetHeight::from(latest_height + 1);
7536 let spendable_utxos = st
7537 .wallet()
7538 .get_spendable_transparent_outputs(
7539 &t_addr,
7540 target_height,
7541 ConfirmationsPolicy::default(),
7542 CoinbaseFilter::AllTransparentOutputs,
7543 LockFilter::Policy(&LockedInputPolicy::Exclude),
7544 )
7545 .unwrap();
7546 assert!(
7547 !spendable_utxos.is_empty(),
7548 "Coinbase output should be spendable at blockheight {latest_height} \
7549 with {confirmations} confirmations since the coinbase tx was mined (at {h})\n \
7550 target_height {target_height:?} - coinbase_tx.mined_height {h} = {}",
7551 u32::from(target_height) - u32::from(h)
7552 );
7553
7554 let account = st.get_account().id();
7556 let _proposal = st
7557 .propose_shielding(
7558 &GreedyInputSelector::new(),
7559 &single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL),
7560 Zatoshis::from_u64(10000).unwrap(),
7561 &[t_addr],
7562 account,
7563 ConfirmationsPolicy::default(),
7564 CoinbaseFilter::AllTransparentOutputs,
7565 )
7566 .unwrap();
7567}
7568
7569#[cfg(all(feature = "pczt", feature = "transparent-inputs"))]
7570pub fn coinbase_only_filtering<T: ShieldedPoolTester, Dsf>(ds_factory: Dsf, cache: impl TestCache)
7575where
7576 Dsf: DataStoreFactory,
7577 <<Dsf as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
7578{
7579 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
7580 let (t_addr, _) = st.get_account().usk().default_transparent_address();
7581 let account = st.get_account().id();
7582
7583 let coinbase_value = Zatoshis::const_from_u64(50000);
7585 let coinbase_height = st.sapling_activation_height();
7586 let coinbase_build_result = build_transparent_coinbase_tx(
7587 st.network(),
7588 TargetHeight::from(coinbase_height),
7589 coinbase_value,
7590 t_addr,
7591 None,
7592 );
7593 let coinbase_tx = coinbase_build_result.transaction();
7594 let coinbase_outpoint = OutPoint::new(coinbase_tx.txid().into(), 0);
7596 let (h, _) = st.generate_next_block_from_tx(0, coinbase_tx);
7597 st.scan_cached_blocks(h, 1);
7598 let params = *st.network();
7599 decrypt_and_store_transaction(¶ms, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
7600
7601 let non_coinbase_value = Zatoshis::const_from_u64(60000);
7605 let non_coinbase_outpoint = OutPoint::fake();
7606 let utxo = WalletTransparentOutput::from_parts(
7607 non_coinbase_outpoint.clone(),
7608 TxOut::new(non_coinbase_value, t_addr.script().into()),
7609 Some(h),
7610 Some(account),
7611 Some(TransparentKeyScope::EXTERNAL),
7612 None,
7613 )
7614 .unwrap();
7615 st.wallet_mut()
7616 .put_received_transparent_utxo(&utxo)
7617 .unwrap();
7618
7619 st.add_empty_blocks(100);
7621 let target_height = TargetHeight::from(h + 101);
7622
7623 let all_utxos = st
7625 .wallet()
7626 .get_spendable_transparent_outputs(
7627 &t_addr,
7628 target_height,
7629 ConfirmationsPolicy::default(),
7630 CoinbaseFilter::AllTransparentOutputs,
7631 LockFilter::Policy(&LockedInputPolicy::Exclude),
7632 )
7633 .unwrap();
7634 assert_eq!(
7635 all_utxos.len(),
7636 2,
7637 "Expected both coinbase and non-coinbase UTXOs with CoinbaseFilter::AllTransparentOutputs"
7638 );
7639 let all_utxos_value = all_utxos
7640 .iter()
7641 .map(|utxo| utxo.value().into_u64())
7642 .sum::<u64>();
7643 assert_eq!(
7644 all_utxos_value,
7645 coinbase_value.into_u64() + non_coinbase_value.into_u64(),
7646 "Unexpected total UTXO value when querying for all transparent transactions"
7647 );
7648
7649 let coinbase_utxos = st
7651 .wallet()
7652 .get_spendable_transparent_outputs(
7653 &t_addr,
7654 target_height,
7655 ConfirmationsPolicy::default(),
7656 CoinbaseFilter::CoinbaseOnly,
7657 LockFilter::Policy(&LockedInputPolicy::Exclude),
7658 )
7659 .unwrap();
7660 assert_eq!(
7661 coinbase_utxos.len(),
7662 1,
7663 "Expected only the coinbase UTXO with CoinbaseFilter::CoinbaseOnly"
7664 );
7665 assert_eq!(coinbase_utxos[0].value(), coinbase_value);
7666 assert_eq!(coinbase_utxos[0].outpoint(), &coinbase_outpoint);
7667
7668 let non_coinbase_utxos = st
7672 .wallet()
7673 .get_spendable_transparent_outputs(
7674 &t_addr,
7675 target_height,
7676 ConfirmationsPolicy::default(),
7677 CoinbaseFilter::NonCoinbaseOnly,
7678 LockFilter::Policy(&LockedInputPolicy::Exclude),
7679 )
7680 .unwrap();
7681 assert_eq!(
7682 non_coinbase_utxos.len(),
7683 1,
7684 "Expected only the non-coinbase UTXO with CoinbaseFilter::NonCoinbaseOnly"
7685 );
7686 assert_eq!(non_coinbase_utxos[0].value(), non_coinbase_value);
7687 assert_eq!(non_coinbase_utxos[0].outpoint(), &non_coinbase_outpoint);
7688
7689 let proposal = st
7691 .propose_shielding(
7692 &GreedyInputSelector::new(),
7693 &single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL),
7694 Zatoshis::from_u64(10000).unwrap(),
7695 &[t_addr],
7696 account,
7697 ConfirmationsPolicy::default(),
7698 CoinbaseFilter::CoinbaseOnly,
7699 )
7700 .unwrap();
7701 let coinbase_inputs = proposal.steps().first().transparent_inputs();
7702 assert_eq!(
7703 coinbase_inputs.len(),
7704 1,
7705 "CoinbaseOnly proposal should contain exactly one transparent input"
7706 );
7707 assert_eq!(coinbase_inputs[0].value(), coinbase_value);
7708 assert_eq!(coinbase_inputs[0].outpoint(), &coinbase_outpoint);
7709
7710 let proposal_non_coinbase = st
7712 .propose_shielding(
7713 &GreedyInputSelector::new(),
7714 &single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL),
7715 Zatoshis::from_u64(10000).unwrap(),
7716 &[t_addr],
7717 account,
7718 ConfirmationsPolicy::default(),
7719 CoinbaseFilter::NonCoinbaseOnly,
7720 )
7721 .unwrap();
7722 let non_coinbase_inputs = proposal_non_coinbase.steps().first().transparent_inputs();
7723 assert_eq!(
7724 non_coinbase_inputs.len(),
7725 1,
7726 "NonCoinbaseOnly proposal should contain exactly one transparent input"
7727 );
7728 assert_eq!(non_coinbase_inputs[0].value(), non_coinbase_value);
7729 assert_eq!(non_coinbase_inputs[0].outpoint(), &non_coinbase_outpoint);
7730
7731 let proposal_all = st
7733 .propose_shielding(
7734 &GreedyInputSelector::new(),
7735 &single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL),
7736 Zatoshis::from_u64(10000).unwrap(),
7737 &[t_addr],
7738 account,
7739 ConfirmationsPolicy::default(),
7740 CoinbaseFilter::AllTransparentOutputs,
7741 )
7742 .unwrap();
7743 let all_inputs = proposal_all.steps().first().transparent_inputs();
7744 assert_eq!(
7745 all_inputs.len(),
7746 2,
7747 "All proposal should contain both transparent inputs"
7748 );
7749 let all_outpoints = all_inputs
7751 .iter()
7752 .map(|input| input.outpoint().clone())
7753 .collect::<BTreeSet<_>>();
7754 assert_eq!(
7755 all_outpoints,
7756 BTreeSet::from([coinbase_outpoint, non_coinbase_outpoint]),
7757 "All proposal should contain both the coinbase and non-coinbase outpoints"
7758 );
7759}
7760
7761#[cfg(all(feature = "pczt", feature = "transparent-inputs"))]
7765pub fn propose_shielding_coinbase_succeeds<T: ShieldedPoolTester, Dsf>(
7766 ds_factory: Dsf,
7767 cache: impl TestCache,
7768) where
7769 Dsf: DataStoreFactory,
7770 <<Dsf as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
7771{
7772 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
7773 let (t_addr, _) = st.get_account().usk().default_transparent_address();
7774 let coinbase_value = Zatoshis::const_from_u64(50000);
7775 let coinbase_build_result = build_transparent_coinbase_tx(
7776 st.network(),
7777 TargetHeight::from(st.sapling_activation_height()),
7778 coinbase_value,
7779 t_addr,
7780 None,
7781 );
7782 let coinbase_tx = coinbase_build_result.transaction();
7783 let (h, _) = st.generate_next_block_from_tx(0, coinbase_tx);
7784 st.scan_cached_blocks(h, 1);
7785 let params = *st.network();
7786 decrypt_and_store_transaction(¶ms, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
7787 st.add_empty_blocks(100);
7789
7790 let to_extsk = T::sk(&[0xab; 32]);
7793 let to_address = T::sk_default_address(&to_extsk).to_zcash_address(st.network());
7794
7795 let proposal = st
7796 .propose_shielding_coinbase(
7797 &GreedyInputSelector::new(),
7798 &StandardFeeRule::Zip317,
7799 Zatoshis::ZERO,
7800 &[t_addr],
7801 to_address.clone(),
7802 None,
7803 None,
7804 )
7805 .expect("propose_shielding_coinbase with a shielded destination should succeed");
7806
7807 let step = proposal.steps().first();
7808 assert_eq!(
7809 step.transparent_inputs().len(),
7810 1,
7811 "Expected exactly one coinbase transparent input"
7812 );
7813 let payments = step.transaction_request().payments();
7814 assert_eq!(
7815 payments.len(),
7816 1,
7817 "Expected exactly one payment in proposal"
7818 );
7819 let (idx, payment) = payments.iter().next().unwrap();
7820 assert_eq!(*idx, 0);
7821 assert_eq!(payment.recipient_address(), &to_address);
7822 assert_eq!(
7823 step.balance().proposed_change().len(),
7824 0,
7825 "Coinbase shielding must produce no change"
7826 );
7827
7828 let fee = step.balance().fee_required();
7829 let payment_amount = payment.amount().expect("payment must have an amount");
7830 assert_eq!(
7831 (payment_amount + fee).unwrap(),
7832 coinbase_value,
7833 "payment_amount + fee must equal coinbase input value"
7834 );
7835}
7836
7837#[cfg(all(feature = "pczt", feature = "transparent-inputs"))]
7844pub fn proposal_without_confirmations_policy_builds<T: ShieldedPoolTester, Dsf>(
7845 ds_factory: Dsf,
7846 cache: impl TestCache,
7847) where
7848 Dsf: DataStoreFactory,
7849 <<Dsf as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
7850{
7851 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
7852 let (t_addr, _) = st.get_account().usk().default_transparent_address();
7853 let coinbase_value = Zatoshis::const_from_u64(50000);
7854 let coinbase_build_result = build_transparent_coinbase_tx(
7855 st.network(),
7856 TargetHeight::from(st.sapling_activation_height()),
7857 coinbase_value,
7858 t_addr,
7859 None,
7860 );
7861 let coinbase_tx = coinbase_build_result.transaction();
7862 let (h, _) = st.generate_next_block_from_tx(0, coinbase_tx);
7863 st.scan_cached_blocks(h, 1);
7864 let params = *st.network();
7865 decrypt_and_store_transaction(¶ms, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
7866 st.add_empty_blocks(100);
7868
7869 let to_extsk = T::sk(&[0xab; 32]);
7870 let to_address = T::sk_default_address(&to_extsk).to_zcash_address(st.network());
7871
7872 let proposal = st
7873 .propose_shielding_coinbase(
7874 &GreedyInputSelector::new(),
7875 &StandardFeeRule::Zip317,
7876 Zatoshis::ZERO,
7877 &[t_addr],
7878 to_address,
7879 None,
7880 None,
7881 )
7882 .expect("coinbase shielding proposal should succeed");
7883
7884 let selected_anchor = proposal
7886 .steps()
7887 .first()
7888 .anchor_height()
7889 .expect("a shielding step must preserve its selected checkpoint");
7890
7891 let proto = crate::proto::proposal::Proposal::from_standard_proposal(&proposal);
7892 assert_eq!(
7893 proto.steps[0].anchor_height,
7894 u32::from(selected_anchor),
7895 "a shielding step must serialize its selected checkpoint",
7896 );
7897
7898 let mut zero_anchor = proto.clone();
7903 zero_anchor.steps[0].anchor_height = 0;
7904 assert_matches!(
7905 zero_anchor.try_into_standard_proposal(¶ms, st.wallet()),
7906 Err(crate::proto::ProposalDecodingError::MissingShieldedAnchor)
7907 );
7908
7909 let mut without_policy = proto;
7912 without_policy.confirmations_policy = None;
7913 let decoded = without_policy
7914 .try_into_standard_proposal(¶ms, st.wallet())
7915 .expect("a proposal without a confirmations policy must decode");
7916 assert_eq!(
7917 decoded.confirmations_policy(),
7918 ConfirmationsPolicy::default(),
7919 "a missing confirmations policy must decode as the default",
7920 );
7921 assert_eq!(
7922 decoded.steps().first().anchor_height(),
7923 Some(selected_anchor),
7924 "decoding must preserve the serialized anchor",
7925 );
7926
7927 let usk = st.get_account().usk().clone();
7928 st.create_proposed_transactions::<Infallible, _, Infallible, _>(
7929 &usk,
7930 OvkPolicy::Sender,
7931 &decoded,
7932 )
7933 .expect("a proposal that carries its selected anchor must build");
7934}
7935
7936#[cfg(all(feature = "pczt", feature = "transparent-inputs"))]
7939pub fn propose_shielding_coinbase_transparent_recipient_rejected<T: ShieldedPoolTester, Dsf>(
7940 ds_factory: Dsf,
7941 cache: impl TestCache,
7942) where
7943 Dsf: DataStoreFactory,
7944 <<Dsf as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
7945{
7946 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
7947 let (t_addr, _) = st.get_account().usk().default_transparent_address();
7948 let coinbase_value = Zatoshis::const_from_u64(50000);
7949 let coinbase_build_result = build_transparent_coinbase_tx(
7950 st.network(),
7951 TargetHeight::from(st.sapling_activation_height()),
7952 coinbase_value,
7953 t_addr,
7954 None,
7955 );
7956 let coinbase_tx = coinbase_build_result.transaction();
7957 let (h, _) = st.generate_next_block_from_tx(0, coinbase_tx);
7958 st.scan_cached_blocks(h, 1);
7959 let params = *st.network();
7960 decrypt_and_store_transaction(¶ms, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
7961 st.add_empty_blocks(100);
7962
7963 let bad_to_address = Address::Transparent(TransparentAddress::PublicKeyHash([7; 20]))
7964 .to_zcash_address(st.network());
7965
7966 let result = st.propose_shielding_coinbase(
7967 &GreedyInputSelector::new(),
7968 &StandardFeeRule::Zip317,
7969 Zatoshis::ZERO,
7970 &[t_addr],
7971 bad_to_address,
7972 None,
7973 None,
7974 );
7975
7976 assert_matches!(
7977 result,
7978 Err(Error::Proposal(
7979 ProposalError::ShieldingRequiresShieldedRecipient
7980 ))
7981 );
7982}
7983
7984#[cfg(all(feature = "pczt", feature = "transparent-inputs"))]
7987pub fn propose_shielding_coinbase_with_memo_succeeds<T: ShieldedPoolTester, Dsf>(
7988 ds_factory: Dsf,
7989 cache: impl TestCache,
7990) where
7991 Dsf: DataStoreFactory,
7992 <<Dsf as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
7993{
7994 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
7995 let (t_addr, _) = st.get_account().usk().default_transparent_address();
7996 let coinbase_value = Zatoshis::const_from_u64(50000);
7997 let coinbase_build_result = build_transparent_coinbase_tx(
7998 st.network(),
7999 TargetHeight::from(st.sapling_activation_height()),
8000 coinbase_value,
8001 t_addr,
8002 None,
8003 );
8004 let coinbase_tx = coinbase_build_result.transaction();
8005 let (h, _) = st.generate_next_block_from_tx(0, coinbase_tx);
8006 st.scan_cached_blocks(h, 1);
8007 let params = *st.network();
8008 decrypt_and_store_transaction(¶ms, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
8009 st.add_empty_blocks(100);
8010
8011 let to_extsk = T::sk(&[0xcd; 32]);
8012 let to_address = T::sk_default_address(&to_extsk).to_zcash_address(st.network());
8013
8014 let memo_text = "shielding to external wallet";
8015 let memo_bytes = MemoBytes::from(memo_text.parse::<Memo>().unwrap());
8016
8017 let proposal = st
8018 .propose_shielding_coinbase(
8019 &GreedyInputSelector::new(),
8020 &StandardFeeRule::Zip317,
8021 Zatoshis::ZERO,
8022 &[t_addr],
8023 to_address,
8024 Some(memo_bytes.clone()),
8025 None,
8026 )
8027 .expect("propose_shielding_coinbase with memo should succeed");
8028
8029 let payments = proposal.steps().first().transaction_request().payments();
8030 let (_, payment) = payments.iter().next().unwrap();
8031 assert_eq!(payment.memo(), Some(&memo_bytes));
8032}
8033
8034#[cfg(all(feature = "pczt", feature = "transparent-inputs"))]
8037pub fn propose_shielding_coinbase_with_limit_truncates_inputs<T: ShieldedPoolTester, Dsf>(
8038 ds_factory: Dsf,
8039 cache: impl TestCache,
8040) where
8041 Dsf: DataStoreFactory,
8042 <<Dsf as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
8043{
8044 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
8045 let (t_addr, _) = st.get_account().usk().default_transparent_address();
8046
8047 let values = [
8050 Zatoshis::const_from_u64(30000),
8051 Zatoshis::const_from_u64(70000),
8052 Zatoshis::const_from_u64(50000),
8053 ];
8054 let mut first_h = None;
8055 for v in values {
8056 let coinbase_height = if let Some(h) = first_h {
8057 h + values.len() as u32 } else {
8059 st.sapling_activation_height()
8060 };
8061 let build = build_transparent_coinbase_tx(
8062 st.network(),
8063 TargetHeight::from(coinbase_height),
8064 v,
8065 t_addr,
8066 None,
8067 );
8068 let tx = build.transaction();
8069 let (h, _) = st.generate_next_block_from_tx(0, tx);
8070 st.scan_cached_blocks(h, 1);
8071 let params = *st.network();
8072 decrypt_and_store_transaction(¶ms, st.wallet_mut(), tx, Some(h)).unwrap();
8073 if first_h.is_none() {
8074 first_h = Some(h);
8075 }
8076 }
8077 st.add_empty_blocks(100);
8079
8080 let to_extsk = T::sk(&[0x55; 32]);
8081 let to_address = T::sk_default_address(&to_extsk).to_zcash_address(st.network());
8082
8083 let proposal = st
8084 .propose_shielding_coinbase(
8085 &GreedyInputSelector::new(),
8086 &StandardFeeRule::Zip317,
8087 Zatoshis::ZERO,
8088 &[t_addr],
8089 to_address,
8090 None,
8091 Some(2),
8092 )
8093 .expect("propose_shielding_coinbase with limit=Some(2) should succeed");
8094
8095 let inputs = proposal.steps().first().transparent_inputs();
8096 assert_eq!(
8097 inputs.len(),
8098 2,
8099 "limit=Some(2) should select exactly 2 inputs"
8100 );
8101
8102 let mut selected_values: Vec<u64> = inputs.iter().map(|i| i.value().into_u64()).collect();
8104 selected_values.sort_unstable_by(|a, b| b.cmp(a));
8105 assert_eq!(selected_values, vec![70000, 50000]);
8106}
8107
8108#[cfg(all(feature = "pczt", feature = "transparent-inputs"))]
8113pub fn propose_shielding_coinbase_with_zero_limit_insufficient_funds<T: ShieldedPoolTester, Dsf>(
8114 ds_factory: Dsf,
8115 cache: impl TestCache,
8116) where
8117 Dsf: DataStoreFactory,
8118 <<Dsf as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
8119{
8120 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
8121 let (t_addr, _) = st.get_account().usk().default_transparent_address();
8122 let coinbase_value = Zatoshis::const_from_u64(50000);
8123 let coinbase_build_result = build_transparent_coinbase_tx(
8124 st.network(),
8125 TargetHeight::from(st.sapling_activation_height()),
8126 coinbase_value,
8127 t_addr,
8128 None,
8129 );
8130 let coinbase_tx = coinbase_build_result.transaction();
8131 let (h, _) = st.generate_next_block_from_tx(0, coinbase_tx);
8132 st.scan_cached_blocks(h, 1);
8133 let params = *st.network();
8134 decrypt_and_store_transaction(¶ms, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
8135 st.add_empty_blocks(100);
8136
8137 let to_extsk = T::sk(&[0x66; 32]);
8138 let to_address = T::sk_default_address(&to_extsk).to_zcash_address(st.network());
8139
8140 let shielding_threshold = Zatoshis::const_from_u64(10000);
8141 let result = st.propose_shielding_coinbase(
8142 &GreedyInputSelector::new(),
8143 &StandardFeeRule::Zip317,
8144 shielding_threshold,
8145 &[t_addr],
8146 to_address,
8147 None,
8148 Some(0),
8149 );
8150
8151 assert_matches!(result, Err(Error::InsufficientFunds { .. }));
8155}
8156
8157#[cfg(all(feature = "pczt", feature = "transparent-inputs"))]
8171pub fn propose_and_build_shielding_coinbase_succeeds<T: ShieldedPoolTester, Dsf>(
8172 ds_factory: Dsf,
8173 cache: impl TestCache,
8174) where
8175 Dsf: DataStoreFactory,
8176 <<Dsf as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
8177{
8178 let mut st = TestDsl::with_sapling_birthday_account(ds_factory, cache).build::<T>();
8179 let account = st.get_account();
8180 let (t_addr, _) = account.usk().default_transparent_address();
8181 let coinbase_value = Zatoshis::const_from_u64(50000);
8182 let coinbase_build_result = build_transparent_coinbase_tx(
8183 st.network(),
8184 TargetHeight::from(st.sapling_activation_height()),
8185 coinbase_value,
8186 t_addr,
8187 None,
8188 );
8189 let coinbase_tx = coinbase_build_result.transaction();
8190 let (h, _) = st.generate_next_block_from_tx(0, coinbase_tx);
8191 st.scan_cached_blocks(h, 1);
8192 let params = *st.network();
8193 decrypt_and_store_transaction(¶ms, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
8194 st.add_empty_blocks(COINBASE_MATURITY_BLOCKS as usize);
8196
8197 let to_extsk = T::sk(&[0xcd; 32]);
8199 let to_address = T::sk_default_address(&to_extsk).to_zcash_address(st.network());
8200
8201 let proposal = st
8202 .propose_shielding_coinbase(
8203 &GreedyInputSelector::new(),
8204 &StandardFeeRule::Zip317,
8205 Zatoshis::ZERO,
8206 &[t_addr],
8207 to_address,
8208 None,
8209 None,
8210 )
8211 .expect("propose_shielding_coinbase should succeed");
8212
8213 let build_result = st.create_proposed_transactions::<Infallible, _, Infallible, _>(
8218 account.usk(),
8219 OvkPolicy::Sender,
8220 &proposal,
8221 );
8222 assert_matches!(
8223 &build_result,
8224 Ok(txids) if txids.len() == 1,
8225 "create_proposed_transactions must succeed for proposal {:?}",
8226 proposal,
8227 );
8228}
8229
8230#[cfg(all(feature = "orchard", feature = "pczt", feature = "transparent-inputs"))]
8234pub fn shielding_coinbase_to_orchard_receiver_delivers_via_ironwood<Dsf>(
8235 ds_factory: Dsf,
8236 cache: impl TestCache,
8237) where
8238 Dsf: DataStoreFactory,
8239 <<Dsf as DataStoreFactory>::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug,
8240{
8241 let ironwood_active_network = {
8243 let activation = BlockHeight::from_u32(100_000);
8244 LocalNetwork {
8245 nu6: Some(activation),
8246 nu6_1: Some(activation),
8247 nu6_2: Some(activation),
8248 nu6_3: Some(activation),
8249 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
8250 }
8251 };
8252
8253 let mut st = TestDsl::from(
8254 TestBuilder::new()
8255 .with_network(ironwood_active_network)
8256 .with_data_store_factory(ds_factory)
8257 .with_block_cache(cache)
8258 .with_account_from_sapling_activation(BlockHash([0; 32])),
8259 )
8260 .build::<OrchardPoolTester>();
8261 let account = st.get_account();
8262 let (t_addr, _) = account.usk().default_transparent_address();
8263 let coinbase_value = Zatoshis::const_from_u64(100000);
8264 let coinbase_build_result = build_transparent_coinbase_tx(
8265 st.network(),
8266 TargetHeight::from(st.sapling_activation_height()),
8267 coinbase_value,
8268 t_addr,
8269 None,
8270 );
8271 let coinbase_tx = coinbase_build_result.transaction();
8272 let (h, _) = st.generate_next_block_from_tx(0, coinbase_tx);
8273 st.scan_cached_blocks(h, 1);
8274 let params = *st.network();
8275 decrypt_and_store_transaction(¶ms, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
8276 st.add_empty_blocks(COINBASE_MATURITY_BLOCKS as usize);
8278
8279 let to_extsk = OrchardPoolTester::sk(&[0xcd; 32]);
8281 let to_address =
8282 OrchardPoolTester::sk_default_address(&to_extsk).to_zcash_address(st.network());
8283
8284 let proposal = st
8285 .propose_shielding_coinbase(
8286 &GreedyInputSelector::new(),
8287 &StandardFeeRule::Zip317,
8288 Zatoshis::ZERO,
8289 &[t_addr],
8290 to_address,
8291 None,
8292 None,
8293 )
8294 .expect("propose_shielding_coinbase to an Orchard receiver should succeed post-NU6.3");
8295
8296 assert_eq!(
8300 proposal.steps().head.payment_pools().get(&0),
8301 Some(&PoolType::IRONWOOD),
8302 );
8303
8304 let build_result = st.create_proposed_transactions::<Infallible, _, Infallible, _>(
8305 account.usk(),
8306 OvkPolicy::Sender,
8307 &proposal,
8308 );
8309 assert_matches!(
8310 &build_result,
8311 Ok(txids) if txids.len() == 1,
8312 "create_proposed_transactions must succeed for proposal {:?}",
8313 proposal,
8314 );
8315 let sent_txid = build_result.unwrap().head;
8316
8317 assert!(
8322 st.wallet()
8323 .transaction_data_requests()
8324 .unwrap()
8325 .contains(&TransactionDataRequest::GetStatus(sent_txid))
8326 );
8327
8328 let (mined_height, _) = st.generate_next_block_including(sent_txid);
8329 st.scan_cached_blocks(mined_height, 1);
8330 assert_eq!(
8331 st.get_tx_from_history(sent_txid)
8332 .unwrap()
8333 .expect("sent transaction is in history")
8334 .mined_height(),
8335 None,
8336 "compact scanning cannot detect an external shielding transaction",
8337 );
8338
8339 st.wallet_mut()
8340 .set_transaction_status(sent_txid, TransactionStatus::Mined(mined_height))
8341 .unwrap();
8342 assert_eq!(
8343 st.get_tx_from_history(sent_txid)
8344 .unwrap()
8345 .expect("sent transaction is in history")
8346 .mined_height(),
8347 Some(mined_height),
8348 );
8349 assert!(
8350 !st.wallet()
8351 .transaction_data_requests()
8352 .unwrap()
8353 .contains(&TransactionDataRequest::GetStatus(sent_txid)),
8354 "status intent is dormant while the transaction is mined",
8355 );
8356
8357 st.truncate_to_height(mined_height - 1);
8358 assert_eq!(
8359 st.get_tx_from_history(sent_txid)
8360 .unwrap()
8361 .expect("sent transaction is retained across the rewind")
8362 .mined_height(),
8363 None,
8364 );
8365 assert!(
8366 st.wallet()
8367 .transaction_data_requests()
8368 .unwrap()
8369 .contains(&TransactionDataRequest::GetStatus(sent_txid)),
8370 "rewinding the mined block reactivates the durable status intent",
8371 );
8372}
8373
8374#[cfg(feature = "orchard")]
8380pub fn propose_v5_payment_to_orchard_receiver_is_rejected<Dsf>(
8381 ds_factory: Dsf,
8382 cache: impl TestCache,
8383) where
8384 Dsf: DataStoreFactory,
8385{
8386 let ironwood_active_network = {
8388 let activation = BlockHeight::from_u32(100_000);
8389 LocalNetwork {
8390 nu6: Some(activation),
8391 nu6_1: Some(activation),
8392 nu6_2: Some(activation),
8393 nu6_3: Some(activation),
8394 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
8395 }
8396 };
8397
8398 let mut st = TestDsl::from(
8399 TestBuilder::new()
8400 .with_network(ironwood_active_network)
8401 .with_data_store_factory(ds_factory)
8402 .with_block_cache(cache)
8403 .with_account_from_sapling_activation(BlockHash([0; 32])),
8404 )
8405 .build::<OrchardPoolTester>();
8406
8407 st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60_000));
8409
8410 let to_extsk = OrchardPoolTester::sk(&[0xf5; 32]);
8412 let to = OrchardPoolTester::sk_default_address(&to_extsk);
8413 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
8414 to.to_zcash_address(st.network()),
8415 Zatoshis::const_from_u64(10_000),
8416 )])
8417 .unwrap();
8418
8419 let change_strategy =
8420 single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Orchard);
8421 let input_selector = GreedyInputSelector::new();
8422
8423 let account = st.get_account();
8424 let network = *st.network();
8425 let result = propose_transfer::<_, _, _, _, Infallible>(
8426 st.wallet_mut(),
8427 &network,
8428 account.id(),
8429 &input_selector,
8430 &change_strategy,
8431 request,
8432 ConfirmationsPolicy::MIN,
8433 &SpendPolicy::default(),
8434 None,
8435 Some(TxVersion::V5),
8436 );
8437
8438 assert_matches!(
8439 result,
8440 Err(Error::Proposal(
8441 ProposalError::OrchardReceiverRequiresIronwood(TxVersion::V5)
8442 ))
8443 );
8444}
8445
8446#[cfg(all(feature = "orchard", feature = "pczt"))]
8451pub fn create_pczt_supports_ironwood_output<Dsf>(ds_factory: Dsf, cache: impl TestCache)
8452where
8453 Dsf: DataStoreFactory,
8454 <Dsf as DataStoreFactory>::AccountId: serde::Serialize + serde::de::DeserializeOwned,
8455{
8456 let ironwood_active_network = {
8458 let activation = BlockHeight::from_u32(100_000);
8459 LocalNetwork {
8460 nu6: Some(activation),
8461 nu6_1: Some(activation),
8462 nu6_2: Some(activation),
8463 nu6_3: Some(activation),
8464 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
8465 }
8466 };
8467
8468 let mut st = TestDsl::from(
8469 TestBuilder::new()
8470 .with_network(ironwood_active_network)
8471 .with_data_store_factory(ds_factory)
8472 .with_block_cache(cache)
8473 .with_account_from_sapling_activation(BlockHash([0; 32])),
8474 )
8475 .build::<OrchardPoolTester>();
8476
8477 st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60_000));
8479
8480 let to_extsk = OrchardPoolTester::sk(&[0xf5; 32]);
8483 let to = OrchardPoolTester::sk_default_address(&to_extsk);
8484 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
8485 to.to_zcash_address(st.network()),
8486 Zatoshis::const_from_u64(10_000),
8487 )])
8488 .unwrap();
8489
8490 let change_strategy =
8491 single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Orchard);
8492 let input_selector = GreedyInputSelector::new();
8493
8494 let account_id = st.get_account().id();
8495 let proposal = st
8496 .propose_transfer(
8497 account_id,
8498 &input_selector,
8499 &change_strategy,
8500 request,
8501 ConfirmationsPolicy::MIN,
8502 )
8503 .expect("proposal construction succeeds; the Orchard-receiver payment routes to Ironwood");
8504
8505 assert_eq!(
8507 proposal.steps().head.payment_pools().get(&0),
8508 Some(&PoolType::IRONWOOD),
8509 );
8510
8511 let pczt = st
8512 .create_pczt_from_proposal::<Infallible, _, Infallible>(
8513 account_id,
8514 OvkPolicy::Sender,
8515 &proposal,
8516 None,
8517 )
8518 .expect("an Ironwood-routed payment builds as a version 6 PCZT");
8519
8520 assert_eq!(
8522 *pczt.global().tx_version(),
8523 zcash_protocol::constants::V6_TX_VERSION,
8524 );
8525 assert!(
8526 !pczt.ironwood().actions().is_empty(),
8527 "the PCZT carries an Ironwood bundle for the Ironwood-routed payment",
8528 );
8529
8530 assert!(
8534 pczt.ironwood()
8535 .actions()
8536 .iter()
8537 .any(|action| !action.output().proprietary().is_empty()),
8538 "the Ironwood output carries recipient metadata",
8539 );
8540
8541 #[derive(Clone, Debug, PartialEq, Eq)]
8542 struct SpendState {
8543 has_fvk: bool,
8544 has_signature: bool,
8545 has_alpha: bool,
8546 has_derivation: bool,
8547 value: Option<u64>,
8548 }
8549
8550 fn spend_states(pczt: pczt::Pczt) -> (Vec<SpendState>, Vec<SpendState>) {
8551 fn for_bundle(bundle: &orchard::pczt::Bundle) -> Vec<SpendState> {
8552 bundle
8553 .actions()
8554 .iter()
8555 .map(|action| SpendState {
8556 has_fvk: action.spend().fvk().is_some(),
8557 has_signature: action.spend().spend_auth_sig().is_some(),
8558 has_alpha: action.spend().alpha().is_some(),
8559 has_derivation: action.spend().zip32_derivation().is_some(),
8560 value: action.spend().value().as_ref().map(|value| value.inner()),
8561 })
8562 .collect()
8563 }
8564
8565 let mut orchard = vec![];
8566 let mut ironwood = vec![];
8567 pczt::roles::verifier::Verifier::new(pczt)
8568 .with_orchard::<Infallible, _>(|bundle| {
8569 orchard = for_bundle(bundle);
8570 Ok(())
8571 })
8572 .unwrap()
8573 .with_ironwood::<Infallible, _>(|bundle| {
8574 ironwood = for_bundle(bundle);
8575 Ok(())
8576 })
8577 .unwrap();
8578 (orchard, ironwood)
8579 }
8580
8581 let original_sighash = Signer::new(pczt.clone()).unwrap().shielded_sighash();
8582 let original_spend_states = spend_states(pczt.clone());
8583
8584 for state in original_spend_states
8591 .0
8592 .iter()
8593 .chain(original_spend_states.1.iter())
8594 {
8595 assert!(
8596 state.has_signature || state.has_derivation,
8597 "every action must be either pre-signed or identifiable by an external signer \
8598 via ZIP 32 derivation: {state:?}",
8599 );
8600 }
8601 assert!(
8605 original_spend_states
8606 .0
8607 .iter()
8608 .any(|state| state.value == Some(0) && !state.has_signature),
8609 "expected at least one unsigned zero-value Orchard spend (the wallet-controlled \
8610 change-pairing dummy)",
8611 );
8612
8613 let original_len = pczt.clone().serialize().unwrap().len();
8614 let signer_view = redact_pczt_for_signer(&pczt, SignerView::Compact);
8615 let signer_view_bytes = signer_view.serialize().unwrap();
8616 assert!(signer_view_bytes.len() < original_len);
8617 let signer_view = pczt::Pczt::parse(&signer_view_bytes).unwrap();
8618 assert_eq!(spend_states(signer_view.clone()), original_spend_states);
8619
8620 assert!(
8623 signer_view
8624 .global()
8625 .proprietary()
8626 .get("zcash_client_backend:proposal_info")
8627 .is_none()
8628 );
8629 assert!(
8630 pczt.global()
8631 .proprietary()
8632 .contains_key("zcash_client_backend:proposal_info")
8633 );
8634
8635 assert!(signer_view.sapling().anchor().is_none());
8638 assert!(signer_view.orchard().anchor().is_none());
8639 assert!(signer_view.ironwood().anchor().is_none());
8640
8641 let assert_redacted_bundle =
8642 |original: &pczt::orchard::Bundle, redacted: &pczt::orchard::Bundle| {
8643 assert_eq!(redacted.actions().len(), original.actions().len());
8644 let mut memo_plaintexts = 0;
8645 for (redacted, original) in redacted.actions().iter().zip(original.actions()) {
8646 assert!(redacted.cv_net().is_none());
8647 assert!(redacted.output().cmx().is_none());
8648 assert_eq!(redacted.spend().nullifier(), original.spend().nullifier());
8649 assert_eq!(redacted.spend().rk(), original.spend().rk());
8650 assert_eq!(
8651 redacted.spend().spend_auth_sig(),
8652 original.spend().spend_auth_sig()
8653 );
8654 assert_eq!(
8655 redacted.output().ephemeral_key(),
8656 original.output().ephemeral_key()
8657 );
8658 assert_eq!(
8659 redacted.output().out_ciphertext(),
8660 original.output().out_ciphertext()
8661 );
8662 assert_eq!(
8663 redacted.output().user_address(),
8664 original.output().user_address()
8665 );
8666 assert!(
8667 redacted
8668 .output()
8669 .proprietary()
8670 .get("zcash_client_backend:output_info")
8671 .is_none()
8672 );
8673 if matches!(
8674 redacted.output().enc_ciphertext(),
8675 pczt::orchard::EncCiphertext::MemoPlaintext(_)
8676 ) {
8677 memo_plaintexts += 1;
8678 }
8679 }
8680 memo_plaintexts
8681 };
8682
8683 let memo_plaintexts = assert_redacted_bundle(pczt.orchard(), signer_view.orchard())
8684 + assert_redacted_bundle(pczt.ironwood(), signer_view.ironwood());
8685 assert!(memo_plaintexts > 0);
8686
8687 let full_view = redact_pczt_for_signer(&pczt, SignerView::Full);
8690 assert_eq!(
8691 Signer::new(full_view.clone()).unwrap().shielded_sighash(),
8692 original_sighash,
8693 );
8694 assert!(full_view.ironwood().actions().iter().all(|action| {
8695 action.spend().witness().is_none() && action.spend().dummy_sk().is_none()
8696 }));
8697 assert_eq!(full_view.ironwood().anchor(), pczt.ironwood().anchor());
8698 assert!(
8699 !full_view
8700 .global()
8701 .proprietary()
8702 .contains_key("zcash_client_backend:proposal_info")
8703 );
8704 for bundle in [full_view.orchard(), full_view.ironwood()] {
8705 assert!(bundle.actions().iter().all(|action| {
8706 !action
8707 .output()
8708 .proprietary()
8709 .contains_key("zcash_client_backend:output_info")
8710 }));
8711 }
8712 assert!(full_view.sapling().outputs().iter().all(|output| {
8713 !output
8714 .proprietary()
8715 .contains_key("zcash_client_backend:output_info")
8716 }));
8717 assert!(full_view.transparent().outputs().iter().all(|output| {
8718 !output
8719 .proprietary()
8720 .contains_key("zcash_client_backend:output_info")
8721 }));
8722
8723 let batch_view = redact_pczt_for_batch_signer(&pczt);
8724 let batch_view_bytes = batch_view.serialize().unwrap();
8725 assert!(batch_view_bytes.len() < signer_view_bytes.len());
8726 let batch_view = pczt::Pczt::parse(&batch_view_bytes).unwrap();
8727 let batch_spend_states = spend_states(batch_view.clone());
8728
8729 let mut preauthorized_actions = 0;
8730 let mut unsigned_zero_value_actions = 0;
8731 for (original_bundle, batch_bundle) in [
8732 (&original_spend_states.0, &batch_spend_states.0),
8733 (&original_spend_states.1, &batch_spend_states.1),
8734 ] {
8735 assert_eq!(batch_bundle.len(), original_bundle.len());
8736 for (original, batch) in original_bundle.iter().zip(batch_bundle) {
8737 assert!(!batch.has_fvk);
8738 assert!(!batch.has_signature);
8739 assert_eq!(
8740 batch.has_alpha,
8741 original.has_alpha && !original.has_signature
8742 );
8743
8744 if original.has_signature {
8745 preauthorized_actions += 1;
8746 } else if original.value == Some(0) && original.has_alpha {
8747 unsigned_zero_value_actions += 1;
8748 }
8749 }
8750 }
8751 assert!(preauthorized_actions > 0);
8752 assert!(unsigned_zero_value_actions > 0);
8753
8754 for compact_view in [&signer_view, &batch_view] {
8757 let mut resolved = (*compact_view).clone();
8758 resolved.resolve_fields().unwrap();
8759 for (resolved_bundle, original_bundle) in [
8760 (resolved.orchard(), pczt.orchard()),
8761 (resolved.ironwood(), pczt.ironwood()),
8762 ] {
8763 for (resolved, original) in resolved_bundle
8764 .actions()
8765 .iter()
8766 .zip(original_bundle.actions())
8767 {
8768 assert_eq!(resolved.cv_net(), original.cv_net());
8769 assert_eq!(resolved.output().cmx(), original.output().cmx());
8770 assert_eq!(
8771 resolved.output().enc_ciphertext(),
8772 original.output().enc_ciphertext()
8773 );
8774 }
8775 }
8776 assert_eq!(
8777 Signer::new((*compact_view).clone())
8778 .unwrap()
8779 .shielded_sighash(),
8780 original_sighash,
8781 );
8782 }
8783
8784 let signable_indices = |states: &[SpendState]| {
8785 states
8786 .iter()
8787 .enumerate()
8788 .filter_map(|(index, state)| (!state.has_signature && state.has_alpha).then_some(index))
8789 .collect::<Vec<_>>()
8790 };
8791 let orchard_signable = signable_indices(&original_spend_states.0);
8792 let ironwood_signable = signable_indices(&original_spend_states.1);
8793 assert!(
8794 orchard_signable
8795 .iter()
8796 .any(|&index| original_spend_states.0[index].value == Some(0))
8797 );
8798
8799 let request = pczt::roles::signer::batch::BatchSignRequest::new(vec![batch_view]);
8803 let request =
8804 pczt::roles::signer::batch::BatchSignRequest::parse(&request.serialize().unwrap()).unwrap();
8805 assert_eq!(request.pczts().len(), 1);
8806 let transported_view = request.pczts()[0].clone();
8807 assert_eq!(spend_states(transported_view.clone()), batch_spend_states);
8808
8809 let usk = st.get_account().usk().clone();
8810 let ask = orchard::keys::SpendAuthorizingKey::from(OrchardPoolTester::usk_to_sk(&usk));
8811 let signed_view = pczt::roles::low_level_signer::Signer::new(transported_view)
8812 .sign_orchard_with::<pczt::roles::low_level_signer::OrchardParseError, _>(|_, bundle, _| {
8813 for &index in &orchard_signable {
8814 bundle.actions_mut()[index]
8815 .sign(original_sighash, &ask, UnwrapErr(SysRng))
8816 .unwrap();
8817 }
8818 Ok(())
8819 })
8820 .unwrap()
8821 .sign_ironwood_with::<pczt::roles::low_level_signer::OrchardParseError, _>(
8822 |_, bundle, _| {
8823 for &index in &ironwood_signable {
8824 bundle.actions_mut()[index]
8825 .sign(original_sighash, &ask, UnwrapErr(SysRng))
8826 .unwrap();
8827 }
8828 Ok(())
8829 },
8830 )
8831 .unwrap()
8832 .finish();
8833
8834 let signatures = pczt::roles::signer::extract_orchard_spend_auth_signatures(&signed_view);
8835 let expected_signature_positions = orchard_signable
8836 .iter()
8837 .copied()
8838 .map(|index| (orchard::ValuePool::Orchard, index))
8839 .chain(
8840 ironwood_signable
8841 .iter()
8842 .copied()
8843 .map(|index| (orchard::ValuePool::Ironwood, index)),
8844 )
8845 .collect::<Vec<_>>();
8846 let signature_positions = signatures
8847 .iter()
8848 .map(|signature| (signature.value_pool(), signature.action_index()))
8849 .collect::<Vec<_>>();
8850 assert_eq!(signature_positions, expected_signature_positions);
8851
8852 let response = pczt::roles::signer::batch::BatchSignResponse::new(vec![signatures]);
8853 let response =
8854 pczt::roles::signer::batch::BatchSignResponse::parse(&response.serialize().unwrap())
8855 .unwrap();
8856 assert_eq!(response.signatures().len(), 1);
8857
8858 let mut signer = Signer::new(pczt.clone()).unwrap();
8859 for signature in &response.signatures()[0] {
8860 signer
8861 .apply_orchard_spend_auth_signature(signature)
8862 .unwrap();
8863 }
8864 let authorized = signer.finish();
8865 assert_eq!(
8866 authorized.global().proprietary(),
8867 pczt.global().proprietary()
8868 );
8869
8870 let authorized_spend_states = spend_states(authorized.clone());
8871 for (original_bundle, authorized_bundle) in [
8872 (&original_spend_states.0, &authorized_spend_states.0),
8873 (&original_spend_states.1, &authorized_spend_states.1),
8874 ] {
8875 for (original, authorized) in original_bundle.iter().zip(authorized_bundle) {
8876 assert_eq!(authorized.has_fvk, original.has_fvk);
8877 assert_eq!(authorized.has_alpha, original.has_alpha);
8878 assert!(authorized.has_signature);
8879 }
8880 }
8881 for (authorized, original) in authorized
8882 .orchard()
8883 .actions()
8884 .iter()
8885 .zip(pczt.orchard().actions())
8886 .chain(
8887 authorized
8888 .ironwood()
8889 .actions()
8890 .iter()
8891 .zip(pczt.ironwood().actions()),
8892 )
8893 {
8894 if original.spend().spend_auth_sig().is_some() {
8895 assert_eq!(
8896 authorized.spend().spend_auth_sig(),
8897 original.spend().spend_auth_sig()
8898 );
8899 }
8900 assert_eq!(
8901 authorized.output().proprietary(),
8902 original.output().proprietary()
8903 );
8904 }
8905
8906 let pczt_branch_id = consensus::BranchId::try_from(*authorized.global().consensus_branch_id())
8913 .expect("the PCZT carries a valid consensus branch ID");
8914 let orchard_pk = zcash_primitives::transaction::builder::cached_orchard_proving_key(
8915 zcash_primitives::transaction::components::orchard::bundle_version_for_branch(
8916 pczt_branch_id,
8917 ::orchard::ValuePool::Orchard,
8918 )
8919 .expect("the PCZT's consensus branch supports the Orchard pool")
8920 .circuit_version(),
8921 );
8922 let proven = Prover::new(authorized)
8923 .create_orchard_proof(UnwrapErr(SysRng), orchard_pk)
8924 .unwrap()
8925 .create_ironwood_proof(UnwrapErr(SysRng), orchard_pk)
8926 .unwrap()
8927 .finish();
8928
8929 let txid = st
8930 .extract_and_store_transaction_from_pczt(proven)
8931 .expect("extracts, verifies, and stores the finalized transaction");
8932
8933 let sent_outputs = st.wallet().get_sent_outputs(&txid).unwrap();
8936 assert!(
8937 sent_outputs.iter().any(|output| {
8938 output.value() == Zatoshis::const_from_u64(10_000)
8939 && output.external_recipient().is_some()
8940 }),
8941 "the Ironwood payment output is recorded among the sent outputs: {sent_outputs:?}",
8942 );
8943 assert!(
8944 !st.wallet()
8945 .get_sent_note_ids(&txid, ShieldedPool::Ironwood)
8946 .unwrap()
8947 .is_empty(),
8948 "the payment's sent-note record belongs to the Ironwood pool",
8949 );
8950}
8951
8952#[cfg(feature = "orchard")]
8957pub fn orchard_to_ironwood_payment_reports_net_value_delta<Dsf>(
8958 ds_factory: Dsf,
8959 cache: impl TestCache,
8960) where
8961 Dsf: DataStoreFactory,
8962{
8963 let ironwood_active_network = {
8965 let activation = BlockHeight::from_u32(100_000);
8966 LocalNetwork {
8967 nu6: Some(activation),
8968 nu6_1: Some(activation),
8969 nu6_2: Some(activation),
8970 nu6_3: Some(activation),
8971 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
8972 }
8973 };
8974
8975 let mut st = TestDsl::from(
8976 TestBuilder::new()
8977 .with_network(ironwood_active_network)
8978 .with_data_store_factory(ds_factory)
8979 .with_block_cache(cache)
8980 .with_account_from_sapling_activation(BlockHash([0; 32])),
8981 )
8982 .build::<OrchardPoolTester>();
8983
8984 let note_value = Zatoshis::const_from_u64(60_000);
8986 st.add_a_single_note_checking_balance(note_value);
8987
8988 let to_extsk = OrchardPoolTester::sk(&[0xf5; 32]);
8991 let to = OrchardPoolTester::sk_default_address(&to_extsk);
8992 let transfer_amount = Zatoshis::const_from_u64(10_000);
8993 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
8994 to.to_zcash_address(st.network()),
8995 transfer_amount,
8996 )])
8997 .unwrap();
8998
8999 let change_strategy =
9000 single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Orchard);
9001 let input_selector = GreedyInputSelector::new();
9002
9003 let account = st.get_account();
9004 let account_id = account.id();
9005 let usk = account.usk().clone();
9006 let proposal = st
9007 .propose_transfer(
9008 account_id,
9009 &input_selector,
9010 &change_strategy,
9011 request,
9012 ConfirmationsPolicy::MIN,
9013 )
9014 .expect("proposal construction succeeds; the Orchard-receiver payment routes to Ironwood");
9015
9016 assert_eq!(
9018 proposal.steps().head.payment_pools().get(&0),
9019 Some(&PoolType::IRONWOOD),
9020 );
9021 let fee = proposal.steps().head.balance().fee_required();
9022 let expected_change = (note_value - transfer_amount - fee).unwrap();
9023
9024 let txids = st
9025 .create_proposed_transactions::<Infallible, _, Infallible, _>(
9026 &usk,
9027 OvkPolicy::Sender,
9028 &proposal,
9029 )
9030 .expect("an Ironwood-routed payment builds successfully");
9031 let txid = *txids.first();
9032
9033 let expected_delta = -zcash_protocol::value::ZatBalance::from((transfer_amount + fee).unwrap());
9036 macro_rules! check_history {
9037 ($phase:literal) => {{
9038 let tx_history = st.wallet().get_tx_history().unwrap();
9039 let tx = tx_history
9040 .iter()
9041 .find(|tx| tx.txid() == txid)
9042 .expect("the created transaction appears in the transaction history");
9043 assert_eq!(
9044 tx.account_value_delta(),
9045 expected_delta,
9046 "account_value_delta ({}) reflects the payment plus fee",
9047 $phase,
9048 );
9049 assert_eq!(
9050 tx.total_spent(),
9051 note_value,
9052 "total_spent ({}) is the total value of the notes spent",
9053 $phase,
9054 );
9055 assert_eq!(
9056 tx.total_received(),
9057 expected_change,
9058 "total_received ({}) is the change returned to the wallet",
9059 $phase,
9060 );
9061 assert!(tx.has_change(), "the transaction has change ({})", $phase);
9062 assert_eq!(
9065 tx.pool_crossing_value(),
9066 None,
9067 "a payment to an external recipient is not a pool crossing ({})",
9068 $phase,
9069 );
9070 }};
9071 }
9072
9073 check_history!("before mining");
9075
9076 let (h, _) = st.generate_next_block_including(txid);
9078 st.scan_cached_blocks(h, 1);
9079 check_history!("after mining");
9080
9081 let tx = st
9085 .wallet()
9086 .get_transaction(txid)
9087 .unwrap()
9088 .expect("the created transaction can be retrieved");
9089 let network = *st.network();
9090 decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, Some(h)).unwrap();
9091 check_history!("after enhancement");
9092}
9093
9094#[cfg(feature = "orchard")]
9101pub fn orchard_to_ironwood_self_migration_reports_fee_only_delta<Dsf>(
9102 ds_factory: Dsf,
9103 cache: impl TestCache,
9104) where
9105 Dsf: DataStoreFactory,
9106{
9107 let ironwood_active_network = {
9109 let activation = BlockHeight::from_u32(100_000);
9110 LocalNetwork {
9111 nu6: Some(activation),
9112 nu6_1: Some(activation),
9113 nu6_2: Some(activation),
9114 nu6_3: Some(activation),
9115 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
9116 }
9117 };
9118
9119 let mut st = TestDsl::from(
9120 TestBuilder::new()
9121 .with_network(ironwood_active_network)
9122 .with_data_store_factory(ds_factory)
9123 .with_block_cache(cache)
9124 .with_account_from_sapling_activation(BlockHash([0; 32])),
9125 )
9126 .build::<OrchardPoolTester>();
9127
9128 let note_value = Zatoshis::const_from_u64(60_000);
9130 st.add_a_single_note_checking_balance(note_value);
9131
9132 let own_fvk = OrchardPoolTester::test_account_fvk(&st);
9135 let own_address = OrchardPoolTester::fvk_default_address(&own_fvk);
9136 let transfer_amount = Zatoshis::const_from_u64(40_000);
9137 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
9138 own_address.to_zcash_address(st.network()),
9139 transfer_amount,
9140 )])
9141 .unwrap();
9142
9143 let change_strategy =
9144 single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Orchard);
9145 let input_selector = GreedyInputSelector::new();
9146
9147 let account = st.get_account();
9148 let account_id = account.id();
9149 let usk = account.usk().clone();
9150 let proposal = st
9151 .propose_transfer(
9152 account_id,
9153 &input_selector,
9154 &change_strategy,
9155 request,
9156 ConfirmationsPolicy::MIN,
9157 )
9158 .expect("proposal construction succeeds; the Orchard-receiver payment routes to Ironwood");
9159
9160 assert_eq!(
9162 proposal.steps().head.payment_pools().get(&0),
9163 Some(&PoolType::IRONWOOD),
9164 );
9165 let fee = proposal.steps().head.balance().fee_required();
9166
9167 let txids = st
9168 .create_proposed_transactions::<Infallible, _, Infallible, _>(
9169 &usk,
9170 OvkPolicy::Sender,
9171 &proposal,
9172 )
9173 .expect("an Ironwood-routed payment builds successfully");
9174 let txid = *txids.first();
9175
9176 let (h, _) = st.generate_next_block_including(txid);
9180 st.scan_cached_blocks(h, 1);
9181
9182 macro_rules! check_history {
9183 ($phase:literal) => {{
9184 let tx_history = st.wallet().get_tx_history().unwrap();
9185 let tx = tx_history
9186 .iter()
9187 .find(|tx| tx.txid() == txid)
9188 .expect("the created transaction appears in the transaction history");
9189 assert_eq!(
9190 tx.total_spent(),
9191 note_value,
9192 "total_spent ({}) is the total value of the notes spent",
9193 $phase,
9194 );
9195 assert_eq!(
9196 tx.total_received(),
9197 (note_value - fee).unwrap(),
9198 "total_received ({}) covers both the migrated value and the change",
9199 $phase,
9200 );
9201 assert_eq!(
9202 tx.account_value_delta(),
9203 -zcash_protocol::value::ZatBalance::from(fee),
9204 "the migration ({}) reduces the account balance only by the fee",
9205 $phase,
9206 );
9207 assert!(
9213 tx.is_pool_crossing(),
9214 "a mined payment to the wallet's own address is a pool crossing ({})",
9215 $phase,
9216 );
9217 assert_eq!(
9218 tx.pool_crossing_value(),
9219 Some(transfer_amount),
9220 "pool_crossing_value ({}) is the migrated payment amount",
9221 $phase,
9222 );
9223 }};
9224 }
9225
9226 check_history!("after mining");
9227 assert_eq!(
9228 st.get_total_balance(account_id),
9229 (note_value - fee).unwrap(),
9230 "the account retains all funds except the fee",
9231 );
9232
9233 let tx = st
9237 .wallet()
9238 .get_transaction(txid)
9239 .unwrap()
9240 .expect("the created transaction can be retrieved");
9241 let network = *st.network();
9242 decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, Some(h)).unwrap();
9243 check_history!("after enhancement");
9244}
9245
9246#[cfg(feature = "orchard")]
9251pub fn proposal_records_and_serializes_proposed_version<Dsf>(ds_factory: Dsf, cache: impl TestCache)
9252where
9253 Dsf: DataStoreFactory,
9254{
9255 let mut st = TestDsl::from(
9256 TestBuilder::new()
9257 .with_data_store_factory(ds_factory)
9258 .with_block_cache(cache)
9259 .with_account_from_sapling_activation(BlockHash([0; 32])),
9260 )
9261 .build::<OrchardPoolTester>();
9262
9263 st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60_000));
9265
9266 let to_extsk = OrchardPoolTester::sk(&[0xf5; 32]);
9267 let to = OrchardPoolTester::sk_default_address(&to_extsk);
9268 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
9269 to.to_zcash_address(st.network()),
9270 Zatoshis::const_from_u64(10_000),
9271 )])
9272 .unwrap();
9273
9274 let change_strategy =
9275 single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Orchard);
9276 let input_selector = GreedyInputSelector::new();
9277
9278 let account_id = st.get_account().id();
9279 let network = *st.network();
9280 let proposal = propose_transfer::<_, _, _, _, Infallible>(
9282 st.wallet_mut(),
9283 &network,
9284 account_id,
9285 &input_selector,
9286 &change_strategy,
9287 request,
9288 ConfirmationsPolicy::MIN,
9289 &SpendPolicy::default(),
9290 None,
9291 Some(TxVersion::V5),
9292 )
9293 .expect("proposal construction succeeds");
9294
9295 assert_eq!(proposal.proposed_version(), Some(TxVersion::V5));
9297
9298 let proto = crate::proto::proposal::Proposal::from_standard_proposal(&proposal);
9300 let decoded = proto
9301 .try_into_standard_proposal(&network, st.wallet())
9302 .expect("the serialized proposal decodes");
9303 assert_eq!(decoded.proposed_version(), Some(TxVersion::V5));
9304
9305 let mut legacy_proto = crate::proto::proposal::Proposal::from_standard_proposal(&proposal);
9308 legacy_proto.proposed_version = None;
9309 let decoded_legacy = legacy_proto
9310 .try_into_standard_proposal(&network, st.wallet())
9311 .expect("a legacy proposal without a requested version must decode");
9312 assert_eq!(decoded_legacy.proposed_version(), None);
9313}
9314
9315#[cfg(feature = "orchard")]
9320pub fn canonical_crossing_is_bucketed_and_unpadded<Dsf: DataStoreFactory>(
9321 ds_factory: Dsf,
9322 cache: impl TestCache,
9323) {
9324 let interval = AnchorBucketInterval::custom(NonZeroU32::new(12).expect("nonzero"));
9326 let activation = BlockHeight::from_u32(100_000);
9327 let ironwood_active_network = LocalNetwork {
9328 nu6: Some(activation),
9329 nu6_1: Some(activation),
9330 nu6_2: Some(activation),
9331 nu6_3: Some(activation),
9332 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
9333 };
9334
9335 let mut st = TestDsl::from(
9336 TestBuilder::new()
9337 .with_network(ironwood_active_network)
9338 .with_data_store_factory(ds_factory)
9339 .with_block_cache(cache)
9340 .with_anchor_retention_interval(interval)
9341 .with_account_from_sapling_activation(BlockHash([0; 32])),
9342 )
9343 .build::<OrchardPoolTester>();
9344
9345 let account = st.test_account().cloned().unwrap();
9346 let fvk = OrchardPoolTester::test_account_fvk(&st);
9347 let recipient = OrchardPoolTester::fvk_default_address(&fvk).to_zcash_address(st.network());
9348
9349 let note_value = Zatoshis::const_from_u64(10_000_000);
9351 let (received_height, _, _) = st.add_a_single_note_checking_balance(note_value);
9352
9353 let received = u32::from(received_height);
9359 let interval_blocks = interval.block_count().get();
9360 let tip = u32::from(anchor_grid(st.network(), interval).boundary_at_or_above(received_height))
9363 + 3 * interval_blocks
9364 + 5;
9365 let filler_count = tip - received;
9366
9367 let not_our_fvk = OrchardPoolTester::sk_to_fvk(&OrchardPoolTester::sk(&[0xf5; 32]));
9370 for _ in 0..filler_count {
9371 st.generate_next_block(
9372 ¬_our_fvk,
9373 AddressType::DefaultExternal,
9374 Zatoshis::const_from_u64(10_000),
9375 );
9376 }
9377 st.scan_cached_blocks(received_height + 1, filler_count as usize);
9378
9379 let input_selector = GreedyInputSelector::new();
9380 let change_strategy =
9381 single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Orchard);
9382
9383 let propose = |st: &mut TestState<_, _, _>, amount: Zatoshis| {
9384 let request =
9385 TransactionRequest::new(vec![Payment::without_memo(recipient.clone(), amount)])
9386 .unwrap();
9387 st.propose_transfer(
9388 account.id(),
9389 &input_selector,
9390 &change_strategy,
9391 request,
9392 ConfirmationsPolicy::MIN,
9393 )
9394 };
9395
9396 let canonical = propose(&mut st, MAX_RESIDUAL_VALUE).expect("the wallet can fund this");
9398 assert_eq!(canonical.steps().len(), 1);
9399 let canonical_fee = crate::fees::canonical_crossing_fee(
9400 st.network(),
9401 BlockHeight::from(canonical.min_target_height()),
9402 )
9403 .expect("the canonical shape is a valid input to the ZIP 317 rule");
9404
9405 let step = canonical.steps().first();
9406 let anchor = step
9407 .anchor_height()
9408 .expect("a shielded step binds an anchor");
9409 assert!(
9410 anchor_grid(st.network(), interval).is_boundary(anchor),
9411 "a canonical crossing must anchor to a grid boundary, got {anchor:?}"
9412 );
9413 assert_eq!(step.input_count_in_pool(PoolType::ORCHARD), 1);
9414 assert_eq!(step.input_count_in_pool(PoolType::IRONWOOD), 0);
9415 assert!(step.is_canonical_crossing(
9416 &st.wallet().pool_migration_params(st.network()),
9417 canonical_fee
9418 ));
9419 assert_eq!(
9420 step.ironwood_action_count(
9421 step.ironwood_bundle_padding(),
9422 ::orchard::bundle::BundleVersion::ironwood_v3()
9423 ),
9424 Ok(1),
9425 "the Ironwood bundle must be a single unpadded action"
9426 );
9427
9428 let off_by_one = propose(
9430 &mut st,
9431 (MAX_RESIDUAL_VALUE + Zatoshis::const_from_u64(1)).unwrap(),
9432 )
9433 .unwrap();
9434 let step = off_by_one.steps().first();
9435 assert!(!step.is_canonical_crossing(
9436 &st.wallet().pool_migration_params(st.network()),
9437 canonical_fee
9438 ));
9439 assert_eq!(
9440 step.ironwood_action_count(
9441 step.ironwood_bundle_padding(),
9442 ::orchard::bundle::BundleVersion::ironwood_v3()
9443 ),
9444 Ok(2)
9445 );
9446 assert!(
9447 !anchor_grid(st.network(), interval).is_boundary(step.anchor_height().unwrap()),
9448 "a non-canonical payment must not pay for a bucketed anchor"
9449 );
9450
9451 assert_eq!(
9458 canonical
9459 .steps()
9460 .first()
9461 .balance()
9462 .dummy_outputs()
9463 .expect("the fee model records dummy outputs")
9464 .ironwood(),
9465 0,
9466 "a canonical crossing has no Ironwood dummy output"
9467 );
9468 assert_eq!(
9469 canonical.steps().first().ironwood_bundle_padding(),
9470 BundlePadding::UNPADDED,
9471 "and the step must report the recorded value to the builder"
9472 );
9473
9474 let proto = crate::proto::proposal::Proposal::from_standard_proposal(&canonical);
9477 let serialized_dummy_outputs = proto.steps[0]
9478 .balance
9479 .as_ref()
9480 .expect("a proposal step carries a balance")
9481 .dummy_outputs
9482 .as_ref()
9483 .expect("new proposals serialize their dummy outputs");
9484 assert_eq!(
9485 (
9486 serialized_dummy_outputs.sapling,
9487 serialized_dummy_outputs.orchard,
9488 serialized_dummy_outputs.ironwood,
9489 ),
9490 (0, 1, 0)
9491 );
9492 let canonical = proto
9493 .try_into_standard_proposal(st.network(), st.wallet())
9494 .expect("the canonical proposal must deserialize");
9495 assert_eq!(
9496 canonical
9497 .steps()
9498 .first()
9499 .balance()
9500 .dummy_outputs()
9501 .expect("dummy outputs survive proposal decoding")
9502 .ironwood(),
9503 0,
9504 "the PCZT proposal round-trip must preserve the Ironwood dummy-output count"
9505 );
9506
9507 let txids = st.create_proposed_expecting(&canonical, 1);
9508 let built = st
9509 .wallet()
9510 .get_transaction(txids[0])
9511 .unwrap()
9512 .expect("the transaction was stored");
9513 assert_eq!(
9514 built
9515 .ironwood_bundle()
9516 .expect("a crossing carries an Ironwood bundle")
9517 .actions()
9518 .len(),
9519 1,
9520 "the BUILT Ironwood bundle must have the single action the fee was charged for"
9521 );
9522
9523 let tx = st.get_tx_from_history(txids[0]).unwrap().unwrap();
9524 assert_eq!(
9525 tx.expiry_height(),
9526 Some(zcash_protocol::zip318::expiry_height(
9527 &zcash_protocol::zip318::BlockSpacing::for_network(st.network()),
9528 BlockHeight::from(canonical.min_target_height())
9529 )),
9530 "a canonical crossing must carry the ZIP 318 rolling expiry"
9531 );
9532 assert_eq!(
9533 tx.fee_paid(),
9534 Some(canonical_fee),
9535 "and the canonical ZIP 317 fee that shape costs"
9536 );
9537}
9538
9539#[cfg(feature = "orchard")]
9545pub fn canonical_crossing_builds_at_empty_boundary_block<Dsf: DataStoreFactory>(
9546 ds_factory: Dsf,
9547 cache: impl TestCache,
9548) {
9549 let interval = AnchorRetentionInterval::custom(NonZeroU32::new(12).expect("nonzero"));
9551 let activation = BlockHeight::from_u32(100_000);
9552 let ironwood_active_network = LocalNetwork {
9553 nu6: Some(activation),
9554 nu6_1: Some(activation),
9555 nu6_2: Some(activation),
9556 nu6_3: Some(activation),
9557 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
9558 };
9559
9560 let mut st = TestDsl::from(
9561 TestBuilder::new()
9562 .with_network(ironwood_active_network)
9563 .with_data_store_factory(ds_factory)
9564 .with_block_cache(cache)
9565 .with_anchor_retention_interval(interval)
9566 .with_account_from_sapling_activation(BlockHash([0; 32])),
9567 )
9568 .build::<OrchardPoolTester>();
9569
9570 let account = st.test_account().cloned().unwrap();
9571 let fvk = OrchardPoolTester::test_account_fvk(&st);
9572 let recipient = OrchardPoolTester::fvk_default_address(&fvk).to_zcash_address(st.network());
9573
9574 let note_value = Zatoshis::const_from_u64(10_000_000);
9576 let (received_height, _, _) = st.add_a_single_note_checking_balance(note_value);
9577 let received = u32::from(received_height);
9578 let interval_blocks = interval.block_count().get();
9579
9580 let tip = u32::from(anchor_grid(st.network(), interval).boundary_at_or_above(received_height))
9586 + 3 * interval_blocks
9587 + 5;
9588 let not_our_fvk = OrchardPoolTester::sk_to_fvk(&OrchardPoolTester::sk(&[0xf5; 32]));
9589 let mut empty_heights = Vec::new();
9590 for height in (received + 1)..=tip {
9591 if anchor_grid(st.network(), interval).is_boundary(BlockHeight::from_u32(height)) {
9592 let (h, _) = st.generate_empty_block();
9593 empty_heights.push(h);
9594 } else {
9595 st.generate_next_block(
9596 ¬_our_fvk,
9597 AddressType::DefaultExternal,
9598 Zatoshis::const_from_u64(10_000),
9599 );
9600 }
9601 }
9602 st.scan_cached_blocks(received_height + 1, (tip - received) as usize);
9605
9606 let input_selector = GreedyInputSelector::new();
9607 let change_strategy =
9608 single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Orchard);
9609 let request = TransactionRequest::new(vec![Payment::without_memo(
9610 recipient.clone(),
9611 MAX_RESIDUAL_VALUE,
9612 )])
9613 .unwrap();
9614 let canonical = st
9615 .propose_transfer(
9616 account.id(),
9617 &input_selector,
9618 &change_strategy,
9619 request,
9620 ConfirmationsPolicy::MIN,
9621 )
9622 .expect("the wallet can fund this");
9623
9624 let canonical_fee = crate::fees::canonical_crossing_fee(
9625 st.network(),
9626 BlockHeight::from(canonical.min_target_height()),
9627 )
9628 .expect("the canonical shape is a valid input to the ZIP 317 rule");
9629 let step = canonical.steps().first();
9630 let anchor = step
9631 .anchor_height()
9632 .expect("a shielded step binds an anchor");
9633 assert!(
9634 step.is_canonical_crossing(
9635 &st.wallet().pool_migration_params(st.network()),
9636 canonical_fee
9637 ),
9638 "the payment must be recognized as a canonical crossing"
9639 );
9640 assert!(
9641 empty_heights.contains(&anchor),
9642 "the bucketed anchor {anchor:?} must be one of the empty boundary blocks {empty_heights:?}"
9643 );
9644
9645 let txids = st.create_proposed_expecting(&canonical, 1);
9648 let built = st
9649 .wallet()
9650 .get_transaction(txids[0])
9651 .unwrap()
9652 .expect("the transaction was stored");
9653 assert_eq!(
9654 built
9655 .ironwood_bundle()
9656 .expect("a crossing carries an Ironwood bundle")
9657 .actions()
9658 .len(),
9659 1,
9660 "the BUILT Ironwood bundle must have the single action of the canonical shape"
9661 );
9662}
9663
9664#[cfg(feature = "orchard")]
9672pub fn multi_note_crossing_is_not_bucketed<Dsf: DataStoreFactory>(
9673 ds_factory: Dsf,
9674 cache: impl TestCache,
9675) {
9676 let interval = AnchorBucketInterval::custom(NonZeroU32::new(12).expect("nonzero"));
9677 let activation = BlockHeight::from_u32(100_000);
9678 let network = LocalNetwork {
9679 nu6: Some(activation),
9680 nu6_1: Some(activation),
9681 nu6_2: Some(activation),
9682 nu6_3: Some(activation),
9683 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
9684 };
9685
9686 let mut st = TestDsl::from(
9687 TestBuilder::new()
9688 .with_network(network)
9689 .with_data_store_factory(ds_factory)
9690 .with_block_cache(cache)
9691 .with_anchor_retention_interval(interval)
9692 .with_account_from_sapling_activation(BlockHash([0; 32])),
9693 )
9694 .build::<OrchardPoolTester>();
9695
9696 let account = st.test_account().cloned().unwrap();
9697 let fvk = OrchardPoolTester::test_account_fvk(&st);
9698 let recipient = OrchardPoolTester::fvk_default_address(&fvk).to_zcash_address(st.network());
9699
9700 let note_value = Zatoshis::const_from_u64(400_000);
9702 let (first_height, _, _) = st.add_a_single_note_checking_balance(note_value);
9703 for _ in 0..2 {
9704 st.generate_next_block(&fvk, AddressType::DefaultExternal, note_value);
9705 }
9706 st.scan_cached_blocks(first_height + 1, 2);
9707
9708 let interval_blocks = interval.block_count().get();
9709 let tip = u32::from(anchor_grid(st.network(), interval).boundary_at_or_above(first_height))
9710 + 3 * interval_blocks
9711 + 5;
9712 let not_our_fvk = OrchardPoolTester::sk_to_fvk(&OrchardPoolTester::sk(&[0xf5; 32]));
9713 let filler_count = tip - u32::from(first_height) - 2;
9714 for _ in 0..filler_count {
9715 st.generate_next_block(
9716 ¬_our_fvk,
9717 AddressType::DefaultExternal,
9718 Zatoshis::const_from_u64(10_000),
9719 );
9720 }
9721 st.scan_cached_blocks(first_height + 3, filler_count as usize);
9722
9723 let input_selector = GreedyInputSelector::new();
9724 let change_strategy =
9725 single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Orchard);
9726 let request =
9727 TransactionRequest::new(vec![Payment::without_memo(recipient, MAX_RESIDUAL_VALUE)])
9728 .unwrap();
9729
9730 let proposal = st
9731 .propose_transfer(
9732 account.id(),
9733 &input_selector,
9734 &change_strategy,
9735 request,
9736 ConfirmationsPolicy::MIN,
9737 )
9738 .expect("three notes together can fund the payment");
9739
9740 let step = proposal.steps().first();
9741 let zip318 = st.wallet().pool_migration_params(st.network());
9742 let canonical_fee = crate::fees::canonical_crossing_fee(
9743 st.network(),
9744 BlockHeight::from(proposal.min_target_height()),
9745 )
9746 .expect("the canonical shape is a valid input to the ZIP 317 rule");
9747 assert!(
9748 step.input_count_in_pool(PoolType::ORCHARD) > 1,
9749 "this scenario is only meaningful if funding needs several notes"
9750 );
9751 assert!(!step.is_canonical_crossing(&zip318, canonical_fee));
9752 assert!(
9753 !anchor_grid(st.network(), interval).is_boundary(step.anchor_height().unwrap()),
9754 "a multi-input transaction must not pay for a bucketed anchor"
9755 );
9756 assert_eq!(
9757 step.ironwood_action_count(
9758 step.ironwood_bundle_padding(),
9759 ::orchard::bundle::BundleVersion::ironwood_v3()
9760 ),
9761 Ok(2)
9762 );
9763}
9764
9765#[cfg(feature = "orchard")]
9777pub fn canonical_crossing_prefers_single_note<Dsf: DataStoreFactory>(
9778 ds_factory: Dsf,
9779 cache: impl TestCache,
9780) {
9781 let interval = AnchorRetentionInterval::custom(NonZeroU32::new(12).expect("nonzero"));
9782 let activation = BlockHeight::from_u32(100_000);
9783 let network = LocalNetwork {
9784 nu6: Some(activation),
9785 nu6_1: Some(activation),
9786 nu6_2: Some(activation),
9787 nu6_3: Some(activation),
9788 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
9789 };
9790
9791 let mut st = TestDsl::from(
9792 TestBuilder::new()
9793 .with_network(network)
9794 .with_data_store_factory(ds_factory)
9795 .with_block_cache(cache)
9796 .with_anchor_retention_interval(interval)
9797 .with_account_from_sapling_activation(BlockHash([0; 32])),
9798 )
9799 .build::<OrchardPoolTester>();
9800
9801 let account = st.test_account().cloned().unwrap();
9802 let fvk = OrchardPoolTester::test_account_fvk(&st);
9803 let recipient = OrchardPoolTester::fvk_default_address(&fvk).to_zcash_address(st.network());
9804
9805 let small_value = Zatoshis::const_from_u64(600_000);
9809 let large_value = Zatoshis::const_from_u64(1_200_000);
9810 let (first_height, _, _) = st.add_a_single_note_checking_balance(small_value);
9811 st.generate_next_block(&fvk, AddressType::DefaultExternal, small_value);
9812 st.generate_next_block(&fvk, AddressType::DefaultExternal, large_value);
9813 st.scan_cached_blocks(first_height + 1, 2);
9814
9815 let interval_blocks = interval.block_count().get();
9816 let tip = u32::from(anchor_grid(st.network(), interval).boundary_at_or_above(first_height))
9817 + 3 * interval_blocks
9818 + 5;
9819 let not_our_fvk = OrchardPoolTester::sk_to_fvk(&OrchardPoolTester::sk(&[0xf5; 32]));
9820 let filler_count = tip - u32::from(first_height) - 2;
9821 for _ in 0..filler_count {
9822 st.generate_next_block(
9823 ¬_our_fvk,
9824 AddressType::DefaultExternal,
9825 Zatoshis::const_from_u64(10_000),
9826 );
9827 }
9828 st.scan_cached_blocks(first_height + 3, filler_count as usize);
9829
9830 let input_selector = GreedyInputSelector::new();
9831 let change_strategy =
9832 single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Orchard);
9833 let propose = |st: &mut TestState<_, _, _>, amount: Zatoshis| {
9834 let request =
9835 TransactionRequest::new(vec![Payment::without_memo(recipient.clone(), amount)])
9836 .unwrap();
9837 st.propose_transfer(
9838 account.id(),
9839 &input_selector,
9840 &change_strategy,
9841 request,
9842 ConfirmationsPolicy::MIN,
9843 )
9844 };
9845
9846 let canonical = propose(&mut st, MAX_RESIDUAL_VALUE).expect("the wallet can fund this");
9848 let step = canonical.steps().first();
9849 let zip318 = st.wallet().pool_migration_params(st.network());
9850 let canonical_fee = crate::fees::canonical_crossing_fee(
9851 st.network(),
9852 BlockHeight::from(canonical.min_target_height()),
9853 )
9854 .expect("the canonical shape is a valid input to the ZIP 317 rule");
9855 assert_eq!(
9856 step.input_count_in_pool(PoolType::ORCHARD),
9857 1,
9858 "the canonical payment must be funded from a single note"
9859 );
9860 assert!(step.is_canonical_crossing(&zip318, canonical_fee));
9861 let spent = step
9862 .shielded_inputs()
9863 .expect("a shielded step spends notes")
9864 .notes()
9865 .first()
9866 .note()
9867 .value();
9868 assert_eq!(
9869 spent, large_value,
9870 "the single covering note funds the payment, not the older small notes"
9871 );
9872
9873 let ordinary = propose(
9875 &mut st,
9876 (MAX_RESIDUAL_VALUE + Zatoshis::const_from_u64(1)).unwrap(),
9877 )
9878 .expect("the wallet can fund this too");
9879 let step = ordinary.steps().first();
9880 assert!(
9881 step.input_count_in_pool(PoolType::ORCHARD) > 1,
9882 "single-note preference must not leak into ordinary selection"
9883 );
9884}
9885
9886#[cfg(feature = "orchard")]
9902pub fn canonical_crossing_abandoned_without_anchor_checkpoint<Dsf, TC>(
9903 ds_factory: Dsf,
9904 cache: TC,
9905 remove_checkpoint: impl FnOnce(&mut TestState<TC, Dsf::DataStore, LocalNetwork>, BlockHeight),
9906) where
9907 Dsf: DataStoreFactory,
9908 TC: TestCache,
9909{
9910 let interval = AnchorRetentionInterval::custom(NonZeroU32::new(12).expect("nonzero"));
9911 let activation = BlockHeight::from_u32(100_000);
9912 let network = LocalNetwork {
9913 nu6: Some(activation),
9914 nu6_1: Some(activation),
9915 nu6_2: Some(activation),
9916 nu6_3: Some(activation),
9917 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
9918 };
9919
9920 let mut st = TestDsl::from(
9921 TestBuilder::new()
9922 .with_network(network)
9923 .with_data_store_factory(ds_factory)
9924 .with_block_cache(cache)
9925 .with_anchor_retention_interval(interval)
9926 .with_account_from_sapling_activation(BlockHash([0; 32])),
9927 )
9928 .build::<OrchardPoolTester>();
9929
9930 let account = st.test_account().cloned().unwrap();
9931 let fvk = OrchardPoolTester::test_account_fvk(&st);
9932 let recipient = OrchardPoolTester::fvk_default_address(&fvk).to_zcash_address(st.network());
9933
9934 let (received_height, _, _) =
9937 st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(10_000_000));
9938 let received = u32::from(received_height);
9939 let interval_blocks = interval.block_count().get();
9940 let tip = u32::from(anchor_grid(st.network(), interval).boundary_at_or_above(received_height))
9941 + 3 * interval_blocks
9942 + 5;
9943 let not_our_fvk = OrchardPoolTester::sk_to_fvk(&OrchardPoolTester::sk(&[0xf5; 32]));
9944 let filler_count = tip - received;
9945 for _ in 0..filler_count {
9946 st.generate_next_block(
9947 ¬_our_fvk,
9948 AddressType::DefaultExternal,
9949 Zatoshis::const_from_u64(10_000),
9950 );
9951 }
9952 st.scan_cached_blocks(received_height + 1, filler_count as usize);
9953
9954 let input_selector = GreedyInputSelector::new();
9955 let change_strategy =
9956 single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Orchard);
9957 let propose = |st: &mut TestState<_, _, _>| {
9958 let request = TransactionRequest::new(vec![Payment::without_memo(
9959 recipient.clone(),
9960 MAX_RESIDUAL_VALUE,
9961 )])
9962 .unwrap();
9963 st.propose_transfer(
9964 account.id(),
9965 &input_selector,
9966 &change_strategy,
9967 request,
9968 ConfirmationsPolicy::MIN,
9969 )
9970 .expect("the wallet can fund this")
9971 };
9972
9973 let canonical = propose(&mut st);
9978 let step = canonical.steps().first();
9979 let zip318 = st.wallet().pool_migration_params(st.network());
9980 let canonical_fee = crate::fees::canonical_crossing_fee(
9981 st.network(),
9982 BlockHeight::from(canonical.min_target_height()),
9983 )
9984 .expect("the canonical shape is a valid input to the ZIP 317 rule");
9985 assert!(step.is_canonical_crossing(&zip318, canonical_fee));
9986 let boundary = step
9987 .anchor_height()
9988 .expect("a shielded step binds an anchor");
9989 assert!(
9990 st.wallet()
9991 .anchor_computable(ShieldedPool::Orchard, boundary)
9992 .unwrap(),
9993 "an anchor is computable at the boundary before removal"
9994 );
9995
9996 remove_checkpoint(&mut st, boundary);
9998 assert!(
9999 !st.wallet()
10000 .anchor_computable(ShieldedPool::Orchard, boundary)
10001 .unwrap(),
10002 "no anchor is computable at the boundary after removal"
10003 );
10004
10005 let fallback = propose(&mut st);
10009 let step = fallback.steps().first();
10010 assert!(
10011 !step.is_canonical_crossing(&zip318, canonical_fee),
10012 "the attempt must be abandoned when its anchor cannot be proved"
10013 );
10014 assert_ne!(
10015 step.anchor_height()
10016 .expect("a shielded step binds an anchor"),
10017 boundary,
10018 "the fallback anchors at the ordinary height, not the unprovable boundary"
10019 );
10020 st.create_proposed_expecting(&fallback, 1);
10021}
10022
10023#[cfg(feature = "orchard")]
10031pub fn self_migration_keeps_spending_orchard<Dsf: DataStoreFactory>(
10032 ds_factory: Dsf,
10033 cache: impl TestCache,
10034) {
10035 let interval = AnchorBucketInterval::custom(NonZeroU32::new(12).expect("nonzero"));
10036 let activation = BlockHeight::from_u32(100_000);
10037 let network = LocalNetwork {
10038 nu6: Some(activation),
10039 nu6_1: Some(activation),
10040 nu6_2: Some(activation),
10041 nu6_3: Some(activation),
10042 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
10043 };
10044
10045 let mut st = TestDsl::from(
10046 TestBuilder::new()
10047 .with_network(network)
10048 .with_data_store_factory(ds_factory)
10049 .with_block_cache(cache)
10050 .with_anchor_retention_interval(interval)
10051 .with_account_from_sapling_activation(BlockHash([0; 32])),
10052 )
10053 .build::<OrchardPoolTester>();
10054
10055 let account = st.test_account().cloned().unwrap();
10056 let fvk = OrchardPoolTester::test_account_fvk(&st);
10057 let recipient = OrchardPoolTester::fvk_default_address(&fvk).to_zcash_address(st.network());
10058
10059 let (received_height, _, _) =
10060 st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(10_000_000));
10061
10062 let interval_blocks = interval.block_count().get();
10063 let not_our_fvk = OrchardPoolTester::sk_to_fvk(&OrchardPoolTester::sk(&[0xf5; 32]));
10064 let advance = |st: &mut TestState<_, _, _>, from: BlockHeight, count: u32| {
10065 for _ in 0..count {
10066 st.generate_next_block(
10067 ¬_our_fvk,
10068 AddressType::DefaultExternal,
10069 Zatoshis::const_from_u64(10_000),
10070 );
10071 }
10072 st.scan_cached_blocks(from, count as usize);
10073 };
10074
10075 let tip = u32::from(anchor_grid(st.network(), interval).boundary_at_or_above(received_height))
10076 + 3 * interval_blocks
10077 + 5;
10078 advance(
10079 &mut st,
10080 received_height + 1,
10081 tip - u32::from(received_height),
10082 );
10083
10084 let input_selector = GreedyInputSelector::new();
10085 let change_strategy =
10086 single_output_change_strategy(StandardFeeRule::Zip317, None, ShieldedPool::Orchard);
10087 let zip318 = st.wallet().pool_migration_params(st.network());
10088
10089 let request = TransactionRequest::new(vec![Payment::without_memo(
10091 recipient.clone(),
10092 MAX_RESIDUAL_VALUE,
10093 )])
10094 .unwrap();
10095 let first = st
10096 .propose_transfer(
10097 account.id(),
10098 &input_selector,
10099 &change_strategy,
10100 request,
10101 ConfirmationsPolicy::MIN,
10102 )
10103 .expect("the wallet can fund the first crossing");
10104 let canonical_fee = crate::fees::canonical_crossing_fee(
10105 st.network(),
10106 BlockHeight::from(first.min_target_height()),
10107 )
10108 .expect("the canonical shape is a valid input to the ZIP 317 rule");
10109 assert!(
10110 first
10111 .steps()
10112 .first()
10113 .is_canonical_crossing(&zip318, canonical_fee)
10114 );
10115
10116 let txids = st.create_proposed_expecting(&first, 1);
10118 let (mined_height, _) = st.generate_next_block_including(txids[0]);
10119 st.scan_cached_blocks(mined_height, 1);
10120
10121 advance(&mut st, mined_height + 1, 2 * interval_blocks + 1);
10123
10124 let summary = st
10127 .get_wallet_summary(ConfirmationsPolicy::MIN)
10128 .expect("the wallet is synced");
10129 let balances = summary
10130 .account_balances()
10131 .get(&account.id())
10132 .expect("the account is known");
10133 assert!(
10134 balances.ironwood_balance().total().is_positive(),
10135 "the first crossing must have left an Ironwood note for the selector to prefer"
10136 );
10137 assert!(
10138 balances.orchard_balance().total().is_positive(),
10139 "and Orchard change to fund the second crossing from"
10140 );
10141
10142 let request =
10144 TransactionRequest::new(vec![Payment::without_memo(recipient, MAX_RESIDUAL_VALUE)])
10145 .unwrap();
10146 let second = st
10147 .propose_transfer(
10148 account.id(),
10149 &input_selector,
10150 &change_strategy,
10151 request,
10152 ConfirmationsPolicy::MIN,
10153 )
10154 .expect("the wallet can fund the second crossing");
10155
10156 let step = second.steps().first();
10157 assert_eq!(
10158 step.input_count_in_pool(PoolType::IRONWOOD),
10159 0,
10160 "the Ironwood note must be left alone, or self-migration stalls here"
10161 );
10162 assert_eq!(step.input_count_in_pool(PoolType::ORCHARD), 1);
10163 assert!(
10164 step.is_canonical_crossing(&zip318, canonical_fee),
10165 "the second crossing must still be canonical"
10166 );
10167}
10168
10169pub fn create_to_address_respects_recipient_expiry<T: ShieldedPoolTester>(
10175 dsf: impl DataStoreFactory,
10176 cache: impl TestCache,
10177) {
10178 let mut st = TestDsl::with_sapling_birthday_account(dsf, cache).build::<T>();
10179
10180 st.add_notes_checking_balance([
10183 [Zatoshis::const_from_u64(100_000)],
10184 [Zatoshis::const_from_u64(100_000)],
10185 [Zatoshis::const_from_u64(100_000)],
10186 ]);
10187
10188 let account = st.test_account().cloned().unwrap();
10189 let birthday = account.birthday().height();
10190 let chain_tip = st.wallet().chain_height().unwrap().unwrap();
10191
10192 let (default_addr, _) = account
10195 .usk()
10196 .to_unified_full_viewing_key()
10197 .default_address(UnifiedAddressRequest::AllAvailableKeys)
10198 .unwrap();
10199 let with_expiry = |expiry_height: Option<BlockHeight>, expiry_time: Option<u64>| {
10200 UnifiedAddress::from_receivers(
10201 #[cfg(feature = "orchard")]
10202 default_addr.orchard().copied(),
10203 default_addr.sapling().copied(),
10204 default_addr.transparent().copied(),
10205 expiry_height,
10206 expiry_time,
10207 )
10208 .expect("the default address has at least one receiver")
10209 };
10210
10211 let payment_value = Zatoshis::const_from_u64(20_000);
10212 let change_strategy =
10213 single_output_change_strategy(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL);
10214 let input_selector = GreedyInputSelector::new();
10215
10216 let expired_by_height = with_expiry(Some(chain_tip), None);
10220 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
10221 Address::from(expired_by_height.clone()).to_zcash_address(st.network()),
10222 payment_value,
10223 )])
10224 .unwrap();
10225 let proposal = st
10226 .propose_transfer(
10227 account.id(),
10228 &input_selector,
10229 &change_strategy,
10230 request,
10231 ConfirmationsPolicy::MIN,
10232 )
10233 .unwrap();
10234 assert_matches!(
10235 st.create_proposed_transactions::<Infallible, _, Infallible, _>(
10236 account.usk(),
10237 OvkPolicy::Sender,
10238 &proposal
10239 ),
10240 Err(Error::RecipientAddressExpiry {
10241 payment_index: 0,
10242 error: AddressExpiryError::ExpiryHeightConflict { .. },
10243 })
10244 );
10245
10246 const EXPIRY_TIME_IN_PAST: u64 = 1;
10250 let valid_far_future = with_expiry(
10251 Some(birthday + 10_000),
10252 Some(super::TEST_CLOCK_EPOCH_OFFSET.as_secs() + 10_000),
10253 );
10254 let expired_by_time = with_expiry(None, Some(EXPIRY_TIME_IN_PAST));
10255 let request = zip321::TransactionRequest::new(vec![
10256 Payment::without_memo(
10257 Address::from(valid_far_future.clone()).to_zcash_address(st.network()),
10258 payment_value,
10259 ),
10260 Payment::without_memo(
10261 Address::from(expired_by_time.clone()).to_zcash_address(st.network()),
10262 payment_value,
10263 ),
10264 ])
10265 .unwrap();
10266 let proposal = st
10267 .propose_transfer(
10268 account.id(),
10269 &input_selector,
10270 &change_strategy,
10271 request,
10272 ConfirmationsPolicy::MIN,
10273 )
10274 .unwrap();
10275 assert_matches!(
10276 st.create_proposed_transactions::<Infallible, _, Infallible, _>(
10277 account.usk(),
10278 OvkPolicy::Sender,
10279 &proposal
10280 ),
10281 Err(Error::RecipientAddressExpiry {
10282 payment_index: 1,
10283 error: AddressExpiryError::TimeExpired { .. },
10284 })
10285 );
10286
10287 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
10289 Address::from(valid_far_future.clone()).to_zcash_address(st.network()),
10290 payment_value,
10291 )])
10292 .unwrap();
10293 let proposal = st
10294 .propose_transfer(
10295 account.id(),
10296 &input_selector,
10297 &change_strategy,
10298 request,
10299 ConfirmationsPolicy::MIN,
10300 )
10301 .unwrap();
10302 assert_matches!(
10303 st.create_proposed_transactions::<Infallible, _, Infallible, _>(
10304 account.usk(),
10305 OvkPolicy::Sender,
10306 &proposal
10307 ),
10308 Ok(_)
10309 );
10310
10311 let recipient = with_expiry(Some(chain_tip - 1), None);
10315 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
10316 Address::from(recipient).to_zcash_address(st.network()),
10317 payment_value,
10318 )])
10319 .unwrap();
10320 let proposal = st
10321 .propose_transfer(
10322 account.id(),
10323 &input_selector,
10324 &change_strategy,
10325 request,
10326 ConfirmationsPolicy::MIN,
10327 )
10328 .unwrap();
10329 assert_matches!(
10330 st.create_proposed_transactions::<Infallible, _, Infallible, _>(
10331 account.usk(),
10332 OvkPolicy::Sender,
10333 &proposal
10334 ),
10335 Err(Error::RecipientAddressExpiry {
10336 payment_index: 0,
10337 error: AddressExpiryError::HeightExpired { .. },
10338 })
10339 );
10340
10341 let target_height = chain_tip + 1;
10346 let default_expiry_height =
10347 target_height + default_tx_expiry_delta(BranchId::for_height(st.network(), target_height));
10348 let recipient = with_expiry(Some(default_expiry_height), None);
10349 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
10350 Address::from(recipient).to_zcash_address(st.network()),
10351 payment_value,
10352 )])
10353 .unwrap();
10354 let proposal = st
10355 .propose_transfer(
10356 account.id(),
10357 &input_selector,
10358 &change_strategy,
10359 request,
10360 ConfirmationsPolicy::MIN,
10361 )
10362 .unwrap();
10363 assert_matches!(
10364 st.create_proposed_transactions::<Infallible, _, Infallible, _>(
10365 account.usk(),
10366 OvkPolicy::Sender,
10367 &proposal
10368 ),
10369 Ok(_)
10370 );
10371
10372 let recipient = with_expiry(None, Some(super::TEST_CLOCK_EPOCH_OFFSET.as_secs()));
10375 let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
10376 Address::from(recipient).to_zcash_address(st.network()),
10377 payment_value,
10378 )])
10379 .unwrap();
10380 let proposal = st
10381 .propose_transfer(
10382 account.id(),
10383 &input_selector,
10384 &change_strategy,
10385 request,
10386 ConfirmationsPolicy::MIN,
10387 )
10388 .unwrap();
10389 assert_matches!(
10390 st.create_proposed_transactions::<Infallible, _, Infallible, _>(
10391 account.usk(),
10392 OvkPolicy::Sender,
10393 &proposal
10394 ),
10395 Ok(_)
10396 );
10397}
10398
10399const NU7_ACTIVATION_OFFSET: u32 = 10;
10402
10403fn nu7_activating_network() -> LocalNetwork {
10406 let activation = TestBuilder::<(), ()>::DEFAULT_NETWORK
10407 .sapling
10408 .expect("the default test network activates Sapling");
10409 LocalNetwork {
10410 nu6: Some(activation),
10411 nu6_1: Some(activation),
10412 nu6_2: Some(activation),
10413 nu6_3: Some(activation),
10414 nu7: Some(activation + NU7_ACTIVATION_OFFSET),
10415 ..TestBuilder::<(), ()>::DEFAULT_NETWORK
10416 }
10417}
10418
10419fn nu7_default_expiry_scenario<T: ShieldedPoolTester, Cache: TestCache, Dsf: DataStoreFactory>(
10423 dsf: Dsf,
10424 cache: Cache,
10425) -> (TestDsl<TestScenario<T, Cache, Dsf>>, BlockHeight) {
10426 let network = nu7_activating_network();
10427 let nu7_activation = network
10428 .activation_height(NetworkUpgrade::Nu7)
10429 .expect("the test network activates NU7");
10430 let mut st = TestDsl::from(
10431 TestBuilder::new()
10432 .with_network(network)
10433 .with_data_store_factory(dsf)
10434 .with_block_cache(cache)
10435 .with_account_from_sapling_activation(BlockHash([0; 32])),
10436 )
10437 .build::<T>();
10438
10439 st.add_notes_checking_balance([
10440 [Zatoshis::const_from_u64(100_000)],
10441 [Zatoshis::const_from_u64(100_000)],
10442 ]);
10443 let tip = st.wallet().chain_height().unwrap().unwrap();
10444 let blocks_to_add = u32::from(nu7_activation) - 2 - u32::from(tip);
10445 st.add_empty_blocks(usize::try_from(blocks_to_add).expect("block count fits in a usize"));
10446 (st, nu7_activation)
10447}
10448
10449pub fn create_proposed_transactions_uses_nu7_default_expiry<T: ShieldedPoolTester>(
10453 dsf: impl DataStoreFactory,
10454 cache: impl TestCache,
10455) {
10456 let (mut st, nu7_activation) = nu7_default_expiry_scenario::<T, _, _>(dsf, cache);
10457 let recipient = T::fvk_default_address(&T::sk_to_fvk(&T::sk(&[0xf5; 32])));
10458 let payment_value = Zatoshis::const_from_u64(20_000);
10459
10460 for (expected_target_height, expected_delta) in [
10461 (nu7_activation - 1, BLOSSOM_DEFAULT_TX_EXPIRY_DELTA),
10462 (nu7_activation, NU7_DEFAULT_TX_EXPIRY_DELTA),
10463 ] {
10464 let proposal = st.propose_transfer_to(&recipient, payment_value);
10465 let target_height = BlockHeight::from(proposal.min_target_height());
10466 assert_eq!(target_height, expected_target_height);
10467
10468 let txids = st.create_proposed_expecting(&proposal, 1);
10469 let tx = st
10470 .wallet()
10471 .get_transaction(txids[0])
10472 .unwrap()
10473 .expect("the created transaction is stored");
10474 assert_eq!(
10475 tx.expiry_height(),
10476 target_height + expected_delta,
10477 "default expiry height for target height {target_height}",
10478 );
10479
10480 st.add_empty_blocks(1);
10481 }
10482}
10483
10484#[cfg(feature = "pczt")]
10491pub fn create_pczt_uses_nu7_default_expiry<T: ShieldedPoolTester, Dsf>(
10492 dsf: Dsf,
10493 cache: impl TestCache,
10494) where
10495 Dsf: DataStoreFactory,
10496 <Dsf as DataStoreFactory>::AccountId: serde::Serialize + serde::de::DeserializeOwned,
10497{
10498 let (mut st, nu7_activation) = nu7_default_expiry_scenario::<T, _, _>(dsf, cache);
10499 let account_id = st.get_account().id();
10500 let recipient = T::fvk_default_address(&T::sk_to_fvk(&T::sk(&[0xf5; 32])));
10501 let payment_value = Zatoshis::const_from_u64(20_000);
10502
10503 for (expected_target_height, expected_delta) in [
10504 (nu7_activation - 1, BLOSSOM_DEFAULT_TX_EXPIRY_DELTA),
10505 (nu7_activation, NU7_DEFAULT_TX_EXPIRY_DELTA),
10506 ] {
10507 let proposal = st.propose_transfer_to(&recipient, payment_value);
10508 let target_height = BlockHeight::from(proposal.min_target_height());
10509 assert_eq!(target_height, expected_target_height);
10510
10511 let pczt = st
10512 .create_pczt_from_proposal::<Infallible, _, Infallible>(
10513 account_id,
10514 OvkPolicy::Sender,
10515 &proposal,
10516 None,
10517 )
10518 .unwrap();
10519 assert_eq!(
10520 *pczt.global().expiry_height(),
10521 u32::from(target_height + expected_delta),
10522 "default expiry height for target height {target_height}",
10523 );
10524
10525 st.add_empty_blocks(1);
10526 }
10527}