Skip to main content

zcash_client_backend/data_api/testing/
pool.rs

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// `ProposalError` also reaches this module through the `transparent-inputs` group below,
100// so this arm covers only the configuration in which that group is absent.
101#[cfg(all(feature = "orchard", not(feature = "transparent-inputs")))]
102use crate::proposal::ProposalError;
103
104// `SpendPolicy` is used only by scenarios behind one of these two features.
105#[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/// Trait that exposes the pool-specific types and operations necessary to run the
159/// single-shielded-pool tests on a given pool.
160///
161/// You should not need to implement this yourself; instead use [`SaplingPoolTester`] or
162/// [`OrchardPoolTester`] as appropriate.
163///
164/// [`SaplingPoolTester`]: super::sapling::SaplingPoolTester
165#[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    /// Computes the actual root of the `shard_index`-th shard from the wallet's local
214    /// shardtree state, reading whatever leaves and cached annotations are currently
215    /// stored. Used by tests that need to call [`Self::put_subtree_roots`] with a root
216    /// that matches the shard's actual computed root — for example, when declaring a
217    /// shard complete after filling its last position via `scan_cached_blocks`.
218    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
274/// The boundaries of `interval` on the chain that `network` describes.
275fn 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
285/// Tests sending funds within the given shielded pool in a single transaction.
286///
287/// The test:
288/// - Adds funds to the wallet in a single note.
289/// - Checks that the wallet balances are correct.
290/// - Constructs a request to spend part of that balance to an external address in the
291///   same pool.
292/// - Builds the transaction.
293/// - Checks that the transaction was stored, and that the outputs are decryptable and
294///   have the expected details.
295pub 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    // Add funds to the wallet in a single note
302    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    // Verify that the sent transaction was stored and that we can decrypt the memos
337    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    // Verify that the stored sent notes match what we're expecting
362    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    // The sent memo should be the empty memo for the sent output, and the
369    // change output's memo should be as specified.
370    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    // Check that querying for a nonexistent sent note returns None
393    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
420/// Builds a real transaction via the proposal/creation path, assembles it into a full
421/// block, and verifies that [`decrypt_block`] followed by [`scan_block`] detects the
422/// wallet output (the change note) that it contains.
423///
424/// [`decrypt_block`]: crate::scanning::full::decrypt_block
425/// [`scan_block`]: crate::scanning::full::scan_block
426pub 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    // Add funds to the wallet in a single note.
433    let (h, _, _) = st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60000));
434
435    // Propose and create a transfer to an external recipient. The resulting transaction
436    // has two shielded outputs: the payment, and the change returned to the wallet.
437    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    // Build a `ScanningKeys` set for the wallet account. The account identifier used for
476    // scanning is independent of the wallet database's account identifier.
477    let ufvk = account.usk().to_unified_full_viewing_key();
478    let scanning_keys = ScanningKeys::from_account_ufvks([(zip32::AccountId::ZERO, ufvk)]);
479
480    // Assemble a single-transaction block containing the created transaction. The block
481    // is scanned in isolation, so we treat the note commitment trees as empty as of the
482    // immediately preceding block.
483    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    // Phase 1: decrypt the block's shielded outputs.
509    let (header, vtx) = decrypt_block(&network, block, &scanning_keys);
510
511    // Phase 2: scan the decrypted block.
512    #[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    // The wallet should have detected exactly the change output returned to its internal
537    // address; the payment output was sent to a recipient outside the wallet.
538    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    // The note commitment tree should have grown by exactly the two shielded outputs in
552    // the block's single transaction (the payment and the change), starting from the
553    // empty prior tree. The outputs all belong to the pool under test; the other pool (if
554    // compiled in) sees no outputs.
555    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    // The final note added in the block must be marked as a checkpoint at this block
566    // height; this is what lets the wallet anchor witnesses to the block. Getting the
567    // "last outputs in the block" boundary wrong is the most error-prone part of position
568    // tracking, so we assert it explicitly.
569    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/// An enumeration of mechanisms for generating transaction inputs for confirmations policy
594/// testing.
595#[derive(Clone, Copy, Debug, PartialEq, Eq)]
596pub enum InputTrust {
597    /// Generate a wallet-internal output.
598    Internal,
599    /// Generate an output as if it was sent to the wallet by an untrusted counterparty.
600    ExternalUntrusted,
601    /// Generate an output as if it was sent to the wallet by a trusted counterparty.
602    ExternalTrusted,
603}
604
605/// Tests that inputs from a source can be spent according to the default
606/// `ConfirmationsPolicy`.
607///
608/// The test:
609/// - Adds funds to the wallet in a single note from an certain source.
610/// - Checks that the wallet balances are correct after N confirmations, according to
611///   the policy.
612pub 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    // Add funds to the wallet in a single note, owned by the internal spending key,
622    // which will have one confirmation.
623    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    // Mark the external input as explicitly trusted, if so requested
642    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    // Generate N confirmations by mining blocks
675    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    // Now that the funds are spendable, propose a transaction
689    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
706/// Tests max spendable funds within the given shielded pool in a
707/// single transaction.
708///
709/// The test:
710/// - Adds funds to the wallet in two notes with different confirmation heights
711/// - Checks that the wallet balances are correct.
712/// - Constructs a request to spend the whole balance to an external address in the
713///   same pool.
714/// - Builds the transaction.
715/// - Checks that the transaction was stored, and that the outputs are decryptable and
716///   have the expected details.
717pub 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    // Add funds to the wallet in two notes over 5 blocks
724    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    // Spendable balance matches total balance
731    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    // Verify that the sent transaction was stored and that we can decrypt the memos
764    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); // there's no empty memo in this case
787
788    // Verify that the stored sent notes match what we're expecting
789    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    // The sent memo should the specified memo for the sent output
796    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    // Check that querying for a nonexistent sent note returns None
820    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
851/// Tests sending every piece of spendable funds within the given shielded pool in a
852/// single transaction.
853///
854/// The test:
855/// - Adds funds to the wallet in a single note.
856/// - Checks that the wallet balances are correct.
857/// - Constructs a request to spend the whole balance to an external address in the
858///   same pool.
859/// - Builds the transaction.
860/// - Checks that the transaction was stored, and that the outputs are decryptable and
861///   have the expected details.
862pub 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    // Add funds to the wallet in a single note
871    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    // Verify that the sent transaction was stored and that we can decrypt the memos
895    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); // there's no empty memo in this case
918
919    // Verify that the stored sent notes match what we're expecting
920    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    // The sent memo should the specified memo for the sent output
927    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    // Check that querying for a nonexistent sent note returns None
951    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/// Tests that sending all the spendable funds within the given shielded pool in a
979/// single transaction to a transparent address with a memo fails.
980///
981/// The test:
982/// - Adds funds to the wallet in a single note.
983/// - Checks that the wallet balances are correct.
984/// - Tries to propose a send max transaction to a T-address with a memo
985/// - Fails gracefully with Zip321Error.
986#[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    // Add funds to the wallet in a single note
994    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/// Tests that sending all the spendable funds within the given shielded pool to a
1022/// transparent (non-TEX) recipient succeeds.
1023///
1024/// The test:
1025/// - Adds funds to the wallet in a single note.
1026/// - Proposes a send-max transaction to a transparent address, without a memo.
1027/// - Verifies that the proposal consists of a single step paying the whole balance,
1028///   less the fee, to the transparent recipient.
1029/// - Builds the transaction.
1030#[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    // Add funds to the wallet in a single note
1038    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    // The proposed transaction carries one shielded spend (padded to two shielded
1047    // outputs) and one transparent output.
1048    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/// Tests that a send-max proposal whose total required fee overflows the maximum
1080/// monetary amount fails with a balance error rather than panicking.
1081#[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    /// A fee rule that requires the maximum monetary amount for every transaction.
1087    #[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    // Add funds to the wallet in a single note
1111    st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60000));
1112
1113    let account = st.test_account().cloned().unwrap();
1114
1115    // A TEX recipient requires a second transaction, so the total required fee is the
1116    // sum of two per-transaction fees, which overflows the maximum monetary amount.
1117    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/// Tests that a send-max proposal spends notes from multiple shielded pools in a single
1136/// transaction when the wallet's funds are split across pools.
1137///
1138/// The test:
1139/// - Adds one note in each of the `P0` and `P1` pools.
1140/// - Proposes a send-max transaction to an external `P1` recipient, so that the value of
1141///   the `P0` note crosses pools.
1142/// - Verifies that the proposal consists of a single step spending both notes and paying
1143///   the whole balance, less the fee, to the recipient.
1144/// - Builds the transaction, mines it, and verifies that the wallet is left empty.
1145#[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    // Add one note in each of the P0 and P1 pools.
1154    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    // The proposed transaction spends one note in each pool and pays a single shielded
1171    // output; under ZIP 317, each of the two shielded bundles is padded to two logical
1172    // actions.
1173    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    // The proposal spends both notes, one from each pool.
1202    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    // Mine the transaction and verify that the entire balance has been spent.
1219    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/// The value of the `P0` note of the across-pools dust shortfall scenario, which covers the
1225/// payment alone.
1226#[cfg(feature = "orchard")]
1227const DUST_SHORTFALL_P0_NOTE_VALUE: Zatoshis = Zatoshis::const_from_u64(60_000);
1228/// The payment of the across-pools dust shortfall scenario. The `P0` note alone would leave
1229/// change of 3000 zatoshis, below the dust threshold of one marginal fee, so the `P1` note is
1230/// drawn on as well: two bundles, each padded to two logical actions.
1231#[cfg(feature = "orchard")]
1232const DUST_SHORTFALL_PAYMENT: Zatoshis = Zatoshis::const_from_u64(47_000);
1233/// A `P1` note that covers its bundle's share of the two-bundle fee, so that both notes
1234/// together fund the payment.
1235#[cfg(feature = "orchard")]
1236const DUST_SHORTFALL_P1_NOTE_COVERING_FEE: Zatoshis = Zatoshis::const_from_u64(20_000);
1237/// A `P1` note that falls short of its bundle's share of the two-bundle fee, so that both
1238/// notes together fall 1000 zatoshis short of the payment plus that fee.
1239#[cfg(feature = "orchard")]
1240const DUST_SHORTFALL_P1_NOTE_BELOW_FEE: Zatoshis = Zatoshis::const_from_u64(6_000);
1241
1242/// The ZIP 317 fee of a transaction spending from two shielded pools, each bundle padded to
1243/// two logical actions.
1244#[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/// The across-pools dust shortfall scenario: an account holding a `P0` note and a smaller `P1`
1252/// note, and a transfer to a `P0` recipient of an amount the `P0` note covers alone, but whose
1253/// change from that note alone would fall below the dust threshold.
1254#[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    /// The value of both notes.
1260    total: Zatoshis,
1261}
1262
1263/// Builds the across-pools dust shortfall scenario with a `P1` note of `p1_note_value`, both
1264/// notes scanned into the wallet.
1265#[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/// Tests that a transfer whose change from the preferred pool's note would be dust draws on
1299/// a note in another pool instead of failing.
1300///
1301/// The test:
1302/// - Builds the across-pools dust shortfall scenario with a `P1` note that covers the
1303///   two-bundle fee.
1304/// - Verifies that the proposal spends both notes, at the two-bundle fee, and that the
1305///   change is the remainder of both.
1306/// - Builds and mines the transaction, and verifies that the change is all that remains.
1307#[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/// Tests that a transfer which draws on a second pool at a dust shortfall reports
1360/// `InsufficientFunds` over both pools when the second note cannot cover the two-bundle fee.
1361///
1362/// The test:
1363/// - Builds the across-pools dust shortfall scenario with a `P1` note too small for the
1364///   two-bundle fee.
1365/// - Verifies that the proposal fails with `InsufficientFunds`, requiring the amount plus
1366///   the two-bundle fee and reporting the value of both notes as available.
1367#[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/// Tests that proposing a send-max transfer to a TEX recipient fails with a meaningful
1392/// error when the `transparent-inputs` feature is not enabled.
1393#[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    // Add funds to the wallet in a single note
1401    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/// Tests that a send-max proposal to a unified address having both Sapling and Orchard
1424/// receivers is delivered via the Sapling receiver when the `orchard` feature is not
1425/// enabled, rather than failing.
1426#[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    // Add funds to the wallet in a single note
1434    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    // Construct a unified address carrying both a Sapling receiver and an Orchard
1440    // receiver. Without the `orchard` feature, the Orchard receiver's contents are
1441    // not parsed, so arbitrary receiver bytes suffice.
1442    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    // The proposed transaction carries one Sapling spend and one requested Sapling
1459    // output (padded to two logical actions under ZIP 317).
1460    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/// Tests that a send-max proposal to a unified address whose only receiver cannot be
1490/// paid by this build (an Orchard-only address, without the `orchard` feature) fails
1491/// with `GreedyInputSelectorError::UnsupportedAddress`.
1492#[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    // Add funds to the wallet in a single note
1500    st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60000));
1501
1502    let account = st.test_account().cloned().unwrap();
1503
1504    // Without the `orchard` feature, the Orchard receiver's contents are not parsed,
1505    // so arbitrary receiver bytes suffice.
1506    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
1530/// Tests that a send-max proposal fails with `InsufficientFunds` when the entire wallet
1531/// balance would be consumed by fees, rather than proposing a transaction that delivers
1532/// nothing to the recipient.
1533pub 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    // Add funds equal to the exact fee of a send-max transaction spending a single note
1540    // to a same-pool recipient (two logical actions under ZIP 317).
1541    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
1563/// Tests that attempting to send all the spendable funds within the given shielded pool in a
1564/// single transaction fail if there are funds that are not yet confirmed.
1565///
1566/// The test:
1567/// - Adds funds to the wallet in a single note.
1568/// - Checks that the wallet balances are correct.
1569/// - Mine empty blocks
1570/// - Add more funds
1571/// - Attempts to construct a request to spend the whole balance to an external address in the
1572///   same pool.
1573/// - catches failure
1574/// - verifies the failure is the one expected
1575pub 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    // Spendable balance doesn't match total balance
1588    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
1625/// Tests that attempting to send `MaxSpendable` funds within the given shielded pool in a
1626/// single transaction succeeds if there are funds that are not yet confirmed.
1627///
1628/// The test:
1629/// - Adds funds to the wallet in a single note.
1630/// - Checks that the wallet balances are correct.
1631/// - Mine empty blocks
1632/// - Add more funds
1633/// - Attempts to construct a request to spend the whole balance to an external address in the
1634///   same pool.
1635/// - succeeds at doing so
1636pub 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    // Spendable balance doesn't match total balance
1654    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    // Verify that the sent transaction was stored and that we can decrypt the memos
1692    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); // there's no empty memo in this case
1715
1716    // Verify that the stored sent notes match what we're expecting
1717    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    // The sent memo should the specified memo for the sent output
1724    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    // Check that querying for a nonexistent sent note returns None
1748    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/// This test attempts to send the max spendable funds to a TEX address recipient
1776/// checks that the transactions were stored and that the amounts involved are correct
1777#[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    // Add funds to the wallet.
1795    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    // check that the napkin math is Ok. Total value send should be the whole
1811    // value of the wallet
1812    assert_eq!(total_sent, value);
1813
1814    // Generate a ZIP 320 proposal, sending to an external TEX address.
1815    let tex_addr = Address::Tex([0x4; 20]);
1816
1817    // TODO: Do we want to allow shielded change memos in ephemeral transfers?
1818    //let change_memo = Memo::from_str("change").expect("valid memo").encode();
1819    let fee_rule = StandardFeeRule::Zip317;
1820
1821    // We use `st.propose_standard_transfer` here in order to also test round-trip
1822    // serialization of the proposal.
1823    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            // TODO: Do we want to allow shielded change memos in ephemeral transfers?
1844            //ChangeValue::shielded(
1845            //    T::SHIELDED_PROTOCOL,
1846            //    expected_step0_change,
1847            //    Some(change_memo)
1848            //),
1849            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    // Mine the created transactions.
1859    for txid in txids.iter() {
1860        let (h, _) = st.generate_next_block_including(*txid);
1861        st.scan_cached_blocks(h, 1);
1862    }
1863
1864    // Check that there are sent outputs with the correct values.
1865    let confirmed_sent: Vec<Vec<_>> = txids
1866        .iter()
1867        .map(|sent_txid| st.wallet().get_sent_outputs(sent_txid).unwrap())
1868        .collect();
1869
1870    // Verify that a status request has been generated for the second transaction of
1871    // the ZIP 320 pair.
1872    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    // Check that the transaction history matches what we expect.
1891    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/// This test attempts to send the max spendable funds to a TEX address recipient
1925/// checks that the transactions were stored and that the amounts involved are correct
1926#[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    // Add funds to the wallet.
1943    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    // check that the napkin math is Ok. Total value send should be the whole
1964    // value of the wallet
1965    assert_eq!(total_sent, value);
1966
1967    // Generate a ZIP 320 proposal, sending to an external TEX address.
1968    let tex_addr = Address::Tex([0x4; 20]);
1969    let fee_rule = StandardFeeRule::Zip317;
1970
1971    // We use `st.propose_standard_transfer` here in order to also test round-trip
1972    // serialization of the proposal.
1973    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    // Mine the created transactions.
2001    for txid in txids.iter() {
2002        let (h, _) = st.generate_next_block_including(*txid);
2003        st.scan_cached_blocks(h, 1);
2004    }
2005
2006    // Check that there are sent outputs with the correct values.
2007    let confirmed_sent: Vec<Vec<_>> = txids
2008        .iter()
2009        .map(|sent_txid| st.wallet().get_sent_outputs(sent_txid).unwrap())
2010        .collect();
2011
2012    // Verify that a status request has been generated for the second transaction of
2013    // the ZIP 320 pair.
2014    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    // Check that the transaction history matches what we expect.
2033    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/// This test attempts to send the max spendable funds to a TEX address recipient.
2067/// The wallet contains many notes denominated with the marginal fee value.
2068/// Checks that the transactions were stored and that the amounts involved are correct
2069#[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    // check that the napkin math is Ok. Total value send should be the whole
2111    // value of the wallet
2112    assert_eq!(total_sent, non_marginal_notes_value);
2113
2114    // Generate a ZIP 320 proposal, sending to an external TEX address.
2115    let tex_addr = Address::Tex([0x4; 20]);
2116
2117    let fee_rule = StandardFeeRule::Zip317;
2118
2119    // We use `st.propose_standard_transfer` here in order to also test round-trip
2120    // serialization of the proposal.
2121    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    // Mine the created transactions.
2153    for txid in txids.iter() {
2154        let (h, _) = st.generate_next_block_including(*txid);
2155        st.scan_cached_blocks(h, 1);
2156    }
2157
2158    // Check that there are sent outputs with the correct values.
2159    let confirmed_sent: Vec<Vec<_>> = txids
2160        .iter()
2161        .map(|sent_txid| st.wallet().get_sent_outputs(sent_txid).unwrap())
2162        .collect();
2163
2164    // Verify that a status request has been generated for the second transaction of
2165    // the ZIP 320 pair.
2166    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    // Check that the transaction history matches what we expect.
2185    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); // ending balance should be zero
2216}
2217
2218/// Tests that the `InsufficientFunds` error of a transfer permitted to spend the account's
2219/// transparent outputs reports the value of those outputs as available, alongside the notes.
2220///
2221/// The test:
2222/// - Adds a shielded note and a transparent UTXO to the account.
2223/// - Proposes a transfer exceeding their combined value, permitting transparent spends.
2224/// - Verifies that `available` is the sum of the note and the UTXO, and that `required` is
2225///   the payment plus the fee for spending both.
2226#[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    // The transaction would spend the transparent input and the note, paying the recipient
2271    // and a change output in the note's pool: one transparent logical action plus two
2272    // shielded ones.
2273    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    // Add funds to the wallet in a single note
2297    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    // Verify that the sent transaction was stored and that we can decrypt the memos
2345    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    // Verify that the stored sent notes match what we're expecting
2370    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    // The sent memo should be the empty memo for the sent output, and each
2377    // change output's memo should be as specified.
2378    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    // Now, create another proposal with more outputs requested. We have two change notes;
2425    // we'll spend one of them, and then we'll generate 7 splits.
2426    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 to the wallet.
2489        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        // Generate a ZIP 320 proposal, sending to another wallet's default transparent address
2506        // expressed as a TEX address.
2507        let change_memo = Some(Memo::from_str("change").expect("valid memo").encode());
2508
2509        // We use `st.propose_standard_transfer` here in order to also test round-trip
2510        // serialization of the proposal.
2511        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        // There should be no ephemeral addresses exposed at the current chain height
2539        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        // After creation, there should be a new ephemeral address exposed.
2555        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        // There should be no unused transparent receivers in this range
2567        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        // Mine the created transactions.
2574        for txid in txids.iter() {
2575            let (h, _) = st.generate_next_block_including(*txid);
2576            st.scan_cached_blocks(h, 1);
2577        }
2578
2579        // Check that there are sent outputs with the correct values.
2580        let confirmed_sent: Vec<Vec<_>> = txids
2581            .iter()
2582            .map(|sent_txid| st.wallet().get_sent_outputs(sent_txid).unwrap())
2583            .collect();
2584
2585        // Verify that a status request has been generated for the second transaction of
2586        // the ZIP 320 pair.
2587        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        // Check that the transaction history matches what we expect.
2616        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    // Each transfer should use a different ephemeral address.
2652    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    // Simulate another wallet sending to an ephemeral address with an index
2664    // within the current gap limit.
2665    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    // Check that the addresses are all distinct.
2675    let known_set: HashSet<_> = known_addrs.iter().map(|(addr, _)| addr).collect();
2676    assert_eq!(known_set.len(), known_addrs.len());
2677    // Check that the metadata is as expected.
2678    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    // Create the transaction. This will cause the the gap start to move & a new
2694    // `gap_limits.ephemeral()` of addresses to be created.
2695    let txids = st
2696        .create_proposed_transactions::<Infallible, _, Infallible, _>(
2697            account.usk(),
2698            OvkPolicy::Sender,
2699            &proposal,
2700        )
2701        .unwrap();
2702
2703    // Mine the transaction & update its status to advance the gap. We have to manually update the
2704    // status because scanning will not detect the transparent outputs.
2705    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    // At this point the start of the gap should be at index `gap_limits.ephemeral()` and the new
2712    // size of the known address set should be `gap_limits.ephemeral() * 2`.
2713    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    // check that known_addrs is a prefix of new_known_addrs; we have already checked their
2723    // lengths.
2724    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    // By reserving the address, its exposure has transitioned from "unknown" to "exposed".
2759    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    // The range of address indices that are safe to reserve now is
2777    // 0..(gap_limits.ephemeral() * 2 - 1)`, and we have already reserved or used
2778    // `gap_limits.ephemeral() + 1`, addresses, so trying to reserve another
2779    // `gap_limits.ephemeral()` should fail.
2780    reservation_should_fail(&mut st, gap_limits.ephemeral(), gap_limits.ephemeral() * 2);
2781    reservation_should_succeed(&mut st, gap_limits.ephemeral() - 1);
2782    // Now we've reserved everything we can, we can't reserve one more
2783    reservation_should_fail(&mut st, 1, gap_limits.ephemeral() * 2);
2784}
2785
2786/// Tests spending all funds within the given shielded pool in a single transaction.
2787///
2788/// The test:
2789/// - Adds funds to the wallet in a single note.
2790/// - Checks that the wallet balances are correct.
2791/// - Constructs a request to spend all of that balance to an external address in the
2792///   same pool.
2793/// - Builds the transaction.
2794/// - Checks that the transaction was stored, and that the outputs are decryptable and
2795///   have the expected details.
2796pub 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    // Add funds to the wallet in a single note
2803    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    // Verify that the sent transaction was stored and that we can decrypt the memos
2840    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    // Verify that the stored sent notes match what we're expecting
2865    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    // The sent memo should be the empty memo for the sent output, and the
2872    // change output's memo should be as specified.
2873    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    // Check that querying for a nonexistent sent note returns None
2896    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/// Test attempts to sweep a wallet into a TEX address
2923///
2924/// 1. funds wallet with 100000 Zatoshis
2925/// 2. sets that spend amount will be 75000 Zatoshi
2926/// 3. proposes a transaction to a TEX address spending 75000 Zatoshi
2927/// 4. attempts to perform the proposal
2928/// 5. proposes the transaction
2929/// 6. "mines" the transaction.
2930/// 7. checks that all funds have been spent by the two involved transactions
2931///
2932/// Desired effects:
2933/// - all funds are spent
2934/// - Fees are the least possible: in this case 15000 for tr0 and 10000 Zats for tr1
2935#[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    // Add funds to the wallet.
2955    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    // Generate a ZIP 320 proposal, sending to an external TEX address.
2974    let tex_addr = Address::Tex([0x4; 20]);
2975
2976    let change_memo: Option<MemoBytes> = None;
2977    // `propose_transfer_to` proposes a standard transfer, so this also tests
2978    // round-trip serialization of the proposal.
2979    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    // Mine the created transactions.
2998    for txid in txids.iter() {
2999        let (h, _) = st.generate_next_block_including(*txid);
3000        st.scan_cached_blocks(h, 1);
3001    }
3002
3003    // Check that there are sent outputs with the correct values.
3004    let confirmed_sent: Vec<Vec<_>> = txids
3005        .iter()
3006        .map(|sent_txid| st.wallet().get_sent_outputs(sent_txid).unwrap())
3007        .collect();
3008
3009    // Verify that a status request has been generated for the second transaction of
3010    // the ZIP 320 pair.
3011    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    // Check that the transaction history matches what we expect.
3033    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 to the wallet.
3101    add_funds(&mut st, value);
3102
3103    // Generate a ZIP 320 proposal, sending to an external TEX address.
3104    let tex_addr = Address::Tex([0x4; 20]);
3105    let proposal = st.propose_transfer_to(&tex_addr, transfer_amount);
3106
3107    // This is somewhat redundant with `send_multi_step_proposed_transfer`,
3108    // but tests the case with no change memo and ensures we haven't messed
3109    // up the test setup.
3110    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    // Frobnicate the proposal to make it invalid because it does not consume
3118    // the ephemeral output, by truncating it to the first step.
3119    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    // Create a USK that doesn't exist in the wallet
3147    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    // Attempting to spend with a USK that is not in the wallet results in an error
3161    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    // Wallet summary is not yet available
3186    assert_eq!(st.get_wallet_summary(ConfirmationsPolicy::MIN), None);
3187
3188    // We cannot do anything if we aren't synchronised
3189    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    // Add funds to the wallet in a single note
3215    let value = Zatoshis::const_from_u64(50000);
3216    st.add_a_single_note_checking_balance(value);
3217
3218    // Value is considered pending at 10 confirmations.
3219    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    // If none of the wallet's accounts have a recover-until height, then there
3229    // is no recovery phase for the wallet, and therefore the denominator in the
3230    // resulting ratio (the number of notes in the recovery range) is zero.
3231    let no_recovery = Some(Ratio::new(0, 0));
3232
3233    // Wallet is fully scanned
3234    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    // Add more funds to the wallet in a second note
3242    let (h2, _, _) = st.add_a_single_note_checking_balance(value);
3243
3244    // Verified balance does not include the second note
3245    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    // Wallet is still fully scanned
3271    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    // Spend fails because there are insufficient verified notes
3279    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    // Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second
3294    // note is verified
3295    for _ in 2..10 {
3296        st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
3297    }
3298    st.scan_cached_blocks(h2 + 1, 8);
3299
3300    // Total balance is value * number of blocks scanned (10).
3301    assert_eq!(st.get_total_balance(account_id), (value * 10u64).unwrap());
3302
3303    // Spend still fails
3304    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    // Mine block 11 so that the second note becomes verified
3313    let (h11, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
3314    st.scan_cached_blocks(h11, 1);
3315
3316    // Total balance is value * number of blocks scanned (11).
3317    assert_eq!(st.get_total_balance(account_id), (value * 11u64).unwrap());
3318    // Spendable balance at 10 confirmations is value * 2.
3319    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    // Should now be able to generate a proposal
3329    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    // Executing the proposal should succeed
3344    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    // TODO: send to an account so that we can check its balance.
3356    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    // Add funds to the wallet in a single note
3376    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        // Executing the proposal should succeed
3407        let txid = st.create_proposed_transactions(account.usk(), ovk_policy, &proposal)?[0];
3408
3409        // Fetch the transaction from the database
3410        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    // Send some of the funds to another address, keeping history.
3420    // The recipient output is decryptable by the sender.
3421    assert_matches!(
3422        send_and_recover_with_policy(&mut st, OvkPolicy::Sender),
3423        Ok(Some((_, recovered_to, _))) if recovered_to == addr2
3424    );
3425
3426    // Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 42 (that don't send us funds)
3427    // so that the first transaction expires
3428    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    // Send the funds again, discarding history.
3438    // Neither transaction output is decryptable by the sender.
3439    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    // Add funds to the wallet in a single note
3452    let value = Zatoshis::const_from_u64(70000);
3453    st.add_a_single_note_checking_balance(value);
3454
3455    // TODO: generate_next_block_from_tx does not currently support transparent outputs.
3456    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    // Executing the proposal should succeed
3462    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    // Add funds to the wallet in a single note owned by the internal spending key
3475    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    // Value is considered pending at 10 confirmations.
3481    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    // TODO: generate_next_block_from_tx does not currently support transparent outputs.
3501    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    // Executing the proposal should succeed
3506    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    // Add two accounts to the wallet.
3523    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    // Add funds to the account 0 in a single note
3538    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    // Spendable balance matches total balance
3547    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 to an account 2
3558        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    // txid0 should exist; we haven't enhanced it so we'll have the mined height, but not the raw
3587    // transaction data.
3588    assert_matches!(st.wallet_mut().get_tx_height(txid0), Ok(Some(_)));
3589
3590    // delete account 1
3591    assert_matches!(st.wallet_mut().delete_account(account1), Ok(_));
3592
3593    // txid0 should no longer exist in the wallet at all, because it only involved account1
3594    assert_matches!(st.wallet_mut().get_tx_height(txid0), Ok(None));
3595
3596    // txid1 should exist in the wallet, as it involves account 2
3597    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    // Create a third account
3619    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    // Creating a new account with the original birthday forces a rescan.
3626    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 to an account 3
3632        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    // txid2 should exist; we haven't enhanced it so we'll have the mined height, but not the raw
3657    // transaction data.
3658    assert_matches!(st.wallet_mut().get_tx_height(txid2), Ok(Some(_)));
3659
3660    // delete account 3
3661    assert_matches!(st.wallet_mut().delete_account(account3), Ok(_));
3662
3663    // txid2 should still exist in the wallet, as it involves account 2
3664    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
3678/// Regression test for a bug in which [`WalletWrite::delete_account`] failed with a
3679/// `rusqlite::Error::InvalidParameterName(":address")` panic when the account being
3680/// deleted was referenced by a `sent_notes` row via its `to_account_id` column.
3681///
3682/// The triggering state is reached when a transaction is sent from one account in the
3683/// wallet to an address belonging to a second account in the same wallet, and the
3684/// transaction is then decrypted via [`decrypt_and_store_transaction`] so that the
3685/// cross-account transfer is recorded with a non-null `to_account_id` and a received
3686/// output that has an associated address. Deleting the recipient account then exercises
3687/// the `sent_notes` update path inside `delete_account`.
3688///
3689/// [`WalletWrite::delete_account`]: crate::data_api::WalletWrite::delete_account
3690pub 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    // Add two accounts to the wallet, derived from the same seed.
3703    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    // Add funds to account 1 in a single note.
3718    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    // Send funds from account 1 to an address belonging to account 2.
3726    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    // Decrypt and store the transaction. Because the wallet owns the funding inputs
3755    // (account 1) and the output is received by account 2, this records the send as an
3756    // internal cross-account transfer, setting `sent_notes.to_account_id` to account 2
3757    // and associating the received output with account 2's address. This is the state
3758    // that triggers the `delete_account` bug.
3759    let tx = st.wallet().get_transaction(txid).unwrap().unwrap();
3760    let params = *st.network();
3761    decrypt_and_store_transaction(&params, st.wallet_mut(), &tx, Some(h)).unwrap();
3762
3763    // Deleting account 2, the recipient of the internal transfer, must succeed. Prior to
3764    // the fix this failed with `rusqlite::Error::InvalidParameterName(":address")` because
3765    // the `sent_notes` update statement bound the wrong parameter name.
3766    assert_matches!(st.wallet_mut().delete_account(account2), Ok(_));
3767
3768    // account 1 should still exist and retain its change balance.
3769    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
3778/// Verifies that [`WalletWrite::set_account_name`] replaces the name of the given account,
3779/// leaves the names of other accounts unchanged, and permits two accounts to share a name.
3780///
3781/// [`WalletWrite::set_account_name`]: crate::data_api::WalletWrite::set_account_name
3782pub fn account_rename<T: ShieldedPoolTester, DSF>(ds_factory: DSF, cache: impl TestCache)
3783where
3784    DSF: DataStoreFactory,
3785    <DSF as DataStoreFactory>::DataStore: Reset,
3786{
3787    /// The initial name of the first account.
3788    const ACCOUNT1_NAME: &str = "account1";
3789    /// The initial name of the second account.
3790    const ACCOUNT2_NAME: &str = "account2";
3791    /// The name that both accounts are renamed to.
3792    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    // Add two accounts to the wallet, derived from the same seed.
3800    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    // Renaming account 1 changes only its name.
3812    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    // Account names are not required to be unique.
3822    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    // Add two accounts to the wallet.
3845    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    // Add funds to the wallet in a single note
3860    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    // Spendable balance matches total balance
3865    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 to an external recipient
3878        Payment::without_memo(addr2.to_zcash_address(st.network()), amount_sent),
3879        // payment back to the originating wallet, simulating legacy change
3880        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    // The "legacy change" is not counted by get_pending_change().
3903    assert_eq!(
3904        st.get_pending_change(account1, ConfirmationsPolicy::MIN),
3905        pending_change
3906    );
3907    // We spent the only note so we only have pending change.
3908    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    // Account creation and DFVK derivation should be deterministic.
3919    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    // Add funds to the wallet
3955    st.add_notes_checking_balance([Some(Zatoshis::const_from_u64(50000))]);
3956
3957    // Add 10 uneconomic (dust) notes to the wallet
3958    for _ in 1..=10 {
3959        st.add_notes_checking_balance([Some(Zatoshis::const_from_u64(1000))]);
3960    }
3961
3962    // Spendable balance matches total balance
3963    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    // This first request will fail due to insufficient non-dust funds
3975    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    // This request will succeed, spending a single dust input to pay the 10000
3996    // ZAT fee in addition to the 41000 ZAT output to the recipient
3997    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    // TODO: send to an account so that we can check its balance.
4018    // We sent back to the same account so the amount_sent should be included
4019    // in the total balance.
4020    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    // Ensure that the wallet has at least one block
4045    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    // Generate and scan the block including the transaction
4100    let (h, _) = st.generate_next_block_including(shielding_txid);
4101    let scan_result = st.scan_cached_blocks(h, 1);
4102
4103    // Ensure that the transaction metadata is still correct after the update produced by scanning.
4104    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    // Verify that a transaction enhancement request for the transaction containing the spent
4112    // outpoint does not yet exist.
4113    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    // Use `decrypt_and_store_transaction` for the side effect of creating enhancement requests for
4121    // the transparent inputs of the transaction.
4122    let tx = st
4123        .wallet()
4124        .get_transaction(*txids.first())
4125        .unwrap()
4126        .unwrap();
4127    let params = *st.network();
4128    decrypt_and_store_transaction(&params, st.wallet_mut(), &tx, Some(h)).unwrap();
4129
4130    // Verify that a transaction enhancement request for the received transaction was created
4131    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    // Now advance the chain by the pre-NU7 default expiry delta; even though a record for the transaction that created
4139    // `spent_outpoint` exists in the wallet database, the transaction can't be enhanced because
4140    // the outpoint was fake. Advancing the chain will cause the request for enhancement to expire.
4141    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    // Simulate the situation where the enhancement request results in `TxidNotRecognized`
4154    st.wallet_mut()
4155        .set_transaction_status(
4156            *spent_outpoint.txid(),
4157            data_api::TransactionStatus::TxidNotRecognized,
4158        )
4159        .unwrap();
4160
4161    // Verify that the transaction enhancement request for the invalid txid has been deleted.
4162    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// FIXME: This requires fixes to the test framework.
4171#[allow(dead_code)]
4172pub fn birthday_in_anchor_shard<T: ShieldedPoolTester>(
4173    ds_factory: impl DataStoreFactory,
4174    cache: impl TestCache,
4175) {
4176    // Set up the following situation:
4177    //
4178    //        |<------ 500 ------->|<--- 10 --->|<--- 10 --->|
4179    // last_shard_start   wallet_birthday  received_tx  anchor_height
4180    //
4181    // We set the Sapling and Orchard frontiers at the birthday block initial state to 1234
4182    // notes beyond the end of the first shard.
4183    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            // Construct a fake chain state for the end of the block with the given
4191            // birthday_offset from the Nu5 birthday.
4192            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            // There will only be one prior root
4199            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            // There will only be one prior root
4212            #[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            // Ironwood is not active at these test heights, so its tree is empty.
4219            #[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    // Generate 9 blocks that have no value for us, starting at the birthday height.
4241    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(&not_our_key, AddressType::DefaultExternal, not_our_value);
4245    for _ in 1..9 {
4246        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
4247    }
4248
4249    // Now, generate a block that belongs to our wallet
4250    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    // Generate some more blocks to get above our anchor height
4257    for _ in 0..15 {
4258        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
4259    }
4260
4261    // Scan a block range that includes our received note, but skips some blocks we need to
4262    // make it spendable.
4263    st.scan_cached_blocks(initial_height + 5, 20);
4264
4265    // Verify that the received note is not considered spendable
4266    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    // Scan the blocks we skipped
4281    st.scan_cached_blocks(initial_height, 5);
4282
4283    // Verify that the received note is now considered spendable
4284    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    // Generate a block with funds belonging to our wallet.
4304    st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(500000));
4305
4306    // Create a gap of 10 blocks having no shielded outputs, then add a block that doesn't
4307    // belong to us so that we can get a checkpoint in the tree.
4308    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            &not_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    // Scan the block
4329    st.scan_cached_blocks(account.birthday().height() + 10, 1);
4330
4331    // Verify that our note is considered spendable
4332    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(&not_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    // Attempt to spend the note with 5 confirmations
4360    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/// Reads, from any pool's note commitment tree, whether a witness for `note_position` as of
4378/// `anchor_height` is still constructible, the set of surviving checkpoint heights, and the set
4379/// of retained-anchor heights.
4380#[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
4411/// A wallet-level test for note-commitment-tree *anchor retention*: once NU6.3 (Ironwood) is
4412/// active, checkpoints on the anchor-retention interval are retained as durable anchors, exempt
4413/// from the ordinary `PRUNING_DEPTH`-checkpoint pruning budget, so that their roots and the
4414/// witnesses anchored to them remain computable even after they age far behind the chain tip.
4415///
4416/// The interval is supplied by the caller (the pruning depth is read from
4417/// `crate::data_api::ll::wallet`, so this test tracks whatever value the implementation defines).
4418/// Passing a short interval keeps the test cheap: the scan must reach a boundary more than
4419/// `PRUNING_DEPTH` checkpoints behind the tip, which at the ZIP 318 interval means generating
4420/// several hundred blocks.
4421///
4422/// The test:
4423/// - Activates NU6.3 from the Sapling activation height, so anchor retention is live and its
4424///   floor equals the account birthday.
4425/// - Receives a single note early, capturing its note-commitment-tree position.
4426/// - Scans forward in a single batch until an interval-aligned anchor has aged *more than
4427///   `PRUNING_DEPTH` checkpoints* behind the chain tip — so it would have been pruned to enforce
4428///   the checkpoint budget had it not been retained.
4429/// - Proves survival behaviorally: a witness for the received note *as of that buried anchor* is
4430///   still constructible (it would be `None` if the anchor checkpoint had been pruned).
4431/// - Confirms the anchors did not consume the pruning budget: the ordinary checkpoint immediately
4432///   above the buried anchor *was* pruned, exactly the interval-aligned anchors at/above the floor
4433///   are retained, and the full `PRUNING_DEPTH` window of checkpoints at the chain tip survives.
4434pub 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    // A network on which NU6.3 (Ironwood) is active from the Sapling activation height, so anchor
4445    // retention is live with its floor at the account birthday.
4446    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    // Receive a single note; its position is captured after it is deeply confirmed, below.
4466    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    // The first interval-aligned anchor height at or above the retention floor and strictly above
4472    // the received note, so the note's position precedes the anchor's checkpoint.
4473    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    // Scan forward in a single batch so the anchor ages more than `PRUNING_DEPTH` checkpoints
4481    // behind the tip: without retention it would be pruned to enforce the checkpoint budget.
4482    let tip = anchor + PRUNING_DEPTH + 10;
4483
4484    // Fillers pay a non-wallet key, so each block still adds a commitment (and thus a checkpoint)
4485    // without changing the received note's position or the wallet's spendable set.
4486    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            &not_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    // Capture the received note's commitment-tree position now that it is deeply confirmed.
4498    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    // The witness for the received note, anchored at the buried anchor, must still be computable —
4530    // impossible if that checkpoint had been pruned.
4531    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    // Exactly the interval-aligned checkpoints at or above the retention floor are retained.
4538    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    // The buried anchor survives, but the ordinary checkpoint immediately above it was pruned — so
4548    // its survival is due to retention, not to pruning having failed to run.
4549    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    // The anchors did not consume the pruning budget: the full `PRUNING_DEPTH` window of
4559    // checkpoints at the chain tip is still retained.
4560    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/// A grid boundary that lands on a block containing no note commitments in ANY pool must still be
4569/// checkpointed and retained. Scanning checkpoints a block only at its last note commitment, so
4570/// such a block produces no checkpoint of its own in any tree and the cross-pool ensure step has
4571/// nothing to copy; unless the retained heights are ensured explicitly, the retained grid gains a
4572/// permanent hole there — on mainnet a sizeable fraction of blocks carry no shielded outputs —
4573/// and a ZIP 318 crossing anchored to the hole can never be proved.
4574///
4575/// The scan must cover the boundaries in a SINGLE batch: scanning block-by-block masks the gap,
4576/// because every batch checkpoints its starting frontier, so each height gets a checkpoint as the
4577/// next block's batch begins.
4578#[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    // A network on which NU6.3 (Ironwood) is active from the Sapling activation height, so anchor
4590    // retention is live with its floor at the account birthday.
4591    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    // Receive a single note; its position is captured after the batch scan, below.
4611    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    // The first two boundaries strictly above the received note, plus slack so neither boundary
4616    // is the final block of the batch.
4617    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    // Fillers pay a non-wallet key so that ordinary blocks each carry a commitment, but every
4628    // BOUNDARY block is generated empty: no note commitments in any pool, and so no checkpoint
4629    // of its own.
4630    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                    &not_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    // One batch, covering both boundaries.
4646    st.scan_cached_blocks(received_height + 1, (tip - received) as usize);
4647
4648    // Capture the received note's commitment-tree position.
4649    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        // The checkpoint must be USABLE as an anchor: it commits to the tree state as of the last
4689        // note commitment at or before the boundary, so the received note has a witness there.
4690        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    // The pool-crossing destination tree gains the same retained checkpoints even though it holds
4698    // no commitments at all, keeping the cross-pool checkpoint sets consistent.
4699    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    // TODO: Allow for Orchard activation after Sapling
4722    // Here we choose P0, but this has no effect since we supply the viewing keys
4723    // and generate the blocks directly on the state.
4724    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    // We expect 4 logical actions, two per pool (due to padding).
4769    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    // Since this is a cross-pool transfer, change will be sent to the preferred pool.
4777    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    // TODO: Allow for Orchard activation after Sapling
4804    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    // We set the default change output pool to P0, because we want to verify later that
4834    // change is actually sent to P1 (as the transaction is fully fundable from P1).
4835    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    // We expect 2 logical actions, since either pool can pay the full balance required
4852    // and note selection should choose the fully-private path.
4853    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    // Since there are sufficient funds in either pool, change is kept in the same pool as
4861    // the source note (the target pool), and does not necessarily follow preference order.
4862    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    // TODO: Allow for Orchard activation after Sapling
4889    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    // We set the default change output pool to P0, because we want to verify later that
4918    // change is actually sent to P1 (as the transaction is fully fundable from P1).
4919    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    // We expect 3 logical actions, one for the transparent output and two for the source pool.
4936    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    // Since there are sufficient funds in either pool, change is kept in the same pool as
4944    // the source note (the target pool), and does not necessarily follow preference order.
4945    // The source note will always be sapling, as we spend Sapling funds preferentially.
4946    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    // Since the recipient address is in the same account, the total balance includes the transfer
4955    // amount.
4956    assert_eq!(
4957        st.get_total_balance(account.id()),
4958        (initial_balance - expected_fee).unwrap()
4959    );
4960    // The spendable balance doesn't include the transparent value, so it excludes the transfer
4961    // amount.
4962    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    // TODO: Allow for Orchard activation after Sapling
4974    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    // Add some funds to the wallet; we add two notes to allow successive spends. Also,
4983    // we will generate a note in the P1 pool to ensure that we have some tree state.
4984    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    // Generate several empty blocks
5001    for _ in 0..10 {
5002        st.generate_empty_block();
5003    }
5004
5005    // Scan into the middle of the empty range
5006    let scanned = st.scan_cached_blocks(next_to_scan, 5);
5007    let next_to_scan = scanned.scanned_range().end;
5008
5009    // The initial balance should be unchanged.
5010    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    // Set up the fee rule and input selector we'll use for all the transfers.
5017    let input_selector = GreedyInputSelector::new();
5018    let change_strategy =
5019        single_output_change_strategy(StandardFeeRule::Zip317, None, P1::SHIELDED_PROTOCOL);
5020
5021    // First, send funds just to P0
5022    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    // In the next block, send funds to both P0 and P1
5052    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    // Generate a few more empty blocks
5076    for _ in 0..5 {
5077        st.generate_empty_block();
5078    }
5079
5080    // Generate another block with funds for us
5081    let (max_height, _, _) =
5082        st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value);
5083
5084    // Scan everything.
5085    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)), // synthetic checkpoint in empty span from scan start
5102        (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)), // synthetic checkpoint in empty span from scan start
5116        (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    // TODO: Allow for Orchard activation after Sapling
5143    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    // Generate 100 P0 blocks, then 100 P1 blocks, then another 100 P0 blocks.
5152    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    // Empty chain should return None
5177    assert_matches!(st.wallet().chain_height(), Ok(None));
5178
5179    // Create a fake CompactBlock sending value to the address
5180    let (h1, _, _) = st.generate_next_block(
5181        &dfvk,
5182        AddressType::DefaultExternal,
5183        Zatoshis::const_from_u64(5),
5184    );
5185
5186    // Scan the cache
5187    st.scan_cached_blocks(h1, 1);
5188
5189    // Create a second fake CompactBlock sending more value to the address
5190    let (h2, _, _) = st.generate_next_block(
5191        &dfvk,
5192        AddressType::DefaultExternal,
5193        Zatoshis::const_from_u64(7),
5194    );
5195
5196    // Scanning should detect no inconsistencies
5197    st.scan_cached_blocks(h2, 1);
5198}
5199
5200// FIXME: This requires fixes to the test framework.
5201#[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    // Create some fake CompactBlocks
5211    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    // Scanning the cache should find no inconsistencies
5223    st.scan_cached_blocks(h, 2);
5224
5225    // Create more fake CompactBlocks that don't connect to the scanned ones
5226    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    // Data+cache chain should be invalid at the data/cache boundary
5247    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    // Wallet summary is not yet available
5268    assert_eq!(st.get_wallet_summary(ConfirmationsPolicy::MIN), None);
5269
5270    // Create fake CompactBlocks sending value to the address
5271    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    // Scan the cache
5277    st.scan_cached_blocks(h, 2);
5278
5279    // Spendable balance should reflect both received notes
5280    assert_eq!(
5281        st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
5282        (value + value2).unwrap()
5283    );
5284
5285    // "Rewind" to height of last scanned block (this is a no-op)
5286    st.wallet_mut().truncate_to_height(h + 1).unwrap();
5287
5288    // Spendable balance should be unaltered
5289    assert_eq!(
5290        st.get_spendable_balance(account.id(), ConfirmationsPolicy::MIN),
5291        (value + value2).unwrap()
5292    );
5293
5294    // Rewind so that one block is dropped
5295    st.wallet_mut().truncate_to_height(h).unwrap();
5296
5297    // Spendable balance should only contain the first received note;
5298    // the rest should be pending.
5299    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    // Scan the cache again
5309    st.scan_cached_blocks(h, 2);
5310
5311    // Account balance should again reflect both received notes
5312    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    // Test plan:
5324    // 1. Set up test environment with account
5325    // 2. Generate and scan initial blocks to populate the note commitment tree
5326    // 3. Capture the chain state at a specific height
5327    // 4. Generate and scan blocks beyond PRUNING_DEPTH to ensure early checkpoints are pruned
5328    // 5. Verify that normal truncate_to_height fails due to missing checkpoints
5329    // 6. Test that truncate_to_chain_state succeeds using the captured chain state
5330    // 7. Verify wallet state after truncation
5331
5332    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    // Step 2: Generate and scan initial blocks to populate the note commitment tree.
5340    // The first block pays the wallet's own account, so the wallet holds a witnessed note
5341    // below the capture height; without one, a store may satisfy the step-5 truncation by
5342    // emptying the pool's tree outright (which is tolerated when no witness data would be
5343    // lost). The remaining blocks use an "other" fvk so that their notes won't be tracked
5344    // by the wallet (keeping the test focused on tree state rather than wallet balances).
5345    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    // Step 3: Capture the chain state at the current tip. The CachedBlock tracks the
5367    // exact frontier that corresponds to the end of each generated block.
5368    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    // Step 4: Generate and scan blocks well beyond PRUNING_DEPTH so that the checkpoint
5377    // at capture_height is pruned from the note commitment tree.
5378    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    // Step 5: Verify that truncate_to_height fails at capture_height: the checkpoint there
5399    // has been pruned, and the wallet holds a witnessed note below that height, so the
5400    // truncation cannot be satisfied by emptying the tree either.
5401    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    // Step 6: truncate_to_chain_state should succeed because it inserts the frontier
5408    // as a checkpoint before truncating.
5409    st.wallet_mut()
5410        .truncate_to_chain_state(captured_chain_state.clone())
5411        .expect("truncate_to_chain_state should succeed");
5412
5413    // Step 7: Verify wallet state after truncation.
5414    // The chain tip should now be at the capture height.
5415    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    // The block hash at capture_height should match what was in the captured chain state.
5423    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    // Blocks above the capture height should have been removed.
5431    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    // Regression test: truncate_to_chain_state should succeed when truncating to a height
5446    // below the wallet birthday (where no entry exists in the blocks table). Previously,
5447    // this would fail with RequestedRewindInvalid because select_truncation_height requires
5448    // the target height to have an entry in the blocks table.
5449
5450    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            // Ironwood is not active at these test heights, so its tree is empty.
5473            #[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    // Generate and scan a few initial blocks from the birthday height.
5495    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    // Generate and scan blocks well beyond PRUNING_DEPTH to ensure early checkpoints
5508    // are pruned from the note commitment tree.
5509    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    // Get the prior chain state from the account birthday. This chain state is at
5520    // birthday_height - 1, which has valid tree frontiers but NO entry in the blocks
5521    // table (since the wallet never scanned a block at that height).
5522    let prior_chain_state = st
5523        .test_account()
5524        .unwrap()
5525        .birthday()
5526        .prior_chain_state()
5527        .clone();
5528
5529    // This should succeed. On the buggy code, this fails with RequestedRewindInvalid
5530    // because select_truncation_height cannot find an entry in the blocks table at the
5531    // target height.
5532    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    // All blocks were above the target height, so they should have been removed.
5538    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    // Regression test: when truncate_to_chain_state is called with a target height above
5553    // the max scanned height, the frontier insertion must be skipped (it would introduce
5554    // a subtree root discontinuity) but the scan queue must still be trimmed. Without the
5555    // fix, inserting a frontier in shard 2 when the wallet only has shard 0 fails because
5556    // shard 1's subtree root is unknown.
5557
5558    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    // Generate and scan initial blocks, then scan beyond PRUNING_DEPTH to ensure
5563    // early checkpoints are pruned.
5564    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    // Simulate downloading subtree roots from the network: add a known subtree root
5587    // for shard 0 only. This creates a state where shard 0 exists in the shard store
5588    // but shard 1 does not.
5589    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    // Extend the scan queue beyond max_scanned.
5600    let chain_tip = max_scanned + 500;
5601    st.wallet_mut().update_chain_tip(chain_tip).unwrap();
5602
5603    // Construct a ChainState above max_scanned with a frontier in shard 2. The wallet
5604    // has shard 0 (from put_subtree_roots above) but does NOT have shard 1. Inserting a
5605    // frontier in shard 2 introduces a discontinuity because shard 1's subtree root is
5606    // unknown.
5607    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    // Ironwood is not active at these test heights, so its tree is empty.
5621    #[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    // Verify the scan queue extends beyond the target.
5635    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    // Truncate to the target height, which is above max_scanned. With the fix, this
5643    // skips the frontier insertion (avoiding the discontinuity) and trims the scan queue.
5644    // Without the fix, this would fail because inserting a frontier in shard 2 requires
5645    // shard 1's subtree root, which is unknown.
5646    st.wallet_mut()
5647        .truncate_to_chain_state(target_chain_state)
5648        .expect("truncate_to_chain_state above max scanned should succeed");
5649
5650    // The scan queue should have been trimmed to the target height.
5651    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    // Existing blocks below max_scanned should be preserved.
5662    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    // Deep-rewind test plan:
5676    // 1. Set up a birthday-aligned account.
5677    // 2. Generate and scan initial blocks to populate the note commitment tree.
5678    // 3. Pick a rewind target well below the future prune floor.
5679    // 4. Generate and scan more than PRUNING_DEPTH extra blocks so that the checkpoint at the
5680    //    target is pruned AND the target lies below `tip - PRUNING_DEPTH` (the "deep" branch).
5681    // 5. Call `rewind_to_chain_state(target)` and verify:
5682    //    - the scan queue is rewound all the way to `target`;
5683    //    - blocks, transactions, tx_locator_map entries, and note commitment trees are
5684    //      only rewound to `tip - (PRUNING_DEPTH - 1)` (the oldest retained checkpoint).
5685
5686    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    // Generate and scan initial blocks using an "other" fvk so notes are not tracked
5694    // by the wallet.
5695    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    // The rewind target is the tip of the initial range.
5709    let rewind_target = sapling_activation + initial_block_count - 1;
5710
5711    // Scan more than PRUNING_DEPTH extra blocks so that the checkpoint at rewind_target is pruned
5712    // AND rewind_target is below `tip - PRUNING_DEPTH`.
5713    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    // Capture the block hash at the prune boundary so we can assert it survives the rewind
5734    // unchanged (rather than merely that something exists at that height).
5735    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    // `rewind_to_chain_state` must succeed at the same target.
5743    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    // The chain tip (derived from scan_queue) should still report the pre-rewind tip:
5751    // `rewind_to_chain_state` overwrites the scan-queue range above the rewind target
5752    // with a `Historic` rescan range that extends up to the pre-rewind tip.
5753    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    // A deep rewind preserves block, transaction, tx_locator_map, and note commitment tree
5761    // data only as far back as the oldest retained checkpoint at `tip - (PRUNING_DEPTH - 1)`.
5762    // Data at that boundary is kept (so stabilized notes remain spendable); data above it is
5763    // removed.
5764    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    // Shallow-rewind test plan:
5796    // 1. Set up a birthday-aligned account.
5797    // 2. Generate and scan initial blocks to populate the note commitment tree.
5798    // 3. Pick a rewind target.
5799    // 4. Generate and scan `PRUNING_DEPTH - 1` extra blocks so that the target sits at
5800    //    the shallow boundary (`target == tip - (PRUNING_DEPTH - 1)`, exactly the oldest
5801    //    retained checkpoint).
5802    // 5. Call `rewind_to_chain_state(target)` and verify all wallet data is rewound to the
5803    //    target: data at the target is preserved, anything above is removed.
5804
5805    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    // Scan `PRUNING_DEPTH - 1` extra blocks so the target sits at the shallow boundary
5828    // (`target == tip - (PRUNING_DEPTH - 1)`, exactly the oldest retained checkpoint
5829    // given the tree's `max_checkpoints = PRUNING_DEPTH`).
5830    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    // The chain tip (derived from scan_queue) should still report the pre-rewind tip:
5865    // `rewind_to_chain_state` overwrites the scan-queue range above the rewind target with
5866    // a `Historic` rescan range that extends up to the pre-rewind tip.
5867    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    // A shallow rewind truncates blocks, tx_locator_map, and note commitment trees
5875    // directly to the rewind target: data at the target is preserved (with the same
5876    // content it had before), anything above is removed.
5877    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    // Regression test: after the scan scheduler processes a `ChainTip` range before a
5905    // lower `Historic` range, `MAX(height) FROM blocks` points into one scanned region
5906    // while `last_scanned - (PRUNING_DEPTH - 1)` lands inside the unscanned gap between
5907    // the two regions. `rewind_to_chain_state` must still succeed: an implementation that
5908    // expected a checkpoint at exactly the PD floor would return `CorruptedData` via
5909    // `truncate_to_checkpoint`; clamping forward to the lowest checkpoint inside the
5910    // prune window keeps us aligned with a real checkpoint.
5911
5912    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    // Scan is always sequential in cache order, but `scan_cached_blocks` is happy to be
5920    // invoked on subranges out of order. We pre-generate a contiguous chain of blocks
5921    // and scan it in two disjoint segments with a gap in between.
5922    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; // must exceed PD so the PD floor lands in the gap
5928    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    // Scan the low range first (simulating a historic range).
5940    st.scan_cached_blocks(low_start, low_count as usize);
5941
5942    // Scan the high range next (simulating a chain-tip range), leaving `gap_size` blocks
5943    // in the middle unscanned. Because `high_start > low_end_inclusive + PRUNING_DEPTH`,
5944    // the PD floor after this scan (`high_end_inclusive - (PRUNING_DEPTH - 1)`) lands
5945    // inside the unscanned gap.
5946    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    // `rewind_to_chain_state` must return `Ok(_)` rather than `CorruptedData`: clamping
5964    // forward to the lowest checkpoint inside the window (which sits at `high_start`)
5965    // keeps us aligned with a real checkpoint.
5966    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
5974/// Multiple wallet notes in a stabilized shard remain spendable after a deep
5975/// `rewind_to_chain_state` moves the scan queue below them.
5976pub 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    // Test plan:
5983    // 1. Set up a wallet with an initial chain state whose tree has shard 0 fully
5984    //    cached and shard 1 one position short of full (frontier at position
5985    //    `2 * 2^16 - 2 = 131070`). The frontier lives in a partial shard 1 rather
5986    //    than at a shard boundary; a boundary-aligned frontier would cause
5987    //    `prior_subtree_roots` to cache shard 1 and then `insert_frontier` would
5988    //    fail trying to reinstall its leaf into the cached-leaf-form shard.
5989    // 2. Scan a single block of 65537 outputs. The first output finishes shard 1
5990    //    (position 131071) and the remaining 65536 outputs fill all of shard 2
5991    //    (positions 131072..196607). Three of those outputs are wallet-owned and
5992    //    land at the first, middle, and last slots of shard 2 (tree positions
5993    //    131072, 163840, and 196607); every other slot is non-wallet filler.
5994    // 3. Declare shard 2 complete at `note_height` via `put_subtree_roots(2, ...)`
5995    //    so `mark_stabilized_notes` has the `subtree_end_height` it needs to flip
5996    //    the shard 2 notes' `witness_stabilized` flag once the pruning floor rises
5997    //    above the shard.
5998    // 4. Scan `PRUNING_DEPTH + 10` one-output post-note blocks. They land in shard
5999    //    3 (positions 196608+), pushing the pruning-floor checkpoint's tree
6000    //    position into shard 3 so `shardtree::truncate_shards(3)` — invoked by the
6001    //    upcoming rewind — preserves shard 2 and every row it indexes.
6002    // 5. Deep-rewind to a height below `note_height` and verify `scan_queue` is
6003    //    rewound all the way to the target.
6004    // 6. Before restoring the chain tip: the balance path reads the witness_stabilized
6005    //    flag directly, so `get_spendable_balance` must return the full
6006    //    three-note sum; the spend path requires a chain tip for the anchor, so
6007    //    `propose_transfer` must fail with `ScanRequired`/`InsufficientFunds`.
6008    // 7. Call `update_chain_tip(pre_rewind_tip)` and re-verify the balance.
6009    // 8. Build and sign an actual spend — exercising the full note-selection and
6010    //    witness-construction path — and assert it produces exactly one tx.
6011
6012    const SHARD_HEIGHT: u32 = 16;
6013    const SHARD_POSITIONS: u32 = 1 << SHARD_HEIGHT; // 65536
6014
6015    // Step 1: set up the wallet with shard 0 cached + frontier in a partial shard 1.
6016    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            // The birthday is anchored at NU5 + 1000 rather than the more common
6023            // Sapling-activation baseline because the orchard variant of this test
6024            // pre-populates an orchard commitment-tree frontier; that requires
6025            // Orchard to be active at the birthday height, which isn't true at
6026            // Sapling activation. `+ 1000` is an arbitrary buffer past NU5 so
6027            // heights like `birthday_height - 500` (see below) stay comfortably
6028            // within the activated range.
6029            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            // Shard 0 is the only complete shard at this tree size.
6038            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            // Ironwood is not active at these test heights, so its tree is empty.
6057            #[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    // Step 2: scan a single block whose outputs finish shard 1 and fill all of
6083    // shard 2. Three wallet outputs at the first, middle, and last slots of
6084    // shard 2; everything else is non-wallet filler. Distinct wallet-output
6085    // values keep failures easier to diagnose.
6086    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    // Shard 2 spans tree positions 2 * 2^16 .. 3 * 2^16 - 1 = 131072..196607.
6093    let note_tree_positions: [u32; 3] = [
6094        2 * SHARD_POSITIONS,                       // first slot of shard 2
6095        2 * SHARD_POSITIONS + SHARD_POSITIONS / 2, // middle slot of shard 2
6096        3 * SHARD_POSITIONS - 1,                   // last slot of shard 2
6097    ];
6098
6099    let scan_block_size: u32 = SHARD_POSITIONS + 1; // finish shard 1 + fill shard 2
6100    let first_scanned_position: u32 = initial_tree_size; // = 131071
6101    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    // Pick a rewind target well below the wallet's birthday so the rewind
6126    // drops every initially-seeded scan_queue row — exercising the case where
6127    // stabilized-shard metadata is the only thing keeping the notes spendable.
6128    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    // Step 3: declare shard 2 complete at `note_height`. We must pass shard 2's
6136    // actual computed root (not an arbitrary placeholder) because the cap already
6137    // contains annotations inherited from the initial chain state's frontier, and
6138    // `put_subtree_roots` refuses to install a conflicting root.
6139    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    // Step 4: scan more than `PRUNING_DEPTH` blocks past the note-filled block
6148    // into shard 3, so the rewind's truncation position is in shard 3 and the
6149    // ensuing `truncate_shards(3)` leaves shard 2 intact.
6150    let extra_blocks = PRUNING_DEPTH + 10;
6151    for _ in 0..extra_blocks {
6152        st.generate_next_block(&not_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    // Step 5: deep-rewind to the target. The rewind target is below the account birthday,
6159    // so the account must be included in the reset set for the birthday to be lowered.
6160    // `rewind_to_chain_state` overwrites the scan-queue range above the rewind target with
6161    // a `Historic` rescan range, so the chain tip remains observable as the pre-rewind tip
6162    // and notes whose `witness_stabilized = 1` flag survives can still be spent.
6163    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    // Step 6: balance reflects all three stabilized notes, and a spend can be proposed
6171    // immediately because the chain tip is preserved by the rewind.
6172    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    // Step 7: build and sign a real spend end-to-end.
6179    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
6213/// Verifies that when a new account is imported into a fully-scanned wallet,
6214/// the ensuing re-scan discovers the new account's previously-unknown notes
6215/// and `scan_complete → mark_stabilized_notes` flags them `witness_stabilized`
6216/// on the fly, so they remain spendable across a subsequent deep
6217/// `rewind_to_chain_state`.
6218pub 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    // Test plan:
6225    //
6226    // 1. Build a fully-scanned wallet containing account A:
6227    //    (a) install an initial chain state whose commitment tree has shard 0
6228    //        cached and a frontier sitting in a partial shard 1 (the same
6229    //        setup used by `stabilized_note_spendable_after_deep_rewind`);
6230    //    (b) generate one note-containing block at `birthday_height` that
6231    //        finishes shard 1 and fills shard 2, with three A-owned outputs
6232    //        and three outputs for a not-yet-imported account B, non-wallet
6233    //        filler elsewhere;
6234    //    (c) generate `PRUNING_DEPTH + 10` filler blocks past the note block
6235    //        to push the pruning floor past shard 2;
6236    //    (d) scan the note block, declare shard 2 complete via
6237    //        `put_subtree_roots`, then scan the filler blocks — the
6238    //        second-batch `mark_stabilized_notes` call flips A's three notes
6239    //        to `witness_stabilized = 1`. B's outputs are in the cached
6240    //        blocks but produce no `*_received_notes` rows because B is not
6241    //        yet in the wallet.
6242    // 2. Confirm A's notes are spendable (sanity check on the initial
6243    //    stabilization).
6244    // 3. Import account B from the same seed at zip32 index 1, sharing A's
6245    //    `AccountBirthday`. `add_account` rewrites `scan_queue` to replace
6246    //    the post-birthday `Scanned` range with `Historic`, forcing a
6247    //    re-scan.
6248    // 4. Re-scan the cached blocks from the birthday through the chain tip.
6249    //    The blocks are now processed against both A and B; three new
6250    //    `*_received_notes` rows get inserted at B's shard-2 positions.
6251    //    Confirm both accounts' balances reflect their respective totals.
6252    // 5. Deep-rewind to well below the birthday. A non-stabilized note
6253    //    cannot pass the post-rewind scan-state gate in
6254    //    `select_spendable_notes_matching_value`, so if B's notes remain
6255    //    spendable after this rewind, they *must* have been flagged
6256    //    `witness_stabilized` during the re-scan of step 4. Assert that both
6257    //    A's and B's full totals are spendable.
6258
6259    const SHARD_HEIGHT: u32 = 16;
6260    const SHARD_POSITIONS: u32 = 1 << SHARD_HEIGHT; // 65536
6261
6262    // Matches the hard-coded seed used by
6263    // `TestBuilder::with_account_having_current_birthday`. Re-using it at
6264    // zip32 index 1 lets us deterministically derive B's viewing key before
6265    // B is imported, and guarantees that the later `import_account_hd(index=1)`
6266    // call recovers the same key (zip32 derivation is deterministic).
6267    const TEST_SEED: [u8; 32] = [0u8; 32];
6268
6269    // Step 1a: initial tree state — shard 0 fully cached, frontier at one
6270    // position short of shard 1's last slot. A boundary-aligned frontier
6271    // would cause `prior_subtree_roots` to cache shard 1 and then
6272    // `insert_frontier` would fail trying to reinstall its leaf into the
6273    // cached-leaf-form shard.
6274    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            // The birthday is anchored at NU5 + 1000 rather than the more common
6281            // Sapling-activation baseline because the orchard variant of this test
6282            // pre-populates an orchard commitment-tree frontier; that requires
6283            // Orchard to be active at the birthday height, which isn't true at
6284            // Sapling activation. `+ 1000` is an arbitrary buffer past NU5 so
6285            // heights like `birthday_height - 500` (see below) stay comfortably
6286            // within the activated range.
6287            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            // Each prior subtree root is attached to one arbitrary height
6296            // well before the birthday. 500 is a round buffer past NU5 but
6297            // strictly below `birthday_height`; the exact value doesn't
6298            // matter because stabilization only cares about shard *end*
6299            // heights.
6300            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            // Ironwood is not active at these test heights, so its tree is empty.
6319            #[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    // Derive account B's viewing key from the same seed at zip32 index 1.
6345    // This must run before any scanning so the note block below can place
6346    // B-destined outputs even though B is absent from the wallet.
6347    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    // Step 1b: build the note block. A's three outputs sit at the first,
6353    // middle, and last slots of shard 2; B's three outputs occupy adjacent
6354    // (but distinct) slots. Everything else is non-wallet filler.
6355    let a_positions: [u32; 3] = [
6356        2 * SHARD_POSITIONS,                       // first slot of shard 2
6357        2 * SHARD_POSITIONS + SHARD_POSITIONS / 2, // middle slot of shard 2
6358        3 * SHARD_POSITIONS - 1,                   // last slot of shard 2
6359    ];
6360    let b_positions: [u32; 3] = [
6361        2 * SHARD_POSITIONS + 1,                       // one after A's first
6362        2 * SHARD_POSITIONS + SHARD_POSITIONS / 2 + 1, // one after A's middle
6363        3 * SHARD_POSITIONS - 2,                       // one before A's last
6364    ];
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    // `scan_block_size = SHARD_POSITIONS + 1`: one slot finishes shard 1,
6379    // the remaining 65 536 fill all of shard 2.
6380    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    // Step 1c: filler blocks past the note block, sized to put the pruning
6401    // floor past shard 2's end height in step 1d.
6402    let extra_blocks = PRUNING_DEPTH + 10;
6403    for _ in 0..extra_blocks {
6404        st.generate_next_block(&not_our_key, AddressType::DefaultExternal, filler_value);
6405    }
6406
6407    // Step 1d: scan the note block, declare shard 2 complete, scan the
6408    // filler blocks. The batch ordering mirrors
6409    // `stabilized_note_spendable_after_deep_rewind`: shard 2's root can
6410    // only be computed after its leaves are in the wallet's tree, and
6411    // `put_subtree_roots` must run before the next scan batch so
6412    // `mark_stabilized_notes` sees shard 2's `subtree_end_height`.
6413    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    // Step 2: baseline. A's notes are discovered and stabilized; B's
6426    // outputs have no corresponding wallet rows (B absent).
6427    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    // Step 3: import account B, sharing A's birthday so `add_account`
6434    // rewrites the post-birthday `Scanned` range to `Historic` (forcing
6435    // a re-scan).
6436    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    // Step 4: re-scan every cached block at or after the birthday. The
6444    // blocks were previously processed only against A; now they're
6445    // processed against B too, inserting three new `*_received_notes`
6446    // rows at B's shard-2 positions. After this batch
6447    // `mark_stabilized_notes` runs and should flip B's new rows to
6448    // `witness_stabilized = 1`.
6449    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    // Step 5: deep-rewind. The rewind target sits well below the wallet's
6463    // birthday so `scan_queue` is rewound all the way out of any range
6464    // covering shard 2. Only notes flagged `witness_stabilized = 1` can
6465    // pass the post-rewind scan-state gate in
6466    // `select_spendable_notes_matching_value`; any B note that was never
6467    // stabilized would drop out of the balance here.
6468    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    // Create a sequence of blocks to serve as the foundation of our chain state.
6502    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    // The stable portion of the tree will contain 20 notes.
6514    for _ in 0..10 {
6515        gen_random_block(&mut st, 4);
6516    }
6517
6518    // We will reorg to this height.
6519    let reorg_height = account.birthday().height() + 4;
6520    let reorg_position = Position::from(19);
6521
6522    // Scan the first 5 blocks. The last block in this sequence will be where we simulate a
6523    // reorg.
6524    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    // There will be 6 checkpoints: one for the prior block frontier, and then one for each scanned
6535    // block.
6536    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    // Scan another block, then simulate a reorg.
6547    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    //        /\  /\  /\
6567    //  .... /\/\/\/\/\/\
6568    //          c   d   e
6569
6570    // Truncate back to the reorg height, but retain the block cache.
6571    st.truncate_to_height_retaining_cache(reorg_height);
6572
6573    // The following error-prone tree state is generated by the a previous (buggy) truncate
6574    // implementation:
6575    //        /\  /\
6576    //  .... /\/\/\/\
6577    //          c
6578
6579    // We have pruned back to the original checkpoints & tree state.
6580    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    // Skip two blocks, then (re) scan the same block.
6591    st.scan_cached_blocks(reorg_height + 2, 1);
6592
6593    // Given the buggy truncation, this would result in this the following tree state:
6594    //        /\  /\   \  /\
6595    //  .... /\/\/\/\   \/\/\
6596    //          c       e   f
6597
6598    let checkpoints = st
6599        .wallet()
6600        .get_checkpoint_history(&T::SHIELDED_PROTOCOL)
6601        .unwrap();
6602    // Even though we only scanned one block, we get a checkpoint at both the start and the end of
6603    // the block due to the insertion of the prior block frontier.
6604    assert_eq!(checkpoints.len(), 8);
6605    assert_eq!(
6606        checkpoints.last(),
6607        Some(&(reorg_height + 2, Some(reorg_position + 8)))
6608    );
6609
6610    // Now, fully truncate back to the reorg height. This should leave the tree in a state
6611    // where it can be added to with arbitrary notes.
6612    st.truncate_to_height(reorg_height);
6613
6614    // Generate some new random blocks
6615    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    // The previous truncation retained the cache, so re-scanning the same blocks would have
6621    // resulted in the same note commitment tree state, and hence no conflicts; could occur. Now
6622    // that we have cleared the cache and generated a different sequence blocks, if truncation did
6623    // not completely clear the tree state this would generates a note commitment tree conflict.
6624    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    // Create blocks to reach height + 2
6642    let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
6643    let (h3, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
6644
6645    // Scan the later block first
6646    st.scan_cached_blocks(h3, 1);
6647
6648    // Now scan the block of height height + 1
6649    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    // We can spend the received notes
6656    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    // Wallet summary is not yet available
6692    assert_eq!(st.get_wallet_summary(ConfirmationsPolicy::MIN), None);
6693
6694    // Create a fake CompactBlock sending value to the address
6695    let value = Zatoshis::const_from_u64(50000);
6696    let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
6697
6698    // Scan the cache
6699    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    // Account balance should reflect the received note
6705    assert_eq!(st.get_total_balance(account.id()), value);
6706
6707    // Create a second fake CompactBlock sending more value to the address
6708    let value2 = Zatoshis::const_from_u64(70000);
6709    let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value2);
6710
6711    // Scan the cache again
6712    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    // Account balance should reflect both received notes
6718    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    // Wallet summary is not yet available
6737    assert_eq!(st.get_wallet_summary(ConfirmationsPolicy::MIN), None);
6738
6739    // Create a fake CompactBlock sending value to the address
6740    let value = Zatoshis::const_from_u64(50000);
6741    let (_, _, nf) = st.add_a_single_note_checking_balance(value);
6742
6743    // Create a second fake CompactBlock spending value from the address
6744    let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32]));
6745    let to2 = T::fvk_default_address(&not_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    // Scan the cache again
6750    st.scan_cached_blocks(spent_height, 1);
6751
6752    // Account balance should equal the change
6753    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    // Wallet summary is not yet available
6772    assert_eq!(st.get_wallet_summary(ConfirmationsPolicy::MIN), None);
6773
6774    // Create a fake CompactBlock sending value to the address
6775    let value = Zatoshis::const_from_u64(50000);
6776    let (received_height, _, nf) =
6777        st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
6778
6779    // Create a second fake CompactBlock spending value from the address
6780    let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32]));
6781    let to2 = T::fvk_default_address(&not_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    // Scan the spending block first.
6786    st.scan_cached_blocks(spent_height, 1);
6787
6788    // Account balance should equal the change
6789    assert_eq!(
6790        st.get_total_balance(account.id()),
6791        (value - value2).unwrap()
6792    );
6793
6794    // Now scan the block in which we received the note that was spent.
6795    st.scan_cached_blocks(received_height, 1);
6796
6797    // Account balance should be the same.
6798    assert_eq!(
6799        st.get_total_balance(account.id()),
6800        (value - value2).unwrap()
6801    );
6802}
6803
6804/// Note selection draws the OLDEST eligible notes first, where age is chain order — the note's
6805/// commitment tree position — not discovery order. Priority scanning discovers recent blocks
6806/// before back-filling history, so a restored wallet's newest notes carry its lowest row ids;
6807/// ordering by id would spend fresh change while far older notes sat idle. The ordering must
6808/// therefore survive scanning blocks out of order.
6809pub 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    // Two notes; the OLDER one is larger, so which note a small selection returns identifies
6820    // the ordering in use.
6821    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    // Scan the NEWER block first: its note is discovered first and takes the lower row id,
6829    // exactly as happens to recent notes when a restored wallet syncs tip-first.
6830    st.scan_cached_blocks(newer_height, 1);
6831    st.scan_cached_blocks(older_height, 1);
6832
6833    // Either note alone covers the target, so selection returns exactly one — the OLDER.
6834    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    // Create 10 blocks with successively increasing value
6862    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    // We haven't sent any funds yet, so we can't evaluate this query
6904    test_meta(
6905        &st,
6906        NoteFilter::ExceedsPriorSendPercentile(BoundedU8::new_const(50)),
6907        None,
6908    );
6909
6910    // Spend half of each one of our notes, so that we can get a distribution of sent note values.
6911    // FIXME: This test is currently excessively specialized to the `zcash_client_sqlite::WalletDb`
6912    // implmentation of the `InputSource` trait. A better approach would be to create a test input
6913    // source that can select a set of notes directly based upon their nullifiers.
6914    let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32]));
6915    let to = T::fvk_default_address(&not_our_key).to_zcash_address(st.network());
6916    let nz2 = NonZeroU64::new(2).unwrap();
6917
6918    for value in &note_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    // Since we've spent half our notes, our remaining notes each have approximately half their
6927    // original value. The 50th percentile of our spends should be 250_0000 ZAT, and half of our
6928    // remaining notes should have value greater than that.
6929    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            // Initialize the chain state to after ZIP 212 became enforced.
6950            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            // Ironwood is not active at these test heights, so its tree is empty.
6956            #[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    // Only mine a block in P0 to ensure the transactions source is there.
6985    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        // This is rejected before transaction building, so the successful call below
7023        // can reuse the same proposal.
7024        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    // Signability invariant: the test account's derivation is known, so every
7048    // shielded spend must be signable by SOME party: already signed
7049    // (IO-finalized protocol dummies), carrying the dummy key that lets the IO
7050    // Finalizer sign it, or exposing the ZIP 32 derivation metadata by which
7051    // an external Signer identifies the spend as its own. A spend meeting none
7052    // of these is unsignable and dooms transaction extraction.
7053    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    // If we don't create proofs or signatures, we will fail to extract a transaction.
7088    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    // Add proof generation keys to Sapling spends.
7094    let pczt_updated = P0::add_proof_generation_keys(pczt_created, account.usk()).unwrap();
7095
7096    // Create proofs, using the circuit that governs the Orchard pool under the
7097    // consensus branch the PCZT was created for. (The test network's most recent
7098    // upgrade is NU5, so this is currently the historical pre-NU6.2 circuit;
7099    // modernizing the test network fixture is part of the broader Ironwood test
7100    // coverage work.)
7101    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    // The signer view of a v5 PCZT commits to the same transaction effects. V5
7118    // anchors are retained, while Orchard effecting fields are compacted using
7119    // the v2 PCZT encoding. Passing the proof-bearing authoritative copy also
7120    // exercises redaction of data that the external Signer does not need.
7121    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    // The full signer view is for receivers that predate the compact view:
7156    // it commits to the same transaction effects, retains proofs and anchors,
7157    // clears witnesses and dummy signing keys, and (for v5 transactions)
7158    // remains representable in the v1 PCZT encoding.
7159    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    // Redaction's primary purpose is removing the wallet's proprietary metadata,
7181    // which the Signer has no use for and should never see.
7182    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    // Apply signatures to the transported signer view, then combine the
7217    // contribution with the proof-bearing authoritative copy.
7218    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    // Now we can extract the transaction.
7225    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/// Ensure that wallet recovery recomputes fees.
7239///
7240/// Callers must provide an `intervene` function that deletes fee information for the specified
7241/// txid from the database. This deletion is checked and the test will fail if fee information is
7242/// not deleted.
7243#[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    // Get some funds in the source account
7261    let note_value = Zatoshis::const_from_u64(350000);
7262    let _summary = st.add_notes_checking_balance([Some(note_value), Some(note_value)]);
7263
7264    // Create two transactions sending from the source account to a transparent address in the
7265    // destination account.
7266    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        // Make the destination account aware of the received UTXOs
7299        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    // Shield the funds in the destination account
7329    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    // Since our wallet constructed the transaction, we have the fee information;
7353    // we will need to wipe it out via a backend-level intervention in order to simulate
7354    // what happens in recovery.
7355    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    // Verify that the intervention removed the fee information for the transaction.
7362    let shielding_tx = st.get_tx_from_history(txid).unwrap().unwrap();
7363    assert_matches!(shielding_tx.fee_paid, None);
7364
7365    // Run `decrypt_and_store_transaction; this should restore the fee, since the wallet has all of
7366    // the necessary input and output data.
7367    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    // Verify that the fee information has been restored.
7372    let shielding_tx = st.get_tx_from_history(txid).unwrap().unwrap();
7373    assert_eq!(shielding_tx.fee_paid, Some(created_fee));
7374
7375    // Wipe the fee information again; calling `decrypt_and_store_transaction` with the *input* tx
7376    // should also cause the fees to be restored.
7377    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    // Run `decrypt_and_store_transaction with one of the inputs; this should also restore the fee,
7383    // since the wallet has all of the necessary input and output data.
7384    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    // Verify that the fee information has been restored.
7393    let shielding_tx = st.get_tx_from_history(txid).unwrap().unwrap();
7394    assert_eq!(shielding_tx.fee_paid, Some(created_fee));
7395}
7396
7397/// Tests that the wallet correctly reports balance with two notes that are identical
7398/// other than their note randomness.
7399pub 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    // Add funds to the wallet in two identical notes
7406    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    // Spendable balance matches total balance.
7413    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    // Both notes are unspent.
7423    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    // Both notes are spendable with 1 confirmation.
7430    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    // Add transparent output to recipient
7457    builder.add_transparent_output(&recipient, value).unwrap();
7458
7459    // Build the transaction (coinbase transactions don't need provers)
7460    builder
7461        .build(
7462            // unused internally
7463            &TransparentSigningSet::new(),
7464            // unused internally
7465            &[],
7466            // unused internally
7467            &[],
7468            UnwrapErr(SysRng),
7469            &LocalTxProver::bundled(),
7470            &LocalTxProver::bundled(),
7471            // unused internally
7472            &StandardFeeRule::Zip317,
7473        )
7474        .unwrap()
7475}
7476
7477#[cfg(all(feature = "pczt", feature = "transparent-inputs"))]
7478/// Tests that immature coinbase outputs are excluded from note selection.
7479pub 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    // Get the default transparent address
7486    let (t_addr, _) = st.get_account().usk().default_transparent_address();
7487
7488    let coinbase_value = Zatoshis::const_from_u64(50000);
7489
7490    // Get the height where the coinbase tx will be mined
7491    let coinbase_height = st.sapling_activation_height();
7492
7493    // Construct the coinbase transaction and mine the block
7494    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(&params, 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        // Verify the coinbase UTXO is **not** spendable
7512        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    // Add the last block and ensure that the coinbase transaction is spendable
7533    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    // Verify we can propose shielding the coinbase utxo
7555    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"))]
7570/// Tests that `CoinbaseFilter::CoinbaseOnly` excludes non-coinbase outputs and
7571/// `CoinbaseFilter::NonCoinbaseOnly` excludes coinbase outputs from UTXO selection and
7572/// shielding proposals, and that `CoinbaseOnly` still allows proposing shielding when only
7573/// coinbase UTXOs are available.
7574pub 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    // 1. Create a coinbase UTXO (50,000 zats)
7584    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    // The coinbase transaction has a single transparent output at index 0.
7595    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(&params, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
7600
7601    // 2. Create a non-coinbase UTXO (60,000 zats)
7602    // Inserted via put_received_transparent_utxo, which sets tx_index = NULL.
7603    // NULL tx_index is treated as non-coinbase by the filter.
7604    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    // 3. Wait for coinbase maturity (100 confirmations)
7620    st.add_empty_blocks(100);
7621    let target_height = TargetHeight::from(h + 101);
7622
7623    // 4. CoinbaseFilter::All returns both UTXOs
7624    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    // 5. CoinbaseFilter::CoinbaseOnly returns only the coinbase UTXO
7650    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    // 5b. CoinbaseFilter::NonCoinbaseOnly returns only the non-coinbase UTXO.
7669    // The non-coinbase UTXO was inserted with tx_index = NULL, which the filter treats as
7670    // non-coinbase, so it must be included here.
7671    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    // 6. propose_shielding with CoinbaseOnly includes only the coinbase input
7690    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    // 6b. propose_shielding with NonCoinbaseOnly includes only the non-coinbase input
7711    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    // 7. propose_shielding with All includes both inputs
7732    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    // Input ordering is not guaranteed, so compare the set of outpoints.
7750    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/// Verifies that `propose_shielding_coinbase` with a shielded destination produces
7762/// a proposal containing a single ZIP-321 payment to the supplied address for the
7763/// full available value (input total minus fee), with no change.
7764#[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(&params, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
7787    // Coinbase outputs require 100 confirmations.
7788    st.add_empty_blocks(100);
7789
7790    // The destination is a shielded address controlled by a separate spending key
7791    // (i.e. potentially in a different wallet).
7792    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/// A newly constructed shielding proposal preserves the checkpoint selected by the wallet, even
7838/// though it spends no shielded notes. A proposal serialized without the confirmations-policy
7839/// field (as an older library version would have produced) decodes using the default policy and,
7840/// because it still carries that real anchor, builds. Separately, a shielding step produces a
7841/// shielded bundle, so a proposal that encodes such a step with the zero anchor sentinel is
7842/// rejected at the parse boundary.
7843#[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(&params, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
7866    // Coinbase outputs require 100 confirmations.
7867    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    // The shielding step keeps the checkpoint selected by input selection.
7885    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    // The zero anchor is the wire sentinel for "no anchor". A shielding step produces a shielded
7899    // bundle, so decoding must reject a zero anchor at the parse boundary rather than accepting a
7900    // step whose dummy spends would commit to no real anchor. (Checked before building below, which
7901    // spends the input the proposal decodes against.)
7902    let mut zero_anchor = proto.clone();
7903    zero_anchor.steps[0].anchor_height = 0;
7904    assert_matches!(
7905        zero_anchor.try_into_standard_proposal(&params, st.wallet()),
7906        Err(crate::proto::ProposalDecodingError::MissingShieldedAnchor)
7907    );
7908
7909    // A proposal serialized before the confirmations-policy field existed omits it, so decoding
7910    // must fall back to the default policy. The real anchor is preserved, so the step still builds.
7911    let mut without_policy = proto;
7912    without_policy.confirmations_policy = None;
7913    let decoded = without_policy
7914        .try_into_standard_proposal(&params, 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/// Verifies that `propose_shielding_coinbase` rejects a transparent destination
7937/// with [`ProposalError::ShieldingRequiresShieldedRecipient`].
7938#[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(&params, 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/// Verifies that `propose_shielding_coinbase` propagates the supplied `memo`
7985/// into the resulting payment's memo field.
7986#[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(&params, 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/// Verifies that `propose_shielding_coinbase` with `limit = Some(n)` selects at
8035/// most `n` UTXOs, preferring the highest-value coinbase outputs.
8036#[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    // Mine three coinbase transactions to the same recipient at successive heights,
8048    // with distinct values so we can verify the highest-value-first selection.
8049    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 // arbitrary; only first_h matters for maturity
8058        } 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(&params, st.wallet_mut(), tx, Some(h)).unwrap();
8073        if first_h.is_none() {
8074            first_h = Some(h);
8075        }
8076    }
8077    // Mature all three.
8078    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    // The two highest-value coinbase UTXOs are 70000 and 50000.
8103    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/// Verifies that `propose_shielding_coinbase` with `limit = Some(0)` selects no
8109/// inputs, returning [`InputSelectorError::InsufficientFunds`].
8110///
8111/// [`InputSelectorError::InsufficientFunds`]: crate::data_api::wallet::input_selection::InputSelectorError::InsufficientFunds
8112#[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(&params, 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    // With no inputs selected, `payment_amount = input_total - fee` underflows
8152    // (input_total = 0, fee > 0), producing `Error::InsufficientFunds` with
8153    // `available: 0, required: fee`.
8154    assert_matches!(result, Err(Error::InsufficientFunds { .. }));
8155}
8156
8157/// Regression test for the propose-fee/build-fee mismatch fixed in #2376.
8158///
8159/// Both `sapling::builder::BundleType::DEFAULT` and
8160/// `orchard::builder::BundleType::DEFAULT` pad up to a minimum of 2
8161/// outputs/actions (`MIN_SHIELDED_OUTPUTS` / `MIN_ACTIONS`). Before the fix,
8162/// `propose_shielding_coinbase` hardcoded `(1, 0)` / `(0, 1)` when asking the
8163/// fee rule what fee to charge, so the proposal underestimated the fee by
8164/// exactly one ZIP-317 marginal unit (5000 zat). The proposal succeeded, but
8165/// `create_proposed_transactions` then failed at build time with
8166/// `Insufficient funds for transaction construction; need an additional ZatBalance(5000) zatoshis`.
8167///
8168/// This test verifies the propose-and-build round trip succeeds for both
8169/// Sapling and Orchard destinations (parameterized by `T`).
8170#[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(&params, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
8194    // Coinbase outputs require 100 confirmations.
8195    st.add_empty_blocks(COINBASE_MATURITY_BLOCKS as usize);
8196
8197    // The destination is a shielded address controlled by a separate spending key.
8198    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    // Prior to #2376 this would fail at build time with `Insufficient funds for transaction
8214    // construction; need an additional ZatBalance(5000) zatoshis` because the proposal-stage fee
8215    // was computed assuming N output/action slots but the builder materializes N+1 (after `MIN_*`
8216    // padding).
8217    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/// Verifies that once Ironwood is active, `propose_shielding_coinbase` resolves a destination
8231/// with an Orchard receiver to the Ironwood pool — the payment is delivered to the Orchard
8232/// receiver via the Ironwood bundle — and that the proposed transaction builds.
8233#[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    // A network on which Ironwood (NU6.3) is active from the Sapling activation height.
8242    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(&params, st.wallet_mut(), coinbase_tx, Some(h)).unwrap();
8276    // Coinbase outputs require 100 confirmations.
8277    st.add_empty_blocks(COINBASE_MATURITY_BLOCKS as usize);
8278
8279    // The destination has an Orchard receiver controlled by a separate spending key.
8280    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    // The Orchard-receiver payment is represented as an Ironwood-pool output, matching the
8297    // bundle the builder will deliver it through; an Orchard-pool payment would violate the
8298    // Orchard turnstile.
8299    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    // This transaction has a shielded bundle, but it is not observable by this wallet through
8318    // compact-block scanning: it has only transparent wallet inputs, its shielded output belongs
8319    // to another wallet, and it has no change. The sender therefore requires a txid-based status
8320    // request to learn its outcome.
8321    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/// After NU6.3 activation, a payment to an Orchard receiver must be delivered through the
8375/// Ironwood pool, which requires a version 6 transaction. Explicitly requesting a version 5
8376/// transaction — which has no Ironwood bundle — for such a payment must be rejected at proposal
8377/// time with [`ProposalError::OrchardReceiverRequiresIronwood`], rather than producing a proposal
8378/// that could only fail later at build time.
8379#[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    // A network on which Ironwood (NU6.3) is active from the Sapling activation height.
8387    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    // Fund the wallet with a single spendable Orchard note.
8408    st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60_000));
8409
8410    // The destination has an Orchard receiver controlled by a separate spending key.
8411    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/// PCZT construction supports the version 6 transaction format, including its Ironwood bundle.
8447/// After NU6.3 a payment to an Orchard receiver is delivered through the Ironwood pool, so
8448/// `create_pczt_from_proposal` realizes such a proposal as a version 6 PCZT that carries a
8449/// populated Ironwood bundle.
8450#[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    // A network on which NU6.3 — the version 6 transaction format — is active from height 100_000.
8457    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    // Fund the wallet with a single spendable Orchard note.
8478    st.add_a_single_note_checking_balance(Zatoshis::const_from_u64(60_000));
8479
8480    // The destination has an Orchard receiver controlled by a separate spending key; post-NU6.3 the
8481    // payment is routed through the Ironwood pool.
8482    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    // The payment routes to the Ironwood pool, so the resulting PCZT must carry an Ironwood bundle.
8506    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    // The PCZT is a version 6 transaction carrying a populated Ironwood bundle.
8521    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    // The Ironwood output carries the wallet's recipient metadata (the only proprietary field set
8531    // on Ironwood outputs), confirming the bundle is populated during construction rather than left
8532    // as an empty shell.
8533    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    // Every action in both bundles is either already signed (a pre-signed protocol padding
8585    // dummy, cleared by the IO Finalizer) or carries ZIP 32 derivation metadata so that an
8586    // external Signer that matches by derivation path can identify and sign it. Real spends
8587    // and the wallet-controlled zero-value spends the builder pairs with vanilla-pool change
8588    // outputs (post-NU6.3 cross-address rule) both need the derivation; only the pre-signed
8589    // dummies are allowed to lack it.
8590    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    // Confirm this scenario actually exercises a wallet-controlled zero-value spend: the
8602    // vanilla Orchard-pool bundle pairs a zero-value dummy spend (unsigned, carrying `alpha`)
8603    // with the change output, and it is this spend that regressed by being left unsignable.
8604    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    // The backend metadata belongs to the authoritative wallet copy, not the
8621    // external Signer.
8622    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    // V6 signatures do not commit to shielded anchors. Orchard action fields
8636    // that resolve_fields can restore are omitted from the signer view.
8637    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    // The full signer view (for receivers that predate the compact view) commits to
8688    // the same transaction effects and, unlike the compact view, retains v6 anchors.
8689    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    // Resolving either compact representation restores byte-identical effecting
8755    // fields and therefore the same shielded signature digest.
8756    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    // The transported request contains no existing signatures. The batch Signer
8800    // contributes signatures for every unsigned action, including the wallet
8801    // controlled zero value spend, and returns only those new signatures.
8802    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    // Prove both bundles — a single Orchard proving key governs both pools post-NU6.3, selected
8907    // by the circuit version the PCZT's consensus branch implies — then extract the final
8908    // transaction and store it. The stored record must carry the IRONWOOD payment output (the
8909    // payment itself, which post-NU6.3 travels in the Ironwood bundle) alongside the Orchard
8910    // change; the Ironwood outputs were previously dropped from the stored `SentTransaction`
8911    // entirely (issue #2890).
8912    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    // The Ironwood payment output is recorded among the transaction's sent outputs, carrying the
8934    // payment value and the external recipient address.
8935    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/// The transaction history entry for a payment funded from the Orchard pool and delivered
8953/// through the Ironwood pool reports the amount that left the account (payment plus fee) as
8954/// its balance delta, not the total value of the notes spent, which would ignore the change
8955/// returned to the wallet.
8956#[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    // A network on which NU6.3, the version 6 transaction format, is active from height 100_000.
8964    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    // Fund the wallet with a single spendable Orchard note.
8985    let note_value = Zatoshis::const_from_u64(60_000);
8986    st.add_a_single_note_checking_balance(note_value);
8987
8988    // The destination has an Orchard receiver controlled by a separate spending key; post-NU6.3 the
8989    // payment is routed through the Ironwood pool.
8990    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    // The payment crosses from the Orchard pool into the Ironwood pool.
9017    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    // The amount that left the account is the payment plus the fee; the value of the change
9034    // note returned to the wallet must not be counted as spent.
9035    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            // The payment has an external recipient, so it is not a pool crossing even though
9063            // its value moves from the Orchard pool into the Ironwood pool.
9064            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    // The history entry is correct as soon as the transaction is stored...
9074    check_history!("before mining");
9075
9076    // ...and remains correct once the transaction is mined and scanned...
9077    let (h, _) = st.generate_next_block_including(txid);
9078    st.scan_cached_blocks(h, 1);
9079    check_history!("after mining");
9080
9081    // ...and remains correct after transaction enhancement, in which the wallet retrieves the
9082    // full transaction and re-stores it via `decrypt_and_store_transaction` (as the mobile
9083    // SDKs do to recover memos and fee information).
9084    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/// A migration of the wallet's own funds from the Orchard pool to the Ironwood pool, namely a
9095/// payment to the wallet's own external Orchard receiver delivered through the Ironwood pool, leaves
9096/// the account balance unchanged except for the fee. The payment output returned to the wallet
9097/// is not recorded at transaction-creation time (it is an address payment, which the wallet
9098/// expects to detect by scanning), so this exercises detection of the wallet's own Ironwood
9099/// outputs in the scanning path.
9100#[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    // A network on which NU6.3, the version 6 transaction format, is active from height 100_000.
9108    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    // Fund the wallet with a single spendable Orchard note.
9129    let note_value = Zatoshis::const_from_u64(60_000);
9130    st.add_a_single_note_checking_balance(note_value);
9131
9132    // The payment destination is the wallet's own external Orchard receiver; post-NU6.3 the
9133    // payment is routed through the Ironwood pool.
9134    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    // The payment crosses from the Orchard pool into the Ironwood pool.
9161    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    // Once the transaction is mined and scanned, every non-fee output has returned to the
9177    // wallet: the payment output via scanning, and the change output. The account has lost
9178    // only the fee.
9179    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            // Once the wallet has observed the payment output returning to its own account
9208            // (the scanner marks such outputs as change), the transaction presents as a
9209            // wallet-internal transfer between pools, and is classified as a pool crossing
9210            // whose crossing value is the migrated payment amount, the quantity a wallet
9211            // should display for it.
9212            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    // The history entry remains correct after transaction enhancement, in which the wallet
9234    // retrieves the full transaction and re-stores it via `decrypt_and_store_transaction`
9235    // (as the mobile SDKs do to recover memos and fee information).
9236    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/// The transaction version requested at proposal time is recorded on the proposal and preserved
9247/// across serialization, so that transaction building honors it. A proposal serialized without a
9248/// version request (as older serializers produced) decodes with no requested version and falls
9249/// back to the target-height version at build time.
9250#[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    // Fund the wallet with a single spendable Orchard note.
9264    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    // The test network's most recent upgrade is NU5, so version 5 is a valid explicit request.
9281    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    // The requested version is recorded on the proposal.
9296    assert_eq!(proposal.proposed_version(), Some(TxVersion::V5));
9297
9298    // ... and is preserved across a round-trip through the proposal's serialized (proto) form.
9299    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    // A proposal serialized without the field (as an older serializer produced) decodes with no
9306    // requested version.
9307    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/// A payment whose value is a canonical ZIP 318 denomination, fundable from a single Orchard note,
9316/// is proposed against a bucketed anchor and built with a single unpadded Ironwood action — the
9317/// shape of a ZIP 318 migration transfer. Everything else keeps the ordinary anchor and the
9318/// two-action floor.
9319#[cfg(feature = "orchard")]
9320pub fn canonical_crossing_is_bucketed_and_unpadded<Dsf: DataStoreFactory>(
9321    ds_factory: Dsf,
9322    cache: impl TestCache,
9323) {
9324    // A short grid, so the test need not mine 144 blocks to cross a boundary.
9325    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    // One Orchard note, comfortably larger than a canonical denomination plus fees.
9350    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    // Mine well past the next boundary, so the note is spendable at the bucketed anchor and a
9354    // usable boundary exists at or below the ordinary anchor. This is a COUNTED loop over a
9355    // precomputed block count: `generate_next_block` appends to the block cache, whereas
9356    // `chain_height` reads the wallet and only advances on `scan_cached_blocks`, so looping until
9357    // the wallet's height reaches a target would never terminate.
9358    let received = u32::from(received_height);
9359    let interval_blocks = interval.block_count().get();
9360    // Deliberately OFF a boundary: the ordinary anchor must not be a grid boundary by accident,
9361    // or an assertion that a canonical crossing was bucketed would pass trivially.
9362    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    // Fillers pay a non-wallet key, so each block adds a commitment (and thus a checkpoint)
9368    // without changing the wallet's spendable set.
9369    let not_our_fvk = OrchardPoolTester::sk_to_fvk(&OrchardPoolTester::sk(&[0xf5; 32]));
9370    for _ in 0..filler_count {
9371        st.generate_next_block(
9372            &not_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    // (1) A canonical denomination: bucketed anchor, one Orchard input, one Ironwood action.
9397    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    // (2) One zatoshi off a canonical denomination: ordinary anchor, padded Ironwood bundle.
9429    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    // Building the canonical proposal is left until last: it spends the wallet's only note, so the
9452    // comparison above must be proposed while funds remain. The built transaction takes the ZIP 318
9453    // rolling expiry rather than the builder's per-transaction one — every crossing in the same
9454    // modulus period shares it, so it identifies nothing.
9455    // ROUND TRIP. The proposal records the exact dummy outputs costed by the fee model; the
9456    // builder reads that transaction shape back rather than deciding again.
9457    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    // The proposal crosses the FFI boundary before it is built as a PCZT. Preserve all per-pool
9475    // dummy-output counts through that serialization round-trip.
9476    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/// A canonical crossing whose bucketed anchor falls on a block with no shielded outputs must
9540/// still build. Note eligibility at the bucketed anchor is a height comparison, so the proposal
9541/// selects and anchors there regardless; unless the boundary checkpoint was ensured at scan time
9542/// — the boundary block itself contributed no note commitment to create one — building would
9543/// fail attempting to construct witnesses at a checkpoint that does not exist.
9544#[cfg(feature = "orchard")]
9545pub fn canonical_crossing_builds_at_empty_boundary_block<Dsf: DataStoreFactory>(
9546    ds_factory: Dsf,
9547    cache: impl TestCache,
9548) {
9549    // A short grid, so the test need not mine 144 blocks to cross a boundary.
9550    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    // One Orchard note, comfortably larger than a canonical denomination plus fees.
9575    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    // Mine well past the next boundary, deliberately ending OFF a boundary, exactly as in
9581    // `canonical_crossing_is_bucketed_and_unpadded` — but here every BOUNDARY block is generated
9582    // empty, while the other fillers pay a non-wallet key. The bucketed anchor the proposal picks
9583    // is a boundary, and every boundary in the range is an empty block, so the anchor necessarily
9584    // lands on one.
9585    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                &not_our_fvk,
9597                AddressType::DefaultExternal,
9598                Zatoshis::const_from_u64(10_000),
9599            );
9600        }
9601    }
9602    // One batch: block-by-block scanning would checkpoint every height via batch frontiers,
9603    // masking the missing-boundary-checkpoint condition this test exercises.
9604    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    // Building must succeed: the anchor's checkpoint was ensured when the batch was scanned, even
9646    // though the boundary block itself contributed no commitment.
9647    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/// A canonical amount that cannot be funded from a single Orchard note is NOT a canonical
9665/// crossing, and must not pay for a bucketed anchor it gains nothing from.
9666///
9667/// A migration transfer spends exactly one note, so a multi-input transaction resembles none. The
9668/// decision is therefore made before the proposal is kept: had it been made by falling back only
9669/// on insufficient funds, this transaction would have funded perfectly well from several notes and
9670/// been left carrying an anchor up to a full interval older than necessary, for no benefit.
9671#[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    // Three notes, none individually able to cover the payment plus its fee.
9701    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            &not_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/// A canonical payment is funded from the single oldest covering note even when accumulation
9766/// would have reached the target through several smaller notes first.
9767///
9768/// Oldest-first accumulation crosses the target through small notes whenever the oldest notes
9769/// are small, funding the payment while losing the single-input canonical shape. The canonical
9770/// attempt therefore PREFERS single-note funding ([`NoteSelection::PreferSingle`]): when any
9771/// single eligible note covers the payment and its fee, that note is chosen and the transaction
9772/// takes the migration shape. Ordinary (non-canonical) payments keep accumulating.
9773///
9774/// [`NoteSelection::PreferSingle`]:
9775///     crate::data_api::wallet::input_selection::NoteSelection::PreferSingle
9776#[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    // Two OLDER notes that together cover the payment, then one LARGER note that covers it
9806    // alone. Accumulation reaches the target at the second note and never touches the third,
9807    // so which shape the proposal takes distinguishes the two selection behaviors.
9808    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            &not_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    // The canonical payment takes the single-note shape, spending the LARGE note.
9847    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    // An ordinary payment of a NON-canonical amount still accumulates the oldest notes.
9874    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/// A canonical payment whose bucketed anchor checkpoint is MISSING from the wallet falls back to
9887/// an ordinary crossing instead of failing at build time.
9888///
9889/// A wallet that scanned past NU6.3 activation before boundary checkpointing was repaired is
9890/// permanently missing the grid boundaries whose blocks carried no shielded outputs, and the
9891/// holes cannot be backfilled from local state: the tree prunes node data that no retained
9892/// checkpoint references, so reconstruction would require refetching subtree data from a light
9893/// wallet server. Note eligibility at the bucketed anchor is a height comparison, so without a
9894/// checkpoint-existence gate the canonical proposal is kept and the BUILD fails with
9895/// `ProposalError::AnchorNotFound` — a hard send failure where every other miss in the canonical
9896/// path degrades gracefully.
9897///
9898/// `remove_checkpoint` deletes the wallet's checkpoint records at the given height, simulating
9899/// the legacy wallet state; it is supplied by the backend-specific caller because corrupting
9900/// stored state is necessarily a backend-level operation.
9901#[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    // One Orchard note, comfortably larger than a canonical denomination plus fees, mined well
9935    // before the bucketed anchor.
9936    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            &not_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    // With the checkpoint intact the payment is canonical; the anchor it binds is the boundary
9974    // whose checkpoint the second half of the test removes. Reading it off the proposal keeps
9975    // the test's idea of "the bucketed anchor" identical to production's, rather than
9976    // reimplementing the arithmetic.
9977    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    // Simulate the legacy wallet: the boundary's checkpoint records are gone.
9997    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    // The payment now falls back to an ordinary crossing: proposed against the ordinary anchor,
10006    // padded, and — decisively — BUILDABLE. Without the gate the canonical proposal would be
10007    // kept and building would fail with `AnchorNotFound`.
10008    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/// Self-migration does not stall once the wallet holds Ironwood notes.
10024///
10025/// A user moving their own funds across the turnstile sends themselves canonical amounts
10026/// repeatedly. After the first crossing the wallet holds an Ironwood note, and the ordinary
10027/// selector — which prefers to avoid crossing pools — would fund the next Ironwood-destined
10028/// payment from Ironwood, crossing nothing and capping the total that can ever be migrated. The
10029/// canonical path selects from Orchard only, so each transfer keeps moving value across.
10030#[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                &not_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    // First crossing: funded from the original Orchard note.
10090    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    // Mine it, so the wallet now holds an Ironwood note alongside its Orchard change.
10117    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 past another boundary so a bucketed anchor is available for the second crossing.
10122    advance(&mut st, mined_height + 1, 2 * interval_blocks + 1);
10123
10124    // The precondition this test exists to exercise: the wallet really does hold an Ironwood note
10125    // now, so the assertion below that none is spent is not vacuously true.
10126    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    // Second crossing: the wallet now holds Ironwood, which the ordinary selector would prefer.
10143    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
10169/// Tests that payment construction enforces the ZIP 316 Revision 2 address expiration
10170/// rules: a payment to an address whose expiry height conflicts with the transaction's
10171/// expiry height is refused, a payment to an address whose expiry time has passed is
10172/// refused (for any recipient of a multi-recipient transaction), and a payment to an
10173/// address with unexpired metadata succeeds.
10174pub 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    // Add funds to the wallet: one note for each payment expected to succeed. The
10181    // payments expected to be refused are checked first, while every note is unspent.
10182    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    // Construct recipient addresses carrying expiry metadata from the account's own
10193    // default address, so that every receiver is valid for the active pool set.
10194    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    // An address is expired only once the chain has advanced *past* its expiry height,
10217    // so one expiring at exactly the chain tip is not yet expired. It is refused because
10218    // the transaction's default expiry height lies beyond it.
10219    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    // An expiry time before the current time makes the address expired; the constraint
10247    // applies to every recipient of a multi-recipient transaction.
10248    /// One second past the Unix epoch, long before the test clock's current time.
10249    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    // A payment to an address with unexpired metadata succeeds.
10288    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    // One block below the tip, the chain has advanced past the address's expiry height, so
10312    // the address is expired and the payment is refused on that ground rather than for a
10313    // transaction-expiry conflict.
10314    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    // The transaction's expiry height may equal the address's expiry height; only a
10342    // transaction expiry strictly greater than the address's is forbidden. The default
10343    // transaction expiry is the target height plus the default expiry delta for the
10344    // consensus branch in effect there, and the target height is the block after the chain tip.
10345    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    // An address carrying only an expiry time is expired only once the current time is
10373    // *after* it, so one expiring at exactly the current time is still valid.
10374    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
10399/// The number of blocks between Sapling activation and NU7 activation on the network used by the
10400/// NU7 default-expiry tests. It leaves room for the funding blocks below NU7 activation.
10401const NU7_ACTIVATION_OFFSET: u32 = 10;
10402
10403/// Returns a test network on which every upgrade through NU6.3 activates at Sapling activation,
10404/// and NU7 activates [`NU7_ACTIVATION_OFFSET`] blocks later.
10405fn 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
10419/// Builds a wallet on [`nu7_activating_network`] that holds two spendable notes, with its chain
10420/// tip two blocks below NU7 activation. The next transaction then targets the block before
10421/// activation.
10422fn 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
10449/// Tests that a transaction that the wallet creates with no explicit expiry height expires after
10450/// the default expiry delta for its target height: 40 blocks before NU7 activation, and 120 blocks
10451/// from NU7 activation onward.
10452pub 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/// Tests that a PCZT that the wallet creates with no explicit expiry height expires after the
10485/// default expiry delta for its target height: 40 blocks before NU7 activation, and 120 blocks
10486/// from NU7 activation onward.
10487///
10488/// The test network's heights lie in the ZIP 212 grace period, in which the wallet cannot create a
10489/// PCZT with Sapling outputs. `T` must therefore not be the Sapling pool.
10490#[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}