Skip to main content

zcash_client_sqlite/
wallet.rs

1//! Functions for querying information in the wallet database.
2//!
3//! These functions should generally not be used directly; instead,
4//! their functionality is available via the [`WalletRead`] and
5//! [`WalletWrite`] traits.
6//!
7//! [`WalletRead`]: zcash_client_backend::data_api::WalletRead
8//! [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite
9//!
10//! # Views
11//!
12//! The wallet database exposes the following views as part of its public API:
13//!
14//! ## `v_transactions`
15//!
16//! This view exposes the history of transactions that affect the balance of each account in the
17//! wallet. A transaction may be represented by multiple rows in this view, one for each account in
18//! the wallet that contributes funds to or receives funds from the transaction in question. Each
19//! row of the view contains:
20//! - `account_balance_delta`: the net effect of the transaction on the associated account's
21//!   balance. This value is positive when funds are received by the account, and negative when the
22//!   balance of the account decreases due to a spend.
23//! - `fee_paid`: the total fee paid to send the transaction, as a positive value. This fee is
24//!   associated with the transaction (similar to e.g. `txid` or `mined_height`), and not with any
25//!   specific account involved with that transaction. ` If multiple rows exist for a single
26//!   transaction, this fee amount will be repeated for each such row. Therefore, if more than one
27//!   of the wallet's accounts is involved with the transaction, this fee should be considered only
28//!   once in determining the total value sent from the wallet as a whole.
29//! - `pool_crossing_value`: non-NULL exactly when the transaction is a wallet-internal transfer
30//!   that moves the account's own funds between shielded pools (for example, a ZIP 318
31//!   Orchard -> Ironwood migration transfer): every wallet-spent note and wallet-received output
32//!   is shielded, the account spent at least one note, at least one output was received in a pool
33//!   the account spent nothing from, and no external outputs of the transaction are known. Its
34//!   value is the total received in the pools the account did not spend from, the amount that
35//!   crossed. For such a transaction `account_balance_delta` is just the negated fee, so this is
36//!   the amount to present to a user rather than the balance delta; deriving one from
37//!   `total_spent` or `total_received` instead overstates the crossing whenever the transaction
38//!   also returns change to a pool it spent from. Use `pool_crossing_value IS NOT NULL` as the
39//!   classification predicate; there is deliberately no separate boolean column, since it would
40//!   restate the same condition in a second place that could drift. A payment that returns value
41//!   to one of the wallet's own addresses is classified once the wallet has observed the returned
42//!   output (which the scanner marks as change); while such a transaction is unmined it is
43//!   treated as an ordinary payment.
44//!
45//! ### Seed Phrase with Single Account
46//!
47//! In the case that the seed phrase for in this wallet has only been used to create a single
48//! account, this view will contain one row per transaction, in the case that
49//! `account_balance_delta` is negative, it is usually safe to add `fee_paid` back to the
50//! `account_balance_delta` value to determine the amount sent to addresses outside the wallet.
51//!
52//! ### Seed Phrase with Multiple Accounts
53//!
54//! In the case that the seed phrase for in this wallet has been used to create multiple accounts,
55//! this view may contain multiple rows per transaction, one for each account involved. In this
56//! case, the total amount sent to addresses outside the wallet can usually be calculated by
57//! grouping rows by `id_tx` and then using `SUM(account_balance_delta) + MAX(fee_paid)`.
58//!
59//! ### Imported Seed Phrases
60//!
61//! If a seed phrase is imported, and not every account associated with it is loaded into the
62//! wallet, this view may show partial information about some transactions. In particular, any
63//! computation that involves both `account_balance_delta` and `fee_paid` is likely to be
64//! inaccurate.
65//!
66//! ## `v_tx_outputs`
67//!
68//! This view exposes the history of transaction outputs received by and sent from the wallet,
69//! keyed by transaction ID, pool type, and output index. The contents of this view are useful for
70//! producing a detailed report of the effects of a transaction. Each row of this view contains:
71//! - `from_account_id` for sent outputs, the account from which the value was sent.
72//! - `to_account_id` in the case that the output was received by an account in the wallet, the
73//!   identifier for the account receiving the funds.
74//! - `to_address` the address to which an output was sent, or the address at which value was
75//!   received in the case of received transparent funds.
76//! - `value` the value of the output. This is always a positive number, for both sent and received
77//!   outputs.
78//! - `is_change` a boolean flag indicating whether this is a change output belonging to the
79//!   wallet.
80//! - `memo` the shielded memo associated with the output, if any.
81
82use std::{
83    collections::{HashMap, HashSet},
84    convert::TryFrom,
85    io::{self, Cursor},
86    num::NonZeroU32,
87    ops::{Range, RangeInclusive},
88    time::SystemTime,
89};
90
91use encoding::{
92    KeyScope, ReceiverFlags, account_kind_code, decode_diversifier_index_be,
93    encode_diversifier_index_be, memo_repr, parse_pool_code, pool_code,
94};
95use incrementalmerkletree::{Marking, Retention};
96use rusqlite::{self, Connection, OptionalExtension, named_params, params};
97use secrecy::{ExposeSecret, SecretVec};
98use shardtree::{error::ShardTreeError, store::ShardStore};
99use tracing::warn;
100use uuid::Uuid;
101
102use zcash_address::ZcashAddress;
103use zcash_client_backend::{
104    DecryptedOutput,
105    data_api::{
106        Account as _, AccountBalance, AccountBirthday, AccountPurpose, AccountSource, AddressInfo,
107        AddressSource, BlockMetadata, Progress, Ratio, ReceivedTransactionOutput,
108        SAPLING_SHARD_HEIGHT, SentTransaction, SentTransactionOutput, TransactionDataRequest,
109        TransactionStatus, WalletSummary, Zip32Derivation,
110        anchor_retention::AnchorRetentionInterval,
111        chain::ChainState,
112        defaults::address_receiver_matches_ua,
113        error::{FindAccountForAddressError, RewindError},
114        scanning::{ScanPriority, ScanRange},
115        wallet::{ConfirmationsPolicy, TargetHeight},
116    },
117    wallet::{Note, NoteId, Recipient, WalletTx},
118};
119use zcash_keys::{
120    address::{Address, Receiver, UnifiedAddress},
121    encoding::AddressCodec,
122    keys::{
123        AddressGenerationError, ReceiverRequirement, UnifiedAddressRequest, UnifiedFullViewingKey,
124        UnifiedIncomingViewingKey, UnifiedSpendingKey,
125    },
126};
127use zcash_primitives::{
128    block::BlockHash,
129    merkle_tree::{HashSer, read_commitment_tree},
130    transaction::{Transaction, TransactionData, builder::DEFAULT_TX_EXPIRY_DELTA, fees::zip317},
131};
132use zcash_protocol::{
133    PoolType, ShieldedPool, TxId,
134    consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters, TxIndex},
135    memo::{Memo, MemoBytes},
136    value::{ZatBalance, Zatoshis},
137};
138use zip32::{DiversifierIndex, fingerprint::SeedFingerprint};
139
140use self::{
141    common::{TableConstants, table_constants},
142    scanning::{parse_priority_code, priority_code, replace_queue_entries},
143};
144use crate::{
145    AccountRef, AccountUuid, AddressRef, PRUNING_DEPTH, SqlTransaction, TransferType, TxRef,
146    WalletCommitmentTrees, WalletDb,
147    error::{BackendError, SqliteClientError},
148    util::Clock,
149    wallet::{
150        commitment_tree::{SqliteShardStore, get_max_checkpointed_height},
151        encoding::LEGACY_ADDRESS_INDEX_NULL,
152    },
153};
154
155#[cfg(feature = "transparent-inputs")]
156use {
157    crate::GapLimits,
158    ::transparent::{
159        bundle::{OutPoint, TxOut},
160        keys::{IncomingViewingKey as _, NonHardenedChildIndex, TransparentKeyScope},
161    },
162    ReceiverRequirement::*,
163    rusqlite::types::Value,
164    std::rc::Rc,
165    zcash_client_backend::{data_api::DecryptedTransaction, wallet::WalletTransparentOutput},
166};
167
168#[cfg(feature = "orchard")]
169use zcash_client_backend::data_api::{IRONWOOD_SHARD_HEIGHT, ORCHARD_SHARD_HEIGHT};
170
171use FindAccountForAddressError as E;
172#[cfg(feature = "zcashd-compat")]
173use {
174    crate::wallet::encoding::{decode_legacy_account_index, encode_legacy_account_index},
175    zcash_keys::keys::zcashd,
176};
177#[cfg(feature = "transparent-key-import")]
178use {
179    ::transparent::address::TransparentAddress,
180    zcash_script::{descriptor::sh, script::Evaluable},
181};
182
183pub mod commitment_tree;
184pub(crate) mod common;
185mod db;
186pub(crate) mod encoding;
187pub mod init;
188pub(crate) mod locking;
189#[cfg(feature = "orchard")]
190pub(crate) mod orchard;
191pub(crate) mod sapling;
192pub(crate) mod scanning;
193#[cfg(feature = "transparent-inputs")]
194pub(crate) mod transparent;
195
196pub(crate) const BLOCK_SAPLING_FRONTIER_ABSENT: &[u8] = &[0x0];
197
198/// A constant for use in converting Unix timestamps to shielded-only diversifier indices. The
199/// value here is intended to be added to the current time, in seconds since the epoch, to obtain
200/// an index that is greater than or equal to 2^32. While it would be possible to use indices in
201/// the range 2^31..2^32, we wish to avoid any confusion with indices in the BIP 32 child
202/// index derivation space.
203///
204/// 2^32 - (date --date "Oct 28, 2016 07:56 UTC" +%s)
205pub(crate) const MIN_SHIELDED_DIVERSIFIER_OFFSET: u64 = 2817325936;
206
207fn parse_account_source(
208    account_kind: u32,
209    hd_seed_fingerprint: Option<[u8; 32]>,
210    hd_account_index: Option<u32>,
211    #[cfg(feature = "zcashd-compat")] legacy_account_index: i64,
212    spending_key_available: bool,
213    key_source: Option<String>,
214) -> Result<AccountSource, SqliteClientError> {
215    let derivation = hd_seed_fingerprint
216        .zip(hd_account_index)
217        .map(|(seed_fp, idx)| {
218            zip32::AccountId::try_from(idx).map_or_else(
219                |_| {
220                    Err(SqliteClientError::CorruptedData(
221                        "ZIP-32 account ID is out of range.".to_string(),
222                    ))
223                },
224                |idx| {
225                    Ok(Zip32Derivation::new(
226                        SeedFingerprint::from_bytes(seed_fp),
227                        idx,
228                        #[cfg(feature = "zcashd-compat")]
229                        decode_legacy_account_index(legacy_account_index)?,
230                    ))
231                },
232            )
233        })
234        .transpose()?;
235
236    match (account_kind, derivation) {
237        (0, Some(derivation)) => Ok(AccountSource::Derived {
238            derivation,
239            key_source,
240        }),
241        (1, derivation) => Ok(AccountSource::Imported {
242            purpose: if spending_key_available {
243                AccountPurpose::Spending { derivation }
244            } else {
245                AccountPurpose::ViewOnly
246            },
247            key_source,
248        }),
249        (0, None) => Err(SqliteClientError::CorruptedData(
250            "Wallet DB account_kind constraint violated".to_string(),
251        )),
252        (_, _) => Err(SqliteClientError::CorruptedData(
253            "Unrecognized account_kind".to_string(),
254        )),
255    }
256}
257
258/// The viewing key that an [`Account`] has available to it.
259#[derive(Debug, Clone)]
260pub(crate) enum ViewingKey {
261    /// A full viewing key.
262    ///
263    /// This is available to derived accounts, as well as accounts directly imported as
264    /// full viewing keys.
265    Full(Box<UnifiedFullViewingKey>),
266
267    /// An incoming viewing key.
268    ///
269    /// Accounts that have this kind of viewing key cannot be used in wallet contexts,
270    /// because they are unable to maintain an accurate balance.
271    Incoming(Box<UnifiedIncomingViewingKey>),
272}
273
274/// An account stored in a `zcash_client_sqlite` database.
275#[derive(Debug, Clone)]
276pub struct Account {
277    id: AccountRef,
278    uuid: AccountUuid,
279    name: Option<String>,
280    kind: AccountSource,
281    viewing_key: ViewingKey,
282    birthday: BlockHeight,
283}
284
285impl Account {
286    /// Returns the default Unified Address for the account, along with the diversifier index that
287    /// generated it.
288    ///
289    /// The diversifier index may be non-zero if the Unified Address includes a Sapling
290    /// receiver, and there was no valid Sapling receiver at diversifier index zero.
291    pub(crate) fn default_address(
292        &self,
293        request: UnifiedAddressRequest,
294    ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> {
295        self.uivk().default_address(request)
296    }
297
298    pub(crate) fn internal_id(&self) -> AccountRef {
299        self.id
300    }
301
302    pub(crate) fn birthday(&self) -> BlockHeight {
303        self.birthday
304    }
305}
306
307impl zcash_client_backend::data_api::Account for Account {
308    type AccountId = AccountUuid;
309
310    fn id(&self) -> AccountUuid {
311        self.uuid
312    }
313
314    fn name(&self) -> Option<&str> {
315        self.name.as_deref()
316    }
317
318    fn birthday_height(&self) -> BlockHeight {
319        self.birthday()
320    }
321
322    fn source(&self) -> &AccountSource {
323        &self.kind
324    }
325
326    fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
327        self.viewing_key.ufvk()
328    }
329
330    fn uivk(&self) -> UnifiedIncomingViewingKey {
331        self.viewing_key.uivk()
332    }
333}
334
335impl ViewingKey {
336    fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
337        match self {
338            ViewingKey::Full(ufvk) => Some(ufvk),
339            ViewingKey::Incoming(_) => None,
340        }
341    }
342
343    fn uivk(&self) -> UnifiedIncomingViewingKey {
344        match self {
345            ViewingKey::Full(ufvk) => ufvk.as_ref().to_unified_incoming_viewing_key(),
346            ViewingKey::Incoming(uivk) => uivk.as_ref().clone(),
347        }
348    }
349}
350
351/// Serialized IVK items extracted from a [`UnifiedIncomingViewingKey`] for storage
352/// in the `accounts` table cache columns.
353struct IvkItemCache {
354    orchard: Option<Vec<u8>>,
355    sapling: Option<Vec<u8>>,
356    p2pkh: Option<Vec<u8>>,
357}
358
359impl IvkItemCache {
360    fn from_uivk(uivk: &UnifiedIncomingViewingKey) -> Self {
361        #[cfg(feature = "orchard")]
362        let orchard = uivk.orchard().as_ref().map(|k| k.to_bytes().to_vec());
363        #[cfg(not(feature = "orchard"))]
364        let orchard = None;
365
366        let sapling = uivk.sapling().as_ref().map(|k| k.to_bytes().to_vec());
367
368        #[cfg(feature = "transparent-inputs")]
369        let p2pkh = uivk.transparent().as_ref().map(|k| k.serialize());
370        #[cfg(not(feature = "transparent-inputs"))]
371        let p2pkh = None;
372
373        IvkItemCache {
374            orchard,
375            sapling,
376            p2pkh,
377        }
378    }
379}
380
381pub(crate) fn seed_matches_derived_account<P: consensus::Parameters>(
382    params: &P,
383    seed: &SecretVec<u8>,
384    seed_fingerprint: &SeedFingerprint,
385    account_index: zip32::AccountId,
386    uivk: &UnifiedIncomingViewingKey,
387) -> Result<bool, SqliteClientError> {
388    let seed_fingerprint_match =
389        &SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
390            SqliteClientError::BadAccountData(
391                "Seed must be between 32 and 252 bytes in length.".to_owned(),
392            )
393        })? == seed_fingerprint;
394
395    // `UnifiedIncomingViewingKey`s are not comparable with `Eq`, but Unified Address
396    // components are, so we derive corresponding addresses for each key and use
397    // those to check whether any components match.
398    let uivk_match = {
399        let usk = UnifiedSpendingKey::from_seed(params, &seed.expose_secret()[..], account_index)
400            .map_err(|_| SqliteClientError::KeyDerivationError(account_index))?;
401
402        let (seed_addr, _) = usk
403            .to_unified_full_viewing_key()
404            .default_address(UnifiedAddressRequest::AllAvailableKeys)?;
405        let (uivk_addr, _) = uivk.default_address(UnifiedAddressRequest::AllAvailableKeys)?;
406
407        #[cfg(not(feature = "orchard"))]
408        let orchard_match = false;
409        #[cfg(feature = "orchard")]
410        let orchard_match = seed_addr
411            .orchard()
412            .zip(uivk_addr.orchard())
413            .map(|(a, b)| a == b)
414            == Some(true);
415
416        let sapling_match = seed_addr
417            .sapling()
418            .zip(uivk_addr.sapling())
419            .map(|(a, b)| a == b)
420            == Some(true);
421
422        let p2pkh_match = seed_addr
423            .transparent()
424            .zip(uivk_addr.transparent())
425            .map(|(a, b)| a == b)
426            == Some(true);
427
428        orchard_match || sapling_match || p2pkh_match
429    };
430
431    if seed_fingerprint_match != uivk_match {
432        // If these mismatch, it suggests database corruption.
433        Err(SqliteClientError::CorruptedData(format!(
434            "Seed fingerprint match: {seed_fingerprint_match}, uivk match: {uivk_match}"
435        )))
436    } else {
437        Ok(seed_fingerprint_match && uivk_match)
438    }
439}
440
441// Returns the highest used account index for a given seed.
442pub(crate) fn max_zip32_account_index(
443    conn: &rusqlite::Connection,
444    seed_id: &SeedFingerprint,
445) -> Result<Option<zip32::AccountId>, SqliteClientError> {
446    conn.query_row_and_then(
447        "SELECT MAX(hd_account_index) FROM accounts WHERE hd_seed_fingerprint = :hd_seed",
448        [seed_id.to_bytes()],
449        |row| {
450            row.get::<_, Option<u32>>(0)?
451                .map(zip32::AccountId::try_from)
452                .transpose()
453                .map_err(|_| SqliteClientError::Zip32AccountIndexOutOfRange)
454        },
455    )
456}
457
458pub(crate) fn add_account<P: consensus::Parameters>(
459    conn: &rusqlite::Transaction,
460    params: &P,
461    account_name: &str,
462    kind: &AccountSource,
463    viewing_key: ViewingKey,
464    birthday: &AccountBirthday,
465    #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
466) -> Result<Account, SqliteClientError> {
467    // Check whether any IVK component collides with an existing account.
468    let uivk = viewing_key.uivk();
469    if let Some(existing_account) = get_account_for_uivk(conn, params, &uivk)? {
470        match (&viewing_key, existing_account.ufvk()) {
471            (ViewingKey::Full(new_ufvk), _) => {
472                // FVK import over an existing account. The upgrade function
473                // validates that the new FVK strictly adds capability.
474                return upgrade_account_ufvk(conn, params, &existing_account, new_ufvk);
475            }
476            (ViewingKey::Incoming(_), Some(_)) => {
477                // IVK-over-FVK: the existing account already has full viewing
478                // capability. Importing a lower-capability key is not permitted.
479                return Err(SqliteClientError::AccountCollision(existing_account.id()));
480            }
481            (ViewingKey::Incoming(_), None) => {
482                // IVK-over-IVK: the upgrade function validates that the new
483                // UIVK strictly adds capability.
484                return upgrade_account_uivk(conn, params, &existing_account, &uivk);
485            }
486        }
487    }
488
489    let account_uuid = AccountUuid(Uuid::new_v4());
490
491    let (derivation, spending_key_available, key_source) = match kind {
492        AccountSource::Derived {
493            derivation,
494            key_source,
495        } => (Some(derivation), true, key_source),
496        AccountSource::Imported {
497            purpose: AccountPurpose::Spending { derivation },
498            key_source,
499        } => (derivation.as_ref(), true, key_source),
500        AccountSource::Imported {
501            purpose: AccountPurpose::ViewOnly,
502            key_source,
503        } => (None, false, key_source),
504    };
505
506    let ivk_cache = IvkItemCache::from_uivk(&uivk);
507
508    let birthday_sapling_tree_size = Some(birthday.sapling_frontier().tree_size());
509    #[cfg(feature = "orchard")]
510    let birthday_orchard_tree_size = Some(birthday.orchard_frontier().tree_size());
511    #[cfg(not(feature = "orchard"))]
512    let birthday_orchard_tree_size: Option<u64> = None;
513
514    #[cfg(feature = "zcashd-compat")]
515    let zcashd_legacy_address_index =
516        encode_legacy_account_index(derivation.and_then(|d| d.legacy_address_index()));
517    #[cfg(not(feature = "zcashd-compat"))]
518    let zcashd_legacy_address_index: i64 = LEGACY_ADDRESS_INDEX_NULL;
519
520    let ufvk_encoded = viewing_key.ufvk().map(|ufvk| ufvk.encode(params));
521    let account_id = conn
522        .query_row(
523            r#"
524            INSERT INTO accounts (
525                name,
526                uuid,
527                account_kind, hd_seed_fingerprint, hd_account_index,
528                zcashd_legacy_address_index,
529                key_source,
530                ufvk, uivk,
531                orchard_ivk_item_cache, sapling_ivk_item_cache, p2pkh_ivk_item_cache,
532                birthday_height, birthday_sapling_tree_size, birthday_orchard_tree_size,
533                recover_until_height,
534                has_spend_key
535            )
536            VALUES (
537                :account_name,
538                :uuid,
539                :account_kind, :hd_seed_fingerprint, :hd_account_index,
540                :zcashd_legacy_address_index,
541                :key_source,
542                :ufvk, :uivk,
543                :orchard_ivk_item_cache, :sapling_ivk_item_cache, :p2pkh_ivk_item_cache,
544                :birthday_height, :birthday_sapling_tree_size, :birthday_orchard_tree_size,
545                :recover_until_height,
546                :has_spend_key
547            )
548            RETURNING id
549            "#,
550            named_params![
551                ":account_name": account_name,
552                ":uuid": account_uuid.0,
553                ":account_kind": account_kind_code(kind),
554                ":hd_seed_fingerprint": derivation.map(|d| d.seed_fingerprint().to_bytes()),
555                ":hd_account_index": derivation.map(|d| u32::from(d.account_index())),
556                ":zcashd_legacy_address_index": zcashd_legacy_address_index,
557                ":key_source": key_source,
558                ":ufvk": ufvk_encoded,
559                ":uivk": uivk.encode(params),
560                ":orchard_ivk_item_cache": ivk_cache.orchard,
561                ":sapling_ivk_item_cache": ivk_cache.sapling,
562                ":p2pkh_ivk_item_cache": ivk_cache.p2pkh,
563                ":birthday_height": u32::from(birthday.height()),
564                ":birthday_sapling_tree_size": birthday_sapling_tree_size,
565                ":birthday_orchard_tree_size": birthday_orchard_tree_size,
566                ":recover_until_height": birthday.recover_until().map(u32::from),
567                ":has_spend_key": i64::from(spending_key_available),
568            ],
569            |row| row.get(0).map(AccountRef),
570        )
571        .map_err(|e| match e {
572            rusqlite::Error::SqliteFailure(f, s)
573                if f.code == rusqlite::ErrorCode::ConstraintViolation =>
574            {
575                // An account conflict occurred. This should already have been caught by
576                // the IVK collision check above, but in case it wasn't, make a best
577                // effort to determine the AccountRef of the pre-existing row and provide
578                // that to our caller.
579                if let Ok(colliding_uuid) = conn.query_row(
580                    "SELECT uuid FROM accounts WHERE ufvk = ?",
581                    params![ufvk_encoded],
582                    |row| Ok(AccountUuid(row.get(0)?)),
583                ) {
584                    return SqliteClientError::AccountCollision(colliding_uuid);
585                }
586
587                SqliteClientError::from(rusqlite::Error::SqliteFailure(f, s))
588            }
589            _ => SqliteClientError::from(e),
590        })?;
591
592    let account = Account {
593        id: account_id,
594        name: Some(account_name.to_owned()),
595        uuid: account_uuid,
596        kind: kind.clone(),
597        viewing_key,
598        birthday: birthday.height(),
599    };
600
601    // Bring the wallet's note commitment tree, scan queue, and birthday metadata into a state
602    // consistent with the new account's birthday by rewinding to the chain state prior to the
603    // birthday height. The new account is the only entry in `reset_account_birthdays`;
604    // existing accounts retain their own birthdays.
605    //
606    // This handles two concerns that the previous manual implementation couldn't address
607    // safely together:
608    //   - Note commitment tree data above the pruning floor is removed so that subsequent
609    //     re-scanning cannot conflict with stale tree state from prior scans.
610    //   - The scan queue above `birthday.height() - 1` is overwritten with a `Historic`
611    //     rescan range so that blocks that must be re-scanned for the new account's notes
612    //     are queued.
613    match rewind_to_chain_state(
614        conn,
615        params,
616        #[cfg(feature = "transparent-inputs")]
617        gap_limits,
618        birthday.prior_chain_state(),
619        std::iter::once(account_uuid).collect(),
620    ) {
621        Ok(()) => {}
622        Err(RewindError::DataSource(e)) => return Err(e),
623        Err(RewindError::RewindBeyondBirthdays(_)) => {
624            // Cannot occur: `reset_account_birthdays` is non-empty (it contains the new
625            // account), so `rewind_to_chain_state`'s contract specifies that this variant is
626            // not returned.
627            unreachable!(
628                "rewind_to_chain_state cannot return RewindBeyondBirthdays with a non-empty \
629                 reset_account_birthdays set"
630            );
631        }
632        // `RewindError` is `#[non_exhaustive]`, so a variant introduced by a future
633        // `zcash_client_backend` release has no specific handling here until this crate is
634        // updated. Fail the account addition rather than proceeding on an unknown outcome.
635        Err(e) => {
636            return Err(SqliteClientError::BackendError(BackendError::Rewind(
637                Box::new(e),
638            )));
639        }
640    }
641
642    // The ignored range always starts at Sapling activation
643    let sapling_activation_height = params
644        .activation_height(NetworkUpgrade::Sapling)
645        // Fall back to the genesis block in regtest mode.
646        .unwrap_or_else(|| BlockHeight::from(0));
647
648    // Add the ignored range up to the birthday height.
649    if sapling_activation_height < birthday.height() {
650        let ignored_range = sapling_activation_height..birthday.height();
651
652        replace_queue_entries::<SqliteClientError>(
653            conn,
654            &ignored_range,
655            Some(ScanRange::from_parts(
656                ignored_range.clone(),
657                ScanPriority::Ignored,
658            ))
659            .into_iter(),
660            false,
661        )?;
662    };
663
664    // Always derive the default Unified Address for the account. If the account's viewing
665    // key has fewer components than the wallet supports (most likely due to this being an
666    // imported viewing key), derive an address containing the common subset of receivers.
667    let (address, d_idx) = account.default_address(UnifiedAddressRequest::AllAvailableKeys)?;
668    upsert_address(
669        conn,
670        params,
671        account_id,
672        d_idx,
673        &address,
674        Some(birthday.height()),
675        false,
676    )?;
677
678    // Pre-generate external transparent addresses prior to the index of the default address.
679    #[cfg(feature = "transparent-inputs")]
680    if let Ok(default_addr_idx) = NonHardenedChildIndex::try_from(d_idx) {
681        transparent::generate_address_range(
682            conn,
683            params,
684            account_id,
685            TransparentKeyScope::EXTERNAL,
686            UnifiedAddressRequest::ALLOW_ALL,
687            NonHardenedChildIndex::const_from_index(0)..default_addr_idx,
688            false,
689        )?
690    }
691
692    // Pre-generate transparent addresses up to the gap limits for the external, internal,
693    // and ephemeral key scopes.
694    #[cfg(feature = "transparent-inputs")]
695    for key_scope in [
696        TransparentKeyScope::EXTERNAL,
697        TransparentKeyScope::INTERNAL,
698        TransparentKeyScope::EPHEMERAL,
699    ] {
700        transparent::generate_gap_addresses(
701            conn,
702            params,
703            gap_limits,
704            account_id,
705            key_scope,
706            UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
707            false,
708        )?;
709    }
710
711    Ok(account)
712}
713
714pub(crate) fn delete_account(
715    conn: &rusqlite::Transaction,
716    account_uuid: AccountUuid,
717) -> Result<(), SqliteClientError> {
718    // Update all `sent_notes` records where `to_account_id` refers to the account to be deleted to
719    // have the `to_address` field set instead to the address at which the output was received.
720    let mut to_account_tx = conn.prepare(
721        r#"
722        SELECT
723            sn.id AS sent_note_id,
724            COALESCE(addresses.address, addresses.cached_transparent_receiver_address) AS to_address
725        FROM sent_notes sn
726        JOIN v_received_outputs ro ON ro.sent_note_id = sn.id
727        JOIN addresses ON addresses.id = ro.address_id
728        JOIN accounts ta ON ta.id = sn.to_account_id
729        WHERE ta.uuid = :account_uuid
730        "#,
731    )?;
732
733    let mut update_sent_note = conn.prepare(
734        r#"
735        UPDATE sent_notes
736        SET to_address = :to_address, to_account_id = NULL
737        WHERE id = :sent_note_id
738        "#,
739    )?;
740
741    let mut rows = to_account_tx.query(named_params![
742        ":account_uuid": account_uuid.0,
743    ])?;
744
745    while let Some(row) = rows.next()? {
746        if let Some(address) = row.get::<_, Option<String>>("to_address")? {
747            update_sent_note.execute(named_params![
748                ":sent_note_id": row.get::<_, i64>("sent_note_id")?,
749                ":to_address": address
750            ])?;
751        }
752    }
753
754    // Delete all transaction information that is solely linked to this account. This
755    // effectively reverts the wallet state for this account to the time before its
756    // viewing keys and transparent addresses had been used to scan for information.
757    conn.execute(
758        r#"
759        WITH account_transactions AS (
760            SELECT ro.transaction_id
761            FROM v_received_outputs ro
762            JOIN accounts a ON a.id = ro.account_id
763            WHERE a.uuid = :account_uuid
764            UNION
765            SELECT ros.transaction_id
766            FROM v_received_output_spends ros
767            JOIN accounts sa ON sa.id = ros.account_id
768            WHERE sa.uuid = :account_uuid
769        ),
770        non_account_transactions AS (
771            SELECT ro.transaction_id
772            FROM v_received_outputs ro
773            JOIN accounts a ON a.id = ro.account_id
774            WHERE a.uuid != :account_uuid
775            UNION
776            SELECT ros.transaction_id
777            FROM v_received_output_spends ros
778            JOIN accounts sa ON sa.id = ros.account_id
779            WHERE sa.uuid != :account_uuid
780        )
781        DELETE FROM transactions WHERE id_tx IN (
782            SELECT transaction_id FROM account_transactions
783            EXCEPT
784            SELECT transaction_id FROM non_account_transactions
785        )
786        "#,
787        named_params![
788            ":account_uuid": account_uuid.0,
789        ],
790    )?;
791
792    // At this point, the only information remaining about the account is its entry in the
793    // accounts table and its addresses; delete them. Any in-progress pool migration for this
794    // account is removed by the `ON DELETE CASCADE` on `orchard_ironwood_migrations.account_id`
795    // (its own child rows cascade from it in turn).
796    conn.execute(
797        "DELETE FROM accounts WHERE uuid = :account_uuid",
798        named_params![
799            ":account_uuid": account_uuid.0,
800        ],
801    )?;
802
803    Ok(())
804}
805
806/// Returns `true` if `address` (an encoded transparent receiver address) is already recorded
807/// in the `addresses` table, whether as a derived account receiver or as a prior standalone
808/// import.
809///
810/// A transparent receiver appears at most once in `addresses`, enforced by the UNIQUE index on
811/// `cached_transparent_receiver_address`. Callers that would otherwise insert a fresh row for a
812/// receiver can use this to detect an existing row and avoid violating that constraint.
813#[cfg(feature = "transparent-key-import")]
814pub(crate) fn transparent_receiver_address_exists(
815    conn: &rusqlite::Connection,
816    address: &str,
817) -> Result<bool, SqliteClientError> {
818    Ok(conn
819        .query_row(
820            "SELECT 1 FROM addresses WHERE cached_transparent_receiver_address = :address",
821            named_params![":address": address],
822            |_row| Ok(()),
823        )
824        .optional()?
825        .is_some())
826}
827
828/// Imports a standalone transparent P2PKH receiver by its pubkey into the given account.
829///
830/// Returns the number of address rows inserted: `1` when a new receiver row was added, or `0`
831/// when nothing was inserted because the receiver address was already present in the wallet.
832#[cfg(feature = "transparent-key-import")]
833pub(crate) fn import_standalone_transparent_pubkey<P: consensus::Parameters>(
834    conn: &rusqlite::Transaction,
835    params: &P,
836    account_uuid: AccountUuid,
837    pubkey: secp256k1::PublicKey,
838) -> Result<usize, SqliteClientError> {
839    // Resolve the account up front so an unknown account is reported explicitly, rather than
840    // inferred from a zero-row INSERT.
841    let account_id = get_account_ref(conn, account_uuid)?;
842    import_standalone_transparent_pubkey_inner(conn, params, account_uuid, account_id, pubkey)
843}
844
845/// Imports a batch of standalone transparent P2PKH receivers by their pubkeys into the given
846/// account, resolving the account a single time for the whole batch (rather than once per
847/// pubkey). Returns the total number of address rows inserted.
848#[cfg(feature = "transparent-key-import")]
849pub(crate) fn import_standalone_transparent_pubkeys<P: consensus::Parameters>(
850    conn: &rusqlite::Transaction,
851    params: &P,
852    account_uuid: AccountUuid,
853    pubkeys: &[secp256k1::PublicKey],
854) -> Result<usize, SqliteClientError> {
855    let account_id = get_account_ref(conn, account_uuid)?;
856    let mut inserted = 0;
857    for pubkey in pubkeys {
858        inserted += import_standalone_transparent_pubkey_inner(
859            conn,
860            params,
861            account_uuid,
862            account_id,
863            *pubkey,
864        )?;
865    }
866    Ok(inserted)
867}
868
869/// Imports a single standalone transparent P2PKH receiver into the account identified by both
870/// `account_uuid` (for the cross-account conflict check) and its already-resolved `account_id`.
871///
872/// Returns the number of address rows inserted (`1` when a new receiver row was added, `0` when
873/// nothing was inserted because the receiver address was already present).
874#[cfg(feature = "transparent-key-import")]
875fn import_standalone_transparent_pubkey_inner<P: consensus::Parameters>(
876    conn: &rusqlite::Transaction,
877    params: &P,
878    account_uuid: AccountUuid,
879    account_id: AccountRef,
880    pubkey: secp256k1::PublicKey,
881) -> Result<usize, SqliteClientError> {
882    let existing_import_account = conn
883        .query_row(
884            "SELECT accounts.uuid AS account_uuid
885             FROM addresses
886             JOIN accounts ON accounts.id = addresses.account_id
887             WHERE imported_transparent_receiver_pubkey = :imported_transparent_receiver_pubkey",
888            named_params![
889                ":imported_transparent_receiver_pubkey": pubkey.serialize()
890            ],
891            |row| row.get::<_, Uuid>("account_uuid"),
892        )
893        .optional()?;
894
895    if let Some(current) = existing_import_account {
896        if current == account_uuid.expose_uuid() {
897            // The key has already been imported; nothing to do.
898            return Ok(0);
899        } else {
900            return Err(SqliteClientError::StandaloneImportConflict(current));
901        }
902    }
903
904    let addr_str = Address::Transparent(TransparentAddress::from_pubkey(&pubkey)).encode(params);
905
906    // If this transparent receiver is already recorded (for example it was derived as an
907    // account receiver, so its row carries a NULL `imported_transparent_receiver_pubkey` and is
908    // therefore not matched by the pubkey lookup above), do not insert a second row for the same
909    // `cached_transparent_receiver_address`: the UNIQUE index on that column forbids it, and the
910    // existing representation already covers the address. This is the import-direction
911    // counterpart of the resolution in `store_address_range`, which upgrades an imported receiver
912    // in place when the same address is later derived.
913    if transparent_receiver_address_exists(conn, &addr_str)? {
914        return Ok(0);
915    }
916
917    let rows_affected = conn.execute(
918        r#"
919        INSERT INTO addresses (
920          account_id, key_scope, address, cached_transparent_receiver_address,
921          receiver_flags, imported_transparent_receiver_pubkey
922        )
923        VALUES (
924          :account_id, :key_scope, :address, :address,
925          :receiver_flags, :imported_transparent_receiver_pubkey
926        )
927        "#,
928        named_params![
929            ":account_id": account_id.0,
930            ":key_scope": KeyScope::Foreign.encode(),
931            ":address": addr_str,
932            ":receiver_flags": ReceiverFlags::P2PKH.bits(),
933            ":imported_transparent_receiver_pubkey": pubkey.serialize()
934        ],
935    )?;
936
937    // The account is known (resolved above) and the receiver is not already recorded (checked
938    // above), so exactly one row is inserted.
939    Ok(rows_affected)
940}
941
942#[cfg(feature = "transparent-key-import")]
943pub(crate) fn import_standalone_transparent_script<P: consensus::Parameters>(
944    conn: &rusqlite::Transaction,
945    params: &P,
946    account_uuid: AccountUuid,
947    redeem_script: zcash_script::script::Redeem,
948) -> Result<(), SqliteClientError> {
949    // Resolve the account up front so an unknown account is reported explicitly, rather than
950    // inferred from a zero-row INSERT below.
951    let account_id = get_account_ref(conn, account_uuid)?;
952
953    // This mirrors `zcash_script::opcode::push_value::LargeValue::MAX_SIZE`, which is
954    // currently `pub(crate)`. Replace with a direct reference if it becomes public.
955    const MAX_P2SH_REDEEM_SCRIPT_SIZE: usize = 520;
956    let rs_bytes = redeem_script.to_bytes();
957    if rs_bytes.len() > MAX_P2SH_REDEEM_SCRIPT_SIZE {
958        return Err(SqliteClientError::BadAccountData(format!(
959            "Redeem script exceeds maximum P2SH size of {MAX_P2SH_REDEEM_SCRIPT_SIZE} bytes (got {} bytes)",
960            rs_bytes.len()
961        )));
962    }
963
964    // Do not import script types which do not have a supported spend flow.
965    match zcash_script::solver::standard(&redeem_script) {
966        Some(zcash_script::solver::ScriptKind::MultiSig { .. }) => (),
967        _ => {
968            return Err(SqliteClientError::BadAccountData(
969                "Redeem script is not a supported P2SH script kind".to_owned(),
970            ));
971        }
972    }
973
974    let script_pubkey = sh(&redeem_script);
975    // `sh()` always produces a valid P2SH scriptPubKey, so `from_script_pubkey`
976    // should always succeed here. This is a defensive check.
977    let addr = TransparentAddress::from_script_pubkey(&script_pubkey).ok_or_else(|| {
978        SqliteClientError::CorruptedData(
979            "Could not derive P2SH address from redeem script".to_owned(),
980        )
981    })?;
982
983    let existing_import_account = conn
984        .query_row(
985            "SELECT accounts.uuid AS account_uuid
986             FROM addresses
987             JOIN accounts ON accounts.id = addresses.account_id
988             WHERE imported_transparent_receiver_script = :imported_transparent_receiver_script",
989            named_params![
990                ":imported_transparent_receiver_script": &rs_bytes[..]
991            ],
992            |row| row.get::<_, Uuid>("account_uuid"),
993        )
994        .optional()?;
995
996    if let Some(current) = existing_import_account {
997        if current == account_uuid.expose_uuid() {
998            // The key has already been imported; nothing to do.
999            return Ok(());
1000        } else {
1001            return Err(SqliteClientError::StandaloneImportConflict(current));
1002        }
1003    }
1004
1005    let addr_str = Address::Transparent(addr).encode(params);
1006    conn.execute(
1007        r#"
1008        INSERT INTO addresses (
1009          account_id, key_scope, address, cached_transparent_receiver_address,
1010          receiver_flags, imported_transparent_receiver_script
1011        )
1012        VALUES (
1013          :account_id, :key_scope, :address, :address,
1014          :receiver_flags, :imported_transparent_receiver_script
1015        )
1016        "#,
1017        named_params![
1018            ":account_id": account_id.0,
1019            ":key_scope": KeyScope::Foreign.encode(),
1020            ":address": addr_str,
1021            ":receiver_flags": ReceiverFlags::P2SH.bits(),
1022            ":imported_transparent_receiver_script": &rs_bytes[..]
1023        ],
1024    )?;
1025
1026    Ok(())
1027}
1028
1029pub(crate) fn get_next_available_address<P: consensus::Parameters, C: Clock>(
1030    conn: &rusqlite::Transaction,
1031    params: &P,
1032    clock: &C,
1033    account_uuid: AccountUuid,
1034    request: UnifiedAddressRequest,
1035    #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
1036) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, SqliteClientError> {
1037    let account: Account = match get_account(conn, params, account_uuid)? {
1038        Some(account) => account,
1039        None => {
1040            return Ok(None);
1041        }
1042    };
1043
1044    // This will also ensure that the provided request can be satisfied by the account's UIVK
1045    let requirements = account.uivk().receiver_requirements(request)?;
1046
1047    let (addr, diversifier_index) = if requirements.p2pkh() == ReceiverRequirement::Require {
1048        #[cfg(not(feature = "transparent-inputs"))]
1049        {
1050            return Err(SqliteClientError::AddressGeneration(
1051                AddressGenerationError::ReceiverTypeNotSupported(
1052                    zcash_address::unified::Typecode::P2pkh,
1053                ),
1054            ));
1055        }
1056
1057        // If a p2pkh receiver is required, return the first un-exposed address from within the
1058        // transparent gap limit.
1059        #[cfg(feature = "transparent-inputs")]
1060        {
1061            // First, ensure that we have pre-generated as many addresses as we can.
1062            transparent::generate_gap_addresses(
1063                conn,
1064                params,
1065                gap_limits,
1066                account.internal_id(),
1067                TransparentKeyScope::EXTERNAL,
1068                UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
1069                true,
1070            )?;
1071
1072            // Select indices from the transparent gap limit that are available for use as
1073            // diversifier indices.
1074            let (gap_start, addrs) = transparent::select_addrs_to_reserve(
1075                conn,
1076                params,
1077                account.internal_id(),
1078                TransparentKeyScope::EXTERNAL,
1079                gap_limits.external(),
1080                gap_limits
1081                    .external()
1082                    .try_into()
1083                    .expect("gap limit fits in usize"),
1084            )?;
1085
1086            // Find the first index that generates an address conforming to the request.
1087            addrs
1088                .iter()
1089                .find_map(|(_, _, meta)| {
1090                    meta.address_index()
1091                        .map(DiversifierIndex::from)
1092                        .and_then(|j| account.uivk().address(j, request).ok().map(|ua| (ua, j)))
1093                })
1094                .ok_or(SqliteClientError::ReachedGapLimit(
1095                    TransparentKeyScope::EXTERNAL,
1096                    gap_start.index() + gap_limits.external(),
1097                ))?
1098        }
1099    } else {
1100        // compute a base diversifier index from the timestamp
1101        let mut j = DiversifierIndex::from(
1102            clock
1103                .now()
1104                .duration_since(SystemTime::UNIX_EPOCH)
1105                .expect("system time is valid")
1106                .as_secs()
1107                .saturating_add(MIN_SHIELDED_DIVERSIFIER_OFFSET),
1108        );
1109
1110        let mut find_collision = conn.prepare(
1111            "SELECT exposed_at_height
1112             FROM addresses
1113             WHERE account_id = :account_id
1114             AND key_scope = :key_scope
1115             AND diversifier_index_be = :diversifier_index_be",
1116        )?;
1117
1118        // search the diversifier space for a diversifier index that creates a valid address
1119        // satisfying the request and is currently not used in an exposed address
1120        loop {
1121            let found_addr = account.uivk().find_address(j, request)?;
1122            let collision = find_collision
1123                .query_row(
1124                    named_params! {
1125                        ":account_id": account.internal_id().0,
1126                        ":key_scope": KeyScope::EXTERNAL.encode(),
1127                        ":diversifier_index_be": &encode_diversifier_index_be(found_addr.1)
1128                    },
1129                    |row| row.get::<_, Option<u32>>(0),
1130                )
1131                .optional()?
1132                .flatten();
1133
1134            if collision.is_none() {
1135                break found_addr;
1136            } else {
1137                j.increment().map_err(|_| {
1138                    SqliteClientError::AddressGeneration(
1139                        AddressGenerationError::DiversifierSpaceExhausted,
1140                    )
1141                })?;
1142            }
1143        }
1144    };
1145
1146    let chain_tip_height = chain_tip_height(conn)?.ok_or(SqliteClientError::ChainHeightUnknown)?;
1147    upsert_address(
1148        conn,
1149        params,
1150        account.internal_id(),
1151        diversifier_index,
1152        &addr,
1153        Some(chain_tip_height),
1154        true,
1155    )?;
1156
1157    Ok(Some((addr, diversifier_index)))
1158}
1159
1160pub(crate) fn list_addresses<P: consensus::Parameters>(
1161    conn: &rusqlite::Connection,
1162    params: &P,
1163    account_uuid: AccountUuid,
1164) -> Result<Vec<AddressInfo>, SqliteClientError> {
1165    let mut addrs = vec![];
1166
1167    let mut stmt_addrs = conn.prepare(
1168        "SELECT address, diversifier_index_be, key_scope
1169         FROM addresses
1170         JOIN accounts ON accounts.id = addresses.account_id
1171         WHERE accounts.uuid = :account_uuid
1172         AND exposed_at_height IS NOT NULL
1173         ORDER BY exposed_at_height ASC, diversifier_index_be ASC",
1174    )?;
1175
1176    let mut rows = stmt_addrs.query(named_params![
1177        ":account_uuid": account_uuid.0,
1178    ])?;
1179
1180    while let Some(row) = rows.next()? {
1181        let addr_str: String = row.get(0)?;
1182        let di_vec: Option<Vec<u8>> = row.get(1)?;
1183        let _scope = KeyScope::decode(row.get(2)?)?;
1184
1185        let addr = Address::decode(params, &addr_str).ok_or_else(|| {
1186            SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned())
1187        })?;
1188
1189        // Sapling and Unified addresses always have external scope.
1190        #[cfg(feature = "transparent-inputs")]
1191        let transparent_key_scope = matches!(addr, Address::Transparent(_) | Address::Tex(_))
1192            .then(|| _scope.into())
1193            .flatten();
1194
1195        let addr_source = match decode_diversifier_index_be(di_vec)? {
1196            Some(di) => Ok::<_, SqliteClientError>(AddressSource::Derived {
1197                diversifier_index: di,
1198                #[cfg(feature = "transparent-inputs")]
1199                transparent_key_scope,
1200            }),
1201            #[cfg(feature = "transparent-key-import")]
1202            None => Ok::<_, SqliteClientError>(AddressSource::Standalone),
1203            #[cfg(not(feature = "transparent-key-import"))]
1204            None => Err(SqliteClientError::CorruptedData(
1205                "diversifier index may not be null".to_string(),
1206            )),
1207        }?;
1208
1209        addrs.push(AddressInfo::from_parts(addr, addr_source).ok_or(
1210            SqliteClientError::CorruptedData(
1211                "transparent key scope information present for shielded address".to_string(),
1212            ),
1213        )?);
1214    }
1215
1216    Ok(addrs)
1217}
1218
1219/// Returns the wallet account that controls the given address, if any.
1220///
1221/// This is the SQLite-optimized implementation of
1222/// [`zcash_client_backend::data_api::WalletRead::find_account_for_address`].
1223///
1224/// Every call first tries a fast exact-match SQL query against the `addresses` table (see
1225/// [`find_account_by_exact_address`]). If that misses, the lookup is delegated to an
1226/// address-kind-specific fallback:
1227///
1228/// - For Unified Addresses, each account's [`UnifiedIncomingViewingKey`] is asked whether it
1229///   derived one of the UA's receivers, via
1230///   [`UnifiedIncomingViewingKey::decrypt_diversifiers`]. This finds every UA that any wallet
1231///   account could have produced, whether or not it was previously exposed.
1232/// - For Sapling addresses, stored UAs whose `receiver_flags` indicate a matching receiver
1233///   are scanned and [`address_receiver_matches_ua`] confirms the actual overlap. Unlike the
1234///   reference implementation at [`defaults::find_account_for_address`], this path does
1235///   **not** run UIVK algebra against the bare receiver; a bare Sapling address that is
1236///   derivable from an account's UIVK but has never been exposed as the Sapling component
1237///   of a tracked address will therefore resolve to `Ok(None)`. Callers that need
1238///   derivability-complete resolution for a bare Sapling address can wrap it in a
1239///   single-receiver [`UnifiedAddress`] and pass that instead.
1240/// - For transparent addresses, no fallback is needed: the exact-match query already covers
1241///   both standalone transparent rows and transparent receivers cached on stored UAs.
1242///
1243/// [`defaults::find_account_for_address`]: zcash_client_backend::data_api::defaults::find_account_for_address
1244/// [`UnifiedAddress`]: zcash_keys::address::UnifiedAddress
1245///
1246/// [`UnifiedIncomingViewingKey`]: zcash_keys::keys::UnifiedIncomingViewingKey
1247/// [`UnifiedIncomingViewingKey::decrypt_diversifiers`]: zcash_keys::keys::UnifiedIncomingViewingKey::decrypt_diversifiers
1248pub(crate) fn find_account_for_address<P: consensus::Parameters>(
1249    conn: &rusqlite::Connection,
1250    params: &P,
1251    address: &Address,
1252) -> Result<Option<AccountUuid>, FindAccountForAddressError<SqliteClientError>> {
1253    let addr_str = address.encode(params);
1254    // For a UA the transparent receiver (if any) may match the cached column; for non-UA
1255    // addresses the same string serves both roles (the `cached_transparent_receiver_address`
1256    // column only ever holds transparent addresses, so a Sapling query against it simply
1257    // never matches).
1258    let taddr_str = match address {
1259        Address::Unified(ua) => ua
1260            .transparent()
1261            .map(|t| Address::Transparent(*t).encode(params)),
1262        _ => Some(addr_str.clone()),
1263    };
1264
1265    if let Some(acc) =
1266        find_account_by_exact_address(conn, &addr_str, taddr_str.as_deref()).map_err(E::Backend)?
1267    {
1268        return Ok(Some(acc));
1269    }
1270
1271    match address {
1272        Address::Unified(ua) => find_account_for_unified_address_algebraic(conn, params, ua),
1273        Address::Sapling(_) => {
1274            find_account_for_shielded_address(conn, params, address, ReceiverFlags::SAPLING)
1275        }
1276        // For transparent addresses the exact-match query above is complete: the
1277        // `transparent_index_consistency` CHECK constraint and every INSERT path into the
1278        // `addresses` table guarantee that any stored UA with a transparent receiver also
1279        // has `cached_transparent_receiver_address` populated. When the `transparent-inputs`
1280        // feature is disabled the wallet cannot receive transparent funds, so a miss here
1281        // correctly resolves to no account.
1282        _ => Ok(None),
1283    }
1284}
1285
1286/// Looks for an account whose stored addresses contain an exact match for the given address
1287/// string or for its transparent-receiver sub-string.
1288///
1289/// Returns `Some(account)` if a row's `address` column equals `addr_str`, or a row's
1290/// `cached_transparent_receiver_address` column equals `taddr_str`.
1291fn find_account_by_exact_address(
1292    conn: &Connection,
1293    addr_str: &str,
1294    taddr_str: Option<&str>,
1295) -> Result<Option<AccountUuid>, SqliteClientError> {
1296    conn.query_row(
1297        "SELECT accounts.uuid
1298         FROM addresses
1299         JOIN accounts ON accounts.id = addresses.account_id
1300         WHERE address = :addr_str
1301            OR cached_transparent_receiver_address = :taddr_str
1302         LIMIT 1",
1303        named_params![
1304            ":addr_str": addr_str,
1305            ":taddr_str": taddr_str,
1306        ],
1307        |row| row.get::<_, Uuid>(0),
1308    )
1309    .optional()
1310    .map(|opt| opt.map(AccountUuid::from_uuid))
1311    .map_err(SqliteClientError::from)
1312}
1313
1314fn find_account_for_shielded_address<P: consensus::Parameters>(
1315    conn: &Connection,
1316    params: &P,
1317    address: &Address,
1318    shielded_flag: ReceiverFlags,
1319) -> Result<Option<AccountUuid>, FindAccountForAddressError<SqliteClientError>> {
1320    // The address may be a receiver embedded in a stored UA. Query candidate UAs via
1321    // `receiver_flags` and verify at the Rust level.
1322    let mut stmt = conn
1323        .prepare_cached(
1324            "SELECT accounts.uuid, addresses.address
1325             FROM addresses
1326             JOIN accounts ON accounts.id = addresses.account_id
1327             WHERE (receiver_flags & :shielded_flag) != 0",
1328        )
1329        .map_err(|e| E::Backend(e.into()))?;
1330
1331    let mut rows = stmt
1332        .query(named_params![":shielded_flag": shielded_flag.bits()])
1333        .map_err(|e| E::Backend(e.into()))?;
1334
1335    while let Some(row) = rows
1336        .next()
1337        .map_err(|e| E::Backend(SqliteClientError::from(e)))?
1338    {
1339        let row_uuid: Uuid = row.get(0).map_err(|e| E::Backend(e.into()))?;
1340        let stored_addr_str: String = row.get(1).map_err(|e| E::Backend(e.into()))?;
1341        let stored = Address::decode(params, &stored_addr_str).ok_or_else(|| {
1342            E::Backend(SqliteClientError::CorruptedData(
1343                "Not a valid Zcash recipient address".to_owned(),
1344            ))
1345        })?;
1346        if let Address::Unified(stored_ua) = stored
1347            && address_receiver_matches_ua(address, &stored_ua, params)
1348        {
1349            return Ok(Some(AccountUuid::from_uuid(row_uuid)));
1350        }
1351    }
1352
1353    Ok(None)
1354}
1355
1356fn find_account_for_unified_address_algebraic<P: consensus::Parameters>(
1357    conn: &Connection,
1358    params: &P,
1359    unified_address: &UnifiedAddress,
1360) -> Result<Option<AccountUuid>, FindAccountForAddressError<SqliteClientError>> {
1361    // Ask each account's UIVK whether it derived any receiver of the UA. This finds every
1362    // UA that any account in the wallet could have produced, whether or not it was
1363    // previously exposed.
1364    let mut found_acc_id: Option<AccountUuid> = None;
1365    for acc_id in get_account_ids(conn).map_err(|e| E::Backend(e.into()))? {
1366        let Some(account) = get_account(conn, params, acc_id).map_err(E::Backend)? else {
1367            continue;
1368        };
1369        if !account
1370            .uivk()
1371            .decrypt_diversifiers(unified_address)
1372            .is_empty()
1373        {
1374            match found_acc_id {
1375                None => found_acc_id = Some(acc_id),
1376                Some(prev) if prev == acc_id => {}
1377                Some(_) => return Err(E::UnifiedAddressConflict),
1378            }
1379        }
1380    }
1381
1382    Ok(found_acc_id)
1383}
1384
1385pub(crate) fn get_last_generated_address_matching<P: consensus::Parameters>(
1386    conn: &rusqlite::Connection,
1387    params: &P,
1388    account_uuid: AccountUuid,
1389    address_filter: UnifiedAddressRequest,
1390) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, SqliteClientError> {
1391    let account: Account =
1392        get_account(conn, params, account_uuid)?.ok_or(SqliteClientError::AccountUnknown)?;
1393
1394    let requirements = account
1395        .uivk()
1396        .receiver_requirements(address_filter)
1397        .map_err(|_| {
1398            SqliteClientError::BadAccountData(
1399                "Could not generate UnifiedAddressRequest for UIVK".to_string(),
1400            )
1401        })?;
1402    let require_flags = ReceiverFlags::required(requirements);
1403    let omit_flags = ReceiverFlags::omitted(requirements);
1404    // This returns the most recently exposed external-scope address (the address that was exposed
1405    // at the greatest block height, using the largest diversifier index to break ties)
1406    // that conforms to the specified requirements.
1407    let addr: Option<(String, Option<Vec<u8>>)> = conn
1408        .query_row(
1409            "SELECT address, diversifier_index_be
1410             FROM addresses
1411             WHERE account_id = :account_id
1412             AND key_scope = :key_scope
1413             AND (receiver_flags & :require_flags) = :require_flags
1414             AND (receiver_flags & :omit_flags) = 0
1415             AND exposed_at_height IS NOT NULL
1416             ORDER BY exposed_at_height DESC, diversifier_index_be DESC
1417             LIMIT 1",
1418            named_params![
1419                ":account_id": account.internal_id().0,
1420                ":key_scope": KeyScope::EXTERNAL.encode(),
1421                ":require_flags": require_flags.bits(),
1422                ":omit_flags": omit_flags.bits(),
1423            ],
1424            |row| Ok((row.get(0)?, row.get(1)?)),
1425        )
1426        .optional()?;
1427
1428    addr.map(|(addr_str, di_vec)| {
1429        let diversifier_index = decode_diversifier_index_be(di_vec)?.ok_or_else(|| {
1430            SqliteClientError::CorruptedData(
1431                "Addresses in EXTERNAL scope must be HD-derived".to_owned(),
1432            )
1433        })?;
1434        Address::decode(params, &addr_str)
1435            .ok_or_else(|| {
1436                SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned())
1437            })
1438            .and_then(|addr| match addr {
1439                Address::Unified(ua) => Ok(ua),
1440                _ => Err(SqliteClientError::CorruptedData(format!(
1441                    "Addresses table contains {addr_str} which is not a unified address",
1442                ))),
1443            })
1444            .map(|addr| (addr, diversifier_index))
1445    })
1446    .transpose()
1447}
1448
1449/// Adds the given external address and diversifier index to the addresses table.
1450///
1451/// Returns the primary key identifier for the newly-inserted address.
1452///
1453/// ## Parameters
1454/// - `account_id`: The account that the address was generated for.
1455/// - `diversifier_index`: The diversifier index used to generate the address.
1456/// - `address`: The unified address itself.
1457/// - `exposed_at_height`: The block height at the earliest time that the address may have been
1458///   exposed to a user, assuming a single generator of addresses.
1459/// - `force_update_address`: If this argument is set to `true`, an address has already been
1460///   inserted for the given account and diversifier index, and the `exposed_at_height` column
1461///   is currently `NULL` (i.e. the address at this diversifier index has not yet been exposed)
1462///   then the value of the `address` column will be replaced with the provided address.
1463pub(crate) fn upsert_address<P: consensus::Parameters>(
1464    conn: &rusqlite::Connection,
1465    params: &P,
1466    account_id: AccountRef,
1467    diversifier_index: DiversifierIndex,
1468    address: &UnifiedAddress,
1469    exposed_at_height: Option<BlockHeight>,
1470    force_update_address: bool,
1471) -> Result<AddressRef, SqliteClientError> {
1472    // the diversifier index is stored in big-endian order to allow sorting
1473    let di_be = encode_diversifier_index_be(diversifier_index);
1474
1475    // If a force-update was requested, check whether an address has previously been exposed for
1476    // this diversifier index. If so, and if that address differs from the given address, return an
1477    // error.
1478    if force_update_address {
1479        let previously_exposed_as = conn
1480            .query_row(
1481                "SELECT address, exposed_at_height
1482                 FROM addresses
1483                 WHERE account_id = :account_id
1484                 AND diversifier_index_be = :diversifier_index_be
1485                 AND key_scope = :key_scope",
1486                named_params![
1487                    ":account_id": account_id.0,
1488                    ":diversifier_index_be": di_be,
1489                    ":key_scope": KeyScope::EXTERNAL.encode(),
1490                ],
1491                |row| {
1492                    let address = row.get::<_, String>("address")?;
1493                    let exposed_at = row.get::<_, Option<u32>>("exposed_at_height")?;
1494                    Ok(exposed_at.map(|_| address))
1495                },
1496            )
1497            .optional()?
1498            .flatten()
1499            .map(|addr_str| UnifiedAddress::decode(params, &addr_str))
1500            .transpose()
1501            .map_err(SqliteClientError::CorruptedData)?;
1502
1503        match previously_exposed_as {
1504            Some(addr) if &addr != address => {
1505                return Err(SqliteClientError::DiversifierIndexReuse(
1506                    diversifier_index,
1507                    Box::new(addr),
1508                ));
1509            }
1510            _ => (),
1511        }
1512    }
1513
1514    let mut stmt = conn.prepare_cached(
1515        "INSERT INTO addresses (
1516            account_id,
1517            diversifier_index_be,
1518            key_scope,
1519            address,
1520            transparent_child_index,
1521            cached_transparent_receiver_address,
1522            exposed_at_height,
1523            receiver_flags
1524        )
1525        VALUES (
1526            :account_id,
1527            :diversifier_index_be,
1528            :key_scope,
1529            :address,
1530            :transparent_child_index,
1531            :cached_transparent_receiver_address,
1532            :exposed_at_height,
1533            :receiver_flags
1534        )
1535        ON CONFLICT (account_id, diversifier_index_be, key_scope) DO UPDATE
1536        SET exposed_at_height = COALESCE(
1537                MIN(exposed_at_height, :exposed_at_height),
1538                exposed_at_height,
1539                :exposed_at_height
1540            ),
1541            address = IIF(
1542                exposed_at_height IS NULL AND :force_update_address,
1543                :address,
1544                address
1545            ),
1546            receiver_flags = IIF(
1547                exposed_at_height IS NULL AND :force_update_address,
1548                :receiver_flags,
1549                receiver_flags
1550            )
1551        RETURNING id",
1552    )?;
1553
1554    #[cfg(feature = "transparent-inputs")]
1555    let (transparent_child_index, cached_taddr) = {
1556        let idx = NonHardenedChildIndex::try_from(diversifier_index)
1557            .ok()
1558            .map(|i| i.index());
1559
1560        // This upholds the `transparent_index_consistency` check on the `addresses` table.
1561        match (idx, address.transparent()) {
1562            (Some(idx), Some(r)) => Ok((Some(idx), Some(r.encode(params)))),
1563            (_, None) => Ok((None, None)),
1564            (None, Some(addr)) => Err(SqliteClientError::AddressNotRecognized(*addr)),
1565        }
1566    }?;
1567
1568    #[cfg(not(feature = "transparent-inputs"))]
1569    let (transparent_child_index, cached_taddr): (Option<u32>, Option<String>) = (None, None);
1570
1571    stmt.query_row(
1572        named_params![
1573            ":account_id": account_id.0,
1574            // the diversifier index is stored in big-endian order to allow sorting
1575            ":diversifier_index_be": &di_be,
1576            ":key_scope": KeyScope::EXTERNAL.encode(),
1577            ":address": &address.encode(params),
1578            ":transparent_child_index": transparent_child_index,
1579            ":cached_transparent_receiver_address": &cached_taddr,
1580            ":exposed_at_height": exposed_at_height.map(u32::from),
1581            ":force_update_address": force_update_address,
1582            ":receiver_flags": ReceiverFlags::from(address).bits()
1583        ],
1584        |row| row.get(0).map(AddressRef),
1585    )
1586    .map_err(SqliteClientError::from)
1587}
1588
1589#[cfg(feature = "transparent-inputs")]
1590pub(crate) fn involved_accounts(
1591    conn: &rusqlite::Connection,
1592    tx_refs: impl IntoIterator<Item = TxRef>,
1593) -> Result<HashSet<(AccountRef, AccountUuid, Option<TransparentKeyScope>)>, SqliteClientError> {
1594    let mut stmt = conn.prepare_cached(
1595        "SELECT account_id, accounts.uuid, key_scope
1596         FROM v_address_uses
1597         JOIN accounts ON accounts.id = v_address_uses.account_id
1598         WHERE transaction_id IN rarray(:tx_refs_ptr)",
1599    )?;
1600
1601    let tx_refs_values: Vec<Value> = tx_refs.into_iter().map(|r| Value::Integer(r.0)).collect();
1602    let tx_refs_ptr = Rc::new(tx_refs_values);
1603    let result = stmt
1604        .query_and_then(
1605            named_params! {
1606                ":tx_refs_ptr": &tx_refs_ptr
1607            },
1608            |row| {
1609                Ok::<_, SqliteClientError>((
1610                    row.get("account_id").map(AccountRef)?,
1611                    AccountUuid(row.get("uuid")?),
1612                    KeyScope::decode(row.get("key_scope")?)?.as_transparent(),
1613                ))
1614            },
1615        )?
1616        .collect::<Result<HashSet<_>, _>>()?;
1617
1618    Ok(result)
1619}
1620
1621/// Returns the [`UnifiedFullViewingKey`]s for the wallet.
1622pub(crate) fn get_unified_full_viewing_keys<P: consensus::Parameters>(
1623    conn: &rusqlite::Connection,
1624    params: &P,
1625) -> Result<HashMap<AccountUuid, UnifiedFullViewingKey>, SqliteClientError> {
1626    // Fetch the UnifiedFullViewingKeys we are tracking
1627    let mut stmt_fetch_accounts = conn.prepare("SELECT uuid, ufvk FROM accounts")?;
1628
1629    let rows = stmt_fetch_accounts.query_map([], |row| {
1630        let ufvk_str: Option<String> = row.get(1)?;
1631        if let Some(ufvk_str) = ufvk_str {
1632            let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str)
1633                .map_err(SqliteClientError::CorruptedData);
1634            Ok(Some((AccountUuid(row.get(0)?), ufvk)))
1635        } else {
1636            Ok(None)
1637        }
1638    })?;
1639
1640    let mut res: HashMap<AccountUuid, UnifiedFullViewingKey> = HashMap::new();
1641    for row in rows {
1642        if let Some((account_id, ufvkr)) = row? {
1643            res.insert(account_id, ufvkr?);
1644        }
1645    }
1646
1647    Ok(res)
1648}
1649
1650fn parse_account_row<P: consensus::Parameters>(
1651    row: &rusqlite::Row<'_>,
1652    params: &P,
1653) -> Result<Account, SqliteClientError> {
1654    let account_id = AccountRef(row.get("id")?);
1655    let account_name = row.get("name")?;
1656    let account_uuid = AccountUuid(row.get("uuid")?);
1657    let kind = parse_account_source(
1658        row.get("account_kind")?,
1659        row.get("hd_seed_fingerprint")?,
1660        row.get("hd_account_index")?,
1661        #[cfg(feature = "zcashd-compat")]
1662        row.get("zcashd_legacy_address_index")?,
1663        row.get("has_spend_key")?,
1664        row.get("key_source")?,
1665    )?;
1666
1667    let ufvk_str: Option<String> = row.get("ufvk")?;
1668    let viewing_key = if let Some(ufvk_str) = ufvk_str {
1669        ViewingKey::Full(Box::new(
1670            UnifiedFullViewingKey::decode(params, &ufvk_str).map_err(|e| {
1671                SqliteClientError::CorruptedData(format!(
1672                    "Could not decode unified full viewing key for account {}: {}",
1673                    account_uuid.0, e
1674                ))
1675            })?,
1676        ))
1677    } else {
1678        let uivk_str: String = row.get("uivk")?;
1679        ViewingKey::Incoming(Box::new(
1680            UnifiedIncomingViewingKey::decode(params, &uivk_str).map_err(|e| {
1681                SqliteClientError::CorruptedData(format!(
1682                    "Could not decode unified incoming viewing key for account {}: {}",
1683                    account_uuid.0, e
1684                ))
1685            })?,
1686        ))
1687    };
1688
1689    let birthday = BlockHeight::from(row.get::<_, u32>("birthday_height")?);
1690
1691    Ok(Account {
1692        id: account_id,
1693        name: account_name,
1694        uuid: account_uuid,
1695        kind,
1696        viewing_key,
1697        birthday,
1698    })
1699}
1700
1701pub(crate) fn get_account<P: Parameters>(
1702    conn: &rusqlite::Connection,
1703    params: &P,
1704    account_uuid: AccountUuid,
1705) -> Result<Option<Account>, SqliteClientError> {
1706    let mut stmt = conn.prepare_cached(
1707        r#"
1708        SELECT id, name, uuid, account_kind,
1709               hd_seed_fingerprint, hd_account_index, zcashd_legacy_address_index, key_source,
1710               ufvk, uivk, has_spend_key, birthday_height
1711        FROM accounts
1712        WHERE uuid = :account_uuid
1713        "#,
1714    )?;
1715
1716    let mut rows = stmt.query_and_then::<_, SqliteClientError, _, _>(
1717        named_params![":account_uuid": account_uuid.0],
1718        |row| parse_account_row(row, params),
1719    )?;
1720
1721    rows.next().transpose()
1722}
1723
1724#[cfg(feature = "transparent-inputs")]
1725pub(crate) fn get_account_internal<P: Parameters>(
1726    conn: &rusqlite::Connection,
1727    params: &P,
1728    account_id: AccountRef,
1729) -> Result<Option<Account>, SqliteClientError> {
1730    let mut stmt = conn.prepare_cached(
1731        r#"
1732        SELECT id, name, uuid, account_kind,
1733               hd_seed_fingerprint, hd_account_index, zcashd_legacy_address_index, key_source,
1734               ufvk, uivk, has_spend_key, birthday_height
1735        FROM accounts
1736        WHERE id = :account_id
1737        "#,
1738    )?;
1739
1740    let mut rows = stmt.query_and_then::<_, SqliteClientError, _, _>(
1741        named_params![":account_id": account_id.0],
1742        |row| parse_account_row(row, params),
1743    )?;
1744
1745    rows.next().transpose()
1746}
1747
1748/// Returns the account id corresponding to a given [`UnifiedFullViewingKey`],
1749/// if any.
1750pub(crate) fn get_account_for_ufvk<P: consensus::Parameters>(
1751    conn: &rusqlite::Connection,
1752    params: &P,
1753    ufvk: &UnifiedFullViewingKey,
1754) -> Result<Option<Account>, SqliteClientError> {
1755    let uivk = ufvk.to_unified_incoming_viewing_key();
1756    get_account_for_uivk(conn, params, &uivk)
1757}
1758
1759/// Returns the account corresponding to a given [`UnifiedIncomingViewingKey`],
1760/// if any IVK component matches an existing account.
1761pub(crate) fn get_account_for_uivk<P: consensus::Parameters>(
1762    conn: &rusqlite::Connection,
1763    params: &P,
1764    uivk: &UnifiedIncomingViewingKey,
1765) -> Result<Option<Account>, SqliteClientError> {
1766    let ivk_cache = IvkItemCache::from_uivk(uivk);
1767
1768    let mut stmt = conn.prepare(
1769        "SELECT id, name, uuid, account_kind,
1770                hd_seed_fingerprint, hd_account_index, zcashd_legacy_address_index, key_source,
1771                ufvk, uivk, has_spend_key, birthday_height
1772         FROM accounts
1773         WHERE orchard_ivk_item_cache = :orchard_ivk_item_cache
1774            OR sapling_ivk_item_cache = :sapling_ivk_item_cache
1775            OR p2pkh_ivk_item_cache = :p2pkh_ivk_item_cache",
1776    )?;
1777
1778    let accounts = stmt
1779        .query_and_then::<_, SqliteClientError, _, _>(
1780            named_params![
1781                ":orchard_ivk_item_cache": ivk_cache.orchard,
1782                ":sapling_ivk_item_cache": ivk_cache.sapling,
1783                ":p2pkh_ivk_item_cache": ivk_cache.p2pkh,
1784            ],
1785            |row| parse_account_row(row, params),
1786        )?
1787        .collect::<Result<Vec<_>, _>>()?;
1788
1789    if accounts.len() > 1 {
1790        Err(SqliteClientError::CorruptedData(
1791            "Multiple account records matched the provided UIVK".to_owned(),
1792        ))
1793    } else {
1794        Ok(accounts.into_iter().next())
1795    }
1796}
1797
1798/// Upgrades an existing account to store a full viewing key, updating the UIVK
1799/// and IVK cache columns to reflect any newly-added items.
1800///
1801/// Returns [`SqliteClientError::AccountCollision`] if the new UFVK does not
1802/// strictly add capability over the existing account's key material.
1803fn upgrade_account_ufvk<P: consensus::Parameters>(
1804    conn: &rusqlite::Connection,
1805    params: &P,
1806    existing_account: &Account,
1807    ufvk: &UnifiedFullViewingKey,
1808) -> Result<Account, SqliteClientError> {
1809    let existing_uivk = existing_account.uivk();
1810
1811    // The new FVK must subsume the existing account's IVK items.
1812    if !ufvk.subsumes_uivk(&existing_uivk) {
1813        return Err(SqliteClientError::AccountCollision(existing_account.id()));
1814    }
1815
1816    // If the existing account already has a UFVK that subsumes the new one,
1817    // this is a duplicate import (no new capability).
1818    if existing_account
1819        .ufvk()
1820        .is_some_and(|efvk| efvk.subsumes_ufvk(ufvk))
1821    {
1822        return Err(SqliteClientError::AccountCollision(existing_account.id()));
1823    }
1824
1825    let account_id = existing_account.internal_id();
1826    let ufvk_encoded = ufvk.encode(params);
1827    let uivk = ufvk.to_unified_incoming_viewing_key();
1828    let uivk_encoded = uivk.encode(params);
1829    let ivk_cache = IvkItemCache::from_uivk(&uivk);
1830
1831    conn.execute(
1832        "UPDATE accounts
1833         SET ufvk = :ufvk,
1834             uivk = :uivk,
1835             orchard_ivk_item_cache = :orchard_ivk,
1836             sapling_ivk_item_cache = :sapling_ivk,
1837             p2pkh_ivk_item_cache = :p2pkh_ivk
1838         WHERE id = :id",
1839        named_params![
1840            ":ufvk": ufvk_encoded,
1841            ":uivk": uivk_encoded,
1842            ":orchard_ivk": ivk_cache.orchard,
1843            ":sapling_ivk": ivk_cache.sapling,
1844            ":p2pkh_ivk": ivk_cache.p2pkh,
1845            ":id": account_id.0,
1846        ],
1847    )?;
1848
1849    // Reload and return the updated account.
1850    let mut stmt = conn.prepare_cached(
1851        "SELECT id, name, uuid, account_kind,
1852                hd_seed_fingerprint, hd_account_index, zcashd_legacy_address_index, key_source,
1853                ufvk, uivk, has_spend_key, birthday_height
1854         FROM accounts
1855         WHERE id = :account_id",
1856    )?;
1857    stmt.query_row(named_params![":account_id": account_id.0], |row| {
1858        Ok(parse_account_row(row, params))
1859    })?
1860}
1861
1862/// Upgrades an existing IVK-only account with a UIVK that adds new items,
1863/// updating the UIVK encoding and IVK cache columns.
1864///
1865/// Returns [`SqliteClientError::AccountCollision`] if the new UIVK does not
1866/// strictly add capability over the existing UIVK.
1867fn upgrade_account_uivk<P: consensus::Parameters>(
1868    conn: &rusqlite::Connection,
1869    params: &P,
1870    existing_account: &Account,
1871    uivk: &UnifiedIncomingViewingKey,
1872) -> Result<Account, SqliteClientError> {
1873    let existing_uivk = existing_account.uivk();
1874
1875    // The new UIVK must strictly add items: it must subsume the existing,
1876    // but not be identical to it.
1877    if !uivk.subsumes(&existing_uivk) || *uivk == existing_uivk {
1878        return Err(SqliteClientError::AccountCollision(existing_account.id()));
1879    }
1880
1881    let account_id = existing_account.internal_id();
1882    let uivk_encoded = uivk.encode(params);
1883
1884    let ivk_cache = IvkItemCache::from_uivk(uivk);
1885
1886    let rows_affected = conn.execute(
1887        "UPDATE accounts
1888         SET uivk = :uivk,
1889             orchard_ivk_item_cache = :orchard_ivk,
1890             sapling_ivk_item_cache = :sapling_ivk,
1891             p2pkh_ivk_item_cache = :p2pkh_ivk
1892         WHERE id = :id AND ufvk IS NULL",
1893        named_params![
1894            ":uivk": uivk_encoded,
1895            ":orchard_ivk": ivk_cache.orchard,
1896            ":sapling_ivk": ivk_cache.sapling,
1897            ":p2pkh_ivk": ivk_cache.p2pkh,
1898            ":id": account_id.0,
1899        ],
1900    )?;
1901    if rows_affected != 1 {
1902        return Err(SqliteClientError::CorruptedData(
1903            "UIVK upgrade failed: account already has a UFVK".to_owned(),
1904        ));
1905    }
1906
1907    // Reload and return the updated account.
1908    let mut stmt = conn.prepare_cached(
1909        "SELECT id, name, uuid, account_kind,
1910                hd_seed_fingerprint, hd_account_index, zcashd_legacy_address_index, key_source,
1911                ufvk, uivk, has_spend_key, birthday_height
1912         FROM accounts
1913         WHERE id = :account_id",
1914    )?;
1915    stmt.query_row(named_params![":account_id": account_id.0], |row| {
1916        Ok(parse_account_row(row, params))
1917    })?
1918}
1919
1920/// Returns the account id corresponding to a given [`SeedFingerprint`]
1921/// and [`zip32::AccountId`], if any.
1922pub(crate) fn get_derived_account<P: consensus::Parameters>(
1923    conn: &rusqlite::Connection,
1924    params: &P,
1925    seed_fp: &SeedFingerprint,
1926    account_index: zip32::AccountId,
1927    #[cfg(feature = "zcashd-compat")] legacy_address_index: Option<zcashd::LegacyAddressIndex>,
1928) -> Result<Option<Account>, SqliteClientError> {
1929    let mut stmt = conn.prepare(&format!(
1930        "SELECT id, name, key_source, uuid, ufvk, birthday_height, zcashd_legacy_address_index
1931         FROM accounts
1932         WHERE hd_seed_fingerprint = :hd_seed_fingerprint
1933         AND hd_account_index = :hd_account_index
1934         AND (
1935             :zcashd_legacy_address_index = {LEGACY_ADDRESS_INDEX_NULL}
1936             OR zcashd_legacy_address_index = :zcashd_legacy_address_index
1937         )",
1938    ))?;
1939
1940    #[cfg(not(feature = "zcashd-compat"))]
1941    let legacy_address_index: i64 = LEGACY_ADDRESS_INDEX_NULL;
1942    #[cfg(feature = "zcashd-compat")]
1943    let legacy_address_index = encode_legacy_account_index(legacy_address_index);
1944
1945    let mut accounts = stmt.query_and_then::<_, SqliteClientError, _, _>(
1946        named_params![
1947            ":hd_seed_fingerprint": seed_fp.to_bytes(),
1948            ":hd_account_index": u32::from(account_index),
1949            ":zcashd_legacy_address_index": legacy_address_index
1950        ],
1951        |row| {
1952            let account_id = AccountRef(row.get("id")?);
1953            let account_name = row.get("name")?;
1954            let key_source = row.get("key_source")?;
1955            let account_uuid = AccountUuid(row.get("uuid")?);
1956            let ufvk = match row.get::<_, Option<String>>("ufvk")? {
1957                None => Err(SqliteClientError::CorruptedData(format!(
1958                    "Missing unified full viewing key for derived account {}",
1959                    account_uuid.0,
1960                ))),
1961                Some(ufvk_str) => UnifiedFullViewingKey::decode(params, &ufvk_str).map_err(|e| {
1962                    SqliteClientError::CorruptedData(format!(
1963                        "Could not decode unified full viewing key for account {}: {}",
1964                        account_uuid.0, e
1965                    ))
1966                }),
1967            }?;
1968            let birthday = BlockHeight::from(row.get::<_, u32>("birthday_height")?);
1969            #[cfg(feature = "zcashd-compat")]
1970            let legacy_idx = decode_legacy_account_index(row.get("zcashd_legacy_address_index")?)?;
1971
1972            Ok(Account {
1973                id: account_id,
1974                name: account_name,
1975                uuid: account_uuid,
1976                kind: AccountSource::Derived {
1977                    derivation: Zip32Derivation::new(
1978                        *seed_fp,
1979                        account_index,
1980                        #[cfg(feature = "zcashd-compat")]
1981                        legacy_idx,
1982                    ),
1983                    key_source,
1984                },
1985                viewing_key: ViewingKey::Full(Box::new(ufvk)),
1986                birthday,
1987            })
1988        },
1989    )?;
1990
1991    accounts.next().transpose()
1992}
1993
1994pub(crate) trait ProgressEstimator {
1995    fn sapling_scan_progress<P: consensus::Parameters>(
1996        &self,
1997        conn: &rusqlite::Connection,
1998        params: &P,
1999        birthday_height: BlockHeight,
2000        recover_until_height: Option<BlockHeight>,
2001        chain_tip_height: BlockHeight,
2002    ) -> Result<Option<Progress>, SqliteClientError>;
2003
2004    #[cfg(feature = "orchard")]
2005    fn orchard_scan_progress<P: consensus::Parameters>(
2006        &self,
2007        conn: &rusqlite::Connection,
2008        params: &P,
2009        birthday_height: BlockHeight,
2010        recover_until_height: Option<BlockHeight>,
2011        chain_tip_height: BlockHeight,
2012    ) -> Result<Option<Progress>, SqliteClientError>;
2013}
2014
2015#[derive(Debug)]
2016pub(crate) struct SubtreeProgressEstimator;
2017
2018fn estimate_tree_size<P: consensus::Parameters>(
2019    conn: &rusqlite::Connection,
2020    params: &P,
2021    shielded_protocol: ShieldedPool,
2022    pool_activation_height: BlockHeight,
2023    chain_tip_height: BlockHeight,
2024) -> Result<Option<u64>, SqliteClientError> {
2025    let TableConstants {
2026        table_prefix,
2027        shard_height,
2028        ..
2029    } = table_constants::<SqliteClientError>(shielded_protocol)?;
2030
2031    // Estimate the size of the tree by linear extrapolation from available
2032    // data closest to the chain tip.
2033    //
2034    // - If we have scanned blocks within the incomplete subtree, and we know
2035    //   the tree size for the end of the most recent scanned range, then we
2036    //   extrapolate from the start of the incomplete subtree:
2037    //
2038    //         subtree
2039    //         /     \
2040    //       /         \
2041    //     /             \
2042    //   /                 \
2043    //   |<--------->|  |
2044    //     | scanned |  tip
2045    //           last_scanned
2046    //
2047    //
2048    //             subtree
2049    //             /     \
2050    //           /         \
2051    //         /             \
2052    //       /                 \
2053    //       |<------->|    |
2054    //   |   scanned   |    tip
2055    //             last_scanned
2056    //
2057    // - If we don't have scanned blocks within the incomplete subtree, or we
2058    //   don't know the tree size, then we extrapolate from the block-width of
2059    //   the last complete subtree.
2060    //
2061    // This avoids having a sharp discontinuity in the progress percentages
2062    // shown to users, and gets more accurate the closer to the chain tip we
2063    // have scanned.
2064    //
2065    // TODO: it would be nice to be able to reliably have the size of the
2066    // commitment tree at the chain tip without having to have scanned that
2067    // block.
2068
2069    // Get the tree size at the last scanned height, if known.
2070    let last_scanned = block_max_scanned(conn, params)?.and_then(|last_scanned| {
2071        match shielded_protocol {
2072            ShieldedPool::Sapling => last_scanned.sapling_tree_size(),
2073            #[cfg(feature = "orchard")]
2074            ShieldedPool::Orchard => last_scanned.orchard_tree_size(),
2075            #[cfg(not(feature = "orchard"))]
2076            ShieldedPool::Orchard => None,
2077            #[cfg(feature = "orchard")]
2078            ShieldedPool::Ironwood => last_scanned.ironwood_tree_size(),
2079            #[cfg(not(feature = "orchard"))]
2080            ShieldedPool::Ironwood => None,
2081        }
2082        .map(|tree_size| (last_scanned.block_height(), u64::from(tree_size)))
2083    });
2084
2085    // Get the last completed subtree.
2086    let last_completed_subtree = conn
2087        .query_row(
2088            &format!(
2089                "SELECT shard_index, subtree_end_height
2090                 FROM {table_prefix}_tree_shards
2091                 WHERE subtree_end_height IS NOT NULL
2092                 ORDER BY shard_index DESC
2093                 LIMIT 1"
2094            ),
2095            [],
2096            |row| {
2097                Ok((
2098                    incrementalmerkletree::Address::from_parts(
2099                        incrementalmerkletree::Level::new(shard_height),
2100                        row.get(0)?,
2101                    ),
2102                    BlockHeight::from_u32(row.get(1)?),
2103                ))
2104            },
2105        )
2106        // `None` if we have no subtree roots yet.
2107        .optional()?;
2108
2109    let result = if let Some((last_completed_subtree, last_completed_subtree_end)) =
2110        last_completed_subtree
2111    {
2112        // If we know the tree size at the last scanned height, and that
2113        // height is within the incomplete subtree, extrapolate.
2114        let tip_tree_size = last_scanned.and_then(|(last_scanned, last_scanned_tree_size)| {
2115            (last_scanned > last_completed_subtree_end)
2116                .then(|| {
2117                    let scanned_notes = last_scanned_tree_size
2118                        .saturating_sub(u64::from(last_completed_subtree.position_range_end()));
2119                    let scanned_range = u64::from(last_scanned - last_completed_subtree_end);
2120                    let unscanned_range = u64::from(chain_tip_height - last_scanned);
2121
2122                    (scanned_notes * unscanned_range)
2123                        .checked_div(scanned_range)
2124                        .map(|extrapolated_unscanned_notes| {
2125                            last_scanned_tree_size + extrapolated_unscanned_notes
2126                        })
2127                })
2128                .flatten()
2129        });
2130
2131        if let Some(tree_size) = tip_tree_size {
2132            Some(tree_size)
2133        } else if let Some(second_to_last_completed_subtree_end) = last_completed_subtree
2134            .index()
2135            .checked_sub(1)
2136            .and_then(|subtree_index| {
2137                conn.query_row(
2138                    &format!(
2139                        "SELECT subtree_end_height
2140                         FROM {table_prefix}_tree_shards
2141                         WHERE shard_index = :shard_index"
2142                    ),
2143                    named_params! {":shard_index": subtree_index},
2144                    |row| Ok(row.get::<_, Option<_>>(0)?.map(BlockHeight::from_u32)),
2145                )
2146                .transpose()
2147            })
2148            .transpose()?
2149        {
2150            let notes_in_complete_subtrees = u64::from(last_completed_subtree.position_range_end());
2151
2152            let subtree_notes = 1 << shard_height;
2153            let subtree_range =
2154                u64::from(last_completed_subtree_end - second_to_last_completed_subtree_end);
2155            let unscanned_range = u64::from(chain_tip_height - last_completed_subtree_end);
2156
2157            (subtree_notes * unscanned_range)
2158                .checked_div(subtree_range)
2159                .map(|extrapolated_incomplete_subtree_notes| {
2160                    notes_in_complete_subtrees + extrapolated_incomplete_subtree_notes
2161                })
2162        } else {
2163            // There's only one completed subtree; its start height must
2164            // be the activation height for this shielded protocol.
2165            let subtree_notes = 1 << shard_height;
2166
2167            let subtree_range = u64::from(last_completed_subtree_end - pool_activation_height);
2168            let unscanned_range = u64::from(chain_tip_height - last_completed_subtree_end);
2169
2170            (subtree_notes * unscanned_range)
2171                .checked_div(subtree_range)
2172                .map(|extrapolated_incomplete_subtree_notes| {
2173                    subtree_notes + extrapolated_incomplete_subtree_notes
2174                })
2175        }
2176    } else {
2177        // If there are no completed subtrees, but we have scanned some blocks, we can still
2178        // interpolate based upon the tree size as of the last scanned block. Here, since we
2179        // don't have any subtree data to draw on, we will interpolate based on the number of
2180        // blocks since the pool activation height
2181        last_scanned.and_then(|(last_scanned_height, last_scanned_tree_size)| {
2182            let subtree_range = u64::from(last_scanned_height - pool_activation_height);
2183            let unscanned_range = u64::from(chain_tip_height - last_scanned_height);
2184
2185            (last_scanned_tree_size * unscanned_range)
2186                .checked_div(subtree_range)
2187                .map(|extrapolated_incomplete_subtree_notes| {
2188                    last_scanned_tree_size + extrapolated_incomplete_subtree_notes
2189                })
2190        })
2191    };
2192
2193    Ok(result)
2194}
2195
2196#[allow(clippy::too_many_arguments)]
2197fn subtree_scan_progress<P: consensus::Parameters>(
2198    conn: &rusqlite::Connection,
2199    params: &P,
2200    shielded_protocol: ShieldedPool,
2201    pool_activation_height: BlockHeight,
2202    min_birthday_height: BlockHeight,
2203    recover_until_height: Option<BlockHeight>,
2204    chain_tip_height: BlockHeight,
2205) -> Result<Option<Progress>, SqliteClientError> {
2206    let TableConstants {
2207        table_prefix,
2208        output_count_col,
2209        shard_height,
2210        ..
2211    } = table_constants::<SqliteClientError>(shielded_protocol)?;
2212
2213    // Each query against the `blocks` table that contributes to scan-progress accounting
2214    // must exclude heights that fall within a `scan_queue` range whose priority indicates
2215    // the range is pending re-scan. Without this filter, blocks whose tree state was
2216    // recorded by a previous scan but whose enclosing range was subsequently re-queued
2217    // (e.g., by `rewind_to_chain_state`) would be counted as scanned, over-reporting
2218    // progress against a tree-size denominator that no longer reflects the wallet's
2219    // actual scanned state.
2220    let scanned_priority = priority_code(&ScanPriority::Scanned);
2221    let unscanned_filter = "AND NOT EXISTS (
2222            SELECT 1 FROM scan_queue
2223            WHERE block_range_start <= blocks.height
2224              AND blocks.height < block_range_end
2225              AND priority > :scanned_priority
2226        )";
2227
2228    let mut stmt_scanned_count_until = conn.prepare_cached(&format!(
2229        "SELECT SUM({output_count_col})
2230        FROM blocks
2231        WHERE :start_height <= height AND height < :end_height
2232        {unscanned_filter}",
2233    ))?;
2234    let mut stmt_scanned_count_from = conn.prepare_cached(&format!(
2235        "SELECT SUM({output_count_col})
2236        FROM blocks
2237        WHERE :start_height <= height
2238        {unscanned_filter}",
2239    ))?;
2240    let mut stmt_start_tree_size = conn.prepare_cached(&format!(
2241        "SELECT MAX({table_prefix}_commitment_tree_size - {output_count_col})
2242        FROM blocks
2243        WHERE height <= :start_height
2244        {unscanned_filter}",
2245    ))?;
2246    let mut stmt_start_tree_size_at = conn.prepare_cached(&format!(
2247        "SELECT {table_prefix}_commitment_tree_size - {output_count_col}
2248        FROM blocks
2249        WHERE height = :start_height
2250        {unscanned_filter}",
2251    ))?;
2252
2253    // In case we didn't have information about the tree size at the birthday height,
2254    // get the tree size from a nearby subtree. It's fine for this to be approximate;
2255    // it just alters the magnitude of recovery progress a bit.
2256    let mut get_tree_size_near = |as_of: BlockHeight| {
2257        let size_from_blocks = stmt_start_tree_size
2258            .query_row(
2259                named_params![
2260                    ":start_height": u32::from(as_of),
2261                    ":scanned_priority": scanned_priority,
2262                ],
2263                |row| row.get::<_, Option<u64>>(0),
2264            )
2265            .optional()?
2266            .flatten();
2267
2268        let size_from_subtree_roots = || {
2269            conn.query_row(
2270                &format!(
2271                    "SELECT MIN(shard_index)
2272                             FROM {table_prefix}_tree_shards
2273                             WHERE subtree_end_height >= :start_height
2274                             OR subtree_end_height IS NULL",
2275                ),
2276                named_params! {
2277                    ":start_height": u32::from(as_of),
2278                },
2279                |row| {
2280                    let min_tree_size = row
2281                        .get::<_, Option<u64>>(0)?
2282                        .map(|min_idx| min_idx << shard_height);
2283                    Ok(min_tree_size)
2284                },
2285            )
2286            .optional()
2287            .map(|opt| opt.flatten())
2288        };
2289
2290        match size_from_blocks {
2291            Some(size) => Ok(Some(size)),
2292            None => size_from_subtree_roots(),
2293        }
2294    };
2295
2296    // Get the starting note commitment tree size from the wallet birthday, or failing that
2297    // from the blocks table.
2298    let birthday_size = match conn
2299        .query_row(
2300            &format!(
2301                "SELECT birthday_{table_prefix}_tree_size
2302                     FROM accounts
2303                     WHERE birthday_height = :birthday_height",
2304            ),
2305            named_params![":birthday_height": u32::from(min_birthday_height)],
2306            |row| row.get::<_, Option<u64>>(0),
2307        )
2308        .optional()?
2309        .flatten()
2310    {
2311        Some(tree_size) => Some(tree_size),
2312        // If we don't have an explicit birthday tree size, find something nearby.
2313        None => get_tree_size_near(min_birthday_height)?,
2314    };
2315
2316    // If we've scanned the block at the chain tip, we know how many notes are currently in the
2317    // tree.
2318    let tip_tree_size = match conn
2319        .query_row(
2320            &format!(
2321                "SELECT {table_prefix}_commitment_tree_size
2322                    FROM blocks
2323                    WHERE height = :height
2324                    {unscanned_filter}",
2325            ),
2326            named_params! {
2327                ":height": u32::from(chain_tip_height),
2328                ":scanned_priority": scanned_priority,
2329            },
2330            |row| row.get::<_, Option<u64>>(0),
2331        )
2332        .optional()?
2333        .flatten()
2334    {
2335        Some(tree_size) => Some(tree_size),
2336        None => estimate_tree_size(
2337            conn,
2338            params,
2339            shielded_protocol,
2340            pool_activation_height,
2341            chain_tip_height,
2342        )?,
2343    };
2344
2345    // Get the note commitment tree size as of the start of the recover-until height.
2346    // The outer option indicates whether or not we have recover-until height information;
2347    // the inner option indicates whether or not we were able to obtain a tree size given
2348    // the recover-until height.
2349    let recover_until_size: Option<Option<u64>> = recover_until_height
2350        .map(|recover_until_height| {
2351            let size_from_blocks = stmt_start_tree_size_at
2352                .query_row(
2353                    named_params![
2354                        ":start_height": u32::from(recover_until_height),
2355                        ":scanned_priority": scanned_priority,
2356                    ],
2357                    |row| row.get::<_, Option<u64>>(0),
2358                )
2359                .optional()?
2360                .flatten();
2361
2362            match size_from_blocks {
2363                // We know the tree size as of the start of the recover-until height.
2364                Some(size) => Ok::<_, SqliteClientError>(Some(size)),
2365
2366                // If the recover-until height is equal to the chain tip height,
2367                // then this is almost certainly a newly-recovered wallet, and all
2368                // progress can count as recovery progress. Approximate the size
2369                // of the tree at the start of the block as equal to the size of
2370                // the tree at the end of the block; the scan progress will show
2371                // as 0/0 which is fine.
2372                None if recover_until_height == chain_tip_height => Ok(tip_tree_size),
2373
2374                // Linearly extrapolate a tree size between the nearest two bounds
2375                // we have.
2376                // TODO: Use a closer lower bound if available.
2377                None => {
2378                    Ok(birthday_size
2379                        .zip(tip_tree_size)
2380                        .and_then(|(lower_size, upper_size)| {
2381                            let total_notes = upper_size.saturating_sub(lower_size);
2382                            let total_range = u64::from(chain_tip_height)
2383                                .saturating_sub(u64::from(min_birthday_height));
2384                            let recovery_range = u64::from(recover_until_height)
2385                                .saturating_sub(u64::from(min_birthday_height));
2386
2387                            (total_notes * recovery_range).checked_div(total_range).map(
2388                                |extrapolated_recovery_notes| {
2389                                    (lower_size + extrapolated_recovery_notes).min(upper_size)
2390                                },
2391                            )
2392                        }))
2393                }
2394            }
2395        })
2396        .transpose()?;
2397
2398    // Count the total outputs scanned so far on the birthday side of the recover-until height.
2399    let recovered_count = recover_until_height
2400        .map(|end_height| {
2401            stmt_scanned_count_until.query_row(
2402                named_params! {
2403                    ":start_height": u32::from(min_birthday_height),
2404                    ":end_height": u32::from(end_height),
2405                    ":scanned_priority": scanned_priority,
2406                },
2407                |row| row.get::<_, Option<u64>>(0),
2408            )
2409        })
2410        .transpose()?;
2411
2412    let recover = recovered_count
2413        .zip(recover_until_size)
2414        .map(|(recovered, end_size)| {
2415            birthday_size.zip(end_size).map(|(start_size, end_size)| {
2416                Ratio::new(recovered.unwrap_or(0), end_size.saturating_sub(start_size))
2417            })
2418        })
2419        // If none of the wallet's accounts have a recover-until height, then there
2420        // is no recovery phase for the wallet, and therefore the denominator in the
2421        // resulting ratio (the number of notes in the recovery range) is zero.
2422        .unwrap_or_else(|| Some(Ratio::new(0, 0)));
2423
2424    let scan = {
2425        // Count the total outputs scanned so far on the chain tip side of the
2426        // recover-until height.
2427        let scanned_count = stmt_scanned_count_from.query_row(
2428            named_params![
2429                ":start_height": u32::from(recover_until_height.unwrap_or(min_birthday_height)),
2430                ":scanned_priority": scanned_priority,
2431            ],
2432            |row| row.get::<_, Option<u64>>(0),
2433        )?;
2434
2435        recover_until_size
2436            .unwrap_or(birthday_size)
2437            .zip(tip_tree_size)
2438            .map(|(start_size, tip_tree_size)| {
2439                Ratio::new(
2440                    scanned_count.unwrap_or(0),
2441                    tip_tree_size.saturating_sub(start_size),
2442                )
2443            })
2444    };
2445
2446    Ok(scan.map(|scan| Progress::new(scan, recover)))
2447}
2448
2449impl ProgressEstimator for SubtreeProgressEstimator {
2450    #[tracing::instrument(skip(conn, params))]
2451    fn sapling_scan_progress<P: consensus::Parameters>(
2452        &self,
2453        conn: &rusqlite::Connection,
2454        params: &P,
2455        birthday_height: BlockHeight,
2456        recover_until_height: Option<BlockHeight>,
2457        chain_tip_height: BlockHeight,
2458    ) -> Result<Option<Progress>, SqliteClientError> {
2459        let sapling_activation_height = match params.activation_height(NetworkUpgrade::Sapling) {
2460            Some(h) => h,
2461            None => return Ok(None),
2462        };
2463
2464        subtree_scan_progress(
2465            conn,
2466            params,
2467            ShieldedPool::Sapling,
2468            sapling_activation_height,
2469            birthday_height,
2470            recover_until_height,
2471            chain_tip_height,
2472        )
2473    }
2474
2475    #[cfg(feature = "orchard")]
2476    #[tracing::instrument(skip(conn, params))]
2477    fn orchard_scan_progress<P: consensus::Parameters>(
2478        &self,
2479        conn: &rusqlite::Connection,
2480        params: &P,
2481        birthday_height: BlockHeight,
2482        recover_until_height: Option<BlockHeight>,
2483        chain_tip_height: BlockHeight,
2484    ) -> Result<Option<Progress>, SqliteClientError> {
2485        let nu5_activation_height = match params.activation_height(NetworkUpgrade::Nu5) {
2486            Some(h) => h,
2487            None => return Ok(None),
2488        };
2489
2490        subtree_scan_progress(
2491            conn,
2492            params,
2493            ShieldedPool::Orchard,
2494            nu5_activation_height,
2495            birthday_height,
2496            recover_until_height,
2497            chain_tip_height,
2498        )
2499    }
2500}
2501
2502fn next_subtree_index<H: HashSer, const SHARD_HEIGHT: u8>(
2503    tx: &rusqlite::Transaction,
2504    table_prefix: &'static str,
2505) -> Result<u64, SqliteClientError> {
2506    let shard_store = SqliteShardStore::<_, H, SHARD_HEIGHT>::from_connection(tx, table_prefix)?;
2507
2508    // The last shard will be incomplete, and we want the next range to overlap with
2509    // the last complete shard, so return the index of the second-to-last shard root.
2510    let roots = shard_store
2511        .get_shard_roots()
2512        .map_err(ShardTreeError::Storage)?;
2513    Ok(roots
2514        .iter()
2515        .rev()
2516        .nth(1)
2517        .map(|addr| addr.index())
2518        .unwrap_or(0))
2519}
2520
2521/// Returns the spendable balance for the account at the specified height.
2522///
2523/// This may be used to obtain a balance that ignores notes that have been detected so recently
2524/// that they are not yet spendable, or for which it is not yet possible to construct witnesses.
2525#[tracing::instrument(skip(tx, params, progress))]
2526pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
2527    tx: &rusqlite::Transaction,
2528    params: &P,
2529    confirmations_policy: ConfirmationsPolicy,
2530    progress: &impl ProgressEstimator,
2531) -> Result<Option<WalletSummary<AccountUuid>>, SqliteClientError> {
2532    let chain_tip_height = match chain_tip_height(tx)? {
2533        Some(h) => h,
2534        None => {
2535            return Ok(None);
2536        }
2537    };
2538
2539    let birthday_height = match wallet_birthday(tx)? {
2540        Some(h) => h,
2541        None => {
2542            return Ok(None);
2543        }
2544    };
2545
2546    let recover_until_height = recover_until_height(tx)?;
2547    let fully_scanned_height = block_fully_scanned(tx, params)?.map(|m| m.block_height());
2548    let target_height = TargetHeight::from(chain_tip_height + 1);
2549    let anchor_height = get_anchor_height(tx, target_height, confirmations_policy.trusted())?;
2550
2551    let sapling_progress = progress.sapling_scan_progress(
2552        tx,
2553        params,
2554        birthday_height,
2555        recover_until_height,
2556        chain_tip_height,
2557    )?;
2558
2559    #[cfg(feature = "orchard")]
2560    let orchard_progress = progress.orchard_scan_progress(
2561        tx,
2562        params,
2563        birthday_height,
2564        recover_until_height,
2565        chain_tip_height,
2566    )?;
2567    #[cfg(not(feature = "orchard"))]
2568    let orchard_progress: Option<Progress> = None;
2569
2570    // Treat Sapling and Orchard outputs as having the same cost to scan.
2571    let progress = sapling_progress
2572        .as_ref()
2573        .zip(orchard_progress.as_ref())
2574        .map(|(s, o)| {
2575            Progress::new(
2576                Ratio::new(
2577                    s.scan().numerator() + o.scan().numerator(),
2578                    s.scan().denominator() + o.scan().denominator(),
2579                ),
2580                s.recovery()
2581                    .zip(o.recovery())
2582                    .map(|(s, o)| {
2583                        Ratio::new(
2584                            s.numerator() + o.numerator(),
2585                            s.denominator() + o.denominator(),
2586                        )
2587                    })
2588                    .or_else(|| s.recovery())
2589                    .or_else(|| o.recovery()),
2590            )
2591        })
2592        .or(sapling_progress)
2593        .or(orchard_progress);
2594
2595    let progress = match progress {
2596        Some(p) => p,
2597        None => return Ok(None),
2598    };
2599
2600    let mut stmt_accounts = tx.prepare_cached("SELECT uuid FROM accounts")?;
2601    let mut account_balances = stmt_accounts
2602        .query([])?
2603        .and_then(|row| {
2604            Ok::<_, SqliteClientError>((AccountUuid(row.get::<_, Uuid>(0)?), AccountBalance::ZERO))
2605        })
2606        .collect::<Result<HashMap<AccountUuid, AccountBalance>, _>>()?;
2607
2608    fn with_pool_balances<F>(
2609        tx: &rusqlite::Transaction,
2610        target_height: TargetHeight,
2611        anchor_height: Option<BlockHeight>,
2612        confirmations_policy: ConfirmationsPolicy,
2613        account_balances: &mut HashMap<AccountUuid, AccountBalance>,
2614        protocol: ShieldedPool,
2615        with_pool_balance: F,
2616    ) -> Result<(), SqliteClientError>
2617    where
2618        F: Fn(
2619            &mut AccountBalance,
2620            Zatoshis,
2621            Zatoshis,
2622            Zatoshis,
2623            Zatoshis,
2624            Zatoshis,
2625        ) -> Result<(), SqliteClientError>,
2626    {
2627        let TableConstants { table_prefix, .. } = table_constants::<SqliteClientError>(protocol)?;
2628
2629        // If the shard containing the anchor height contains any unscanned ranges that start
2630        // below or including that height, none of our shielded balance is currently spendable.
2631        #[tracing::instrument(skip_all)]
2632        fn is_any_spendable(
2633            conn: &rusqlite::Connection,
2634            anchor_height: BlockHeight,
2635            table_prefix: &'static str,
2636        ) -> Result<bool, SqliteClientError> {
2637            conn.query_row(
2638                &format!(
2639                    "SELECT NOT EXISTS(
2640                         SELECT 1 FROM v_{table_prefix}_shard_unscanned_ranges
2641                         WHERE :anchor_height
2642                            BETWEEN subtree_start_height
2643                            AND IFNULL(subtree_end_height, :anchor_height)
2644                         AND block_range_start <= :anchor_height
2645                     )"
2646                ),
2647                named_params![":anchor_height": u32::from(anchor_height)],
2648                |row| row.get::<_, bool>(0),
2649            )
2650            .map_err(|e| e.into())
2651        }
2652
2653        let trusted_height =
2654            target_height.saturating_sub(u32::from(confirmations_policy.trusted()));
2655
2656        let any_spendable =
2657            anchor_height.map_or(Ok(false), |h| is_any_spendable(tx, h, table_prefix))?;
2658
2659        let mut stmt_select_notes = tx.prepare_cached(&format!(
2660            "SELECT accounts.uuid, rn.id, rn.value, rn.is_change, rn.recipient_key_scope,
2661                    scan_state.max_priority,
2662                    rn.witness_stabilized,
2663                    t.mined_height,
2664                    IFNULL(t.trust_status, 0) AS trust_status,
2665                    MAX(tt.mined_height) AS max_shielding_input_height,
2666                    MIN(IFNULL(tt.trust_status, 0)) AS min_shielding_input_trust,
2667                    rn.lock_expiry_height
2668             FROM {table_prefix}_received_notes rn
2669             INNER JOIN accounts ON accounts.id = rn.account_id
2670             INNER JOIN transactions t ON t.id_tx = rn.transaction_id
2671             LEFT OUTER JOIN v_{table_prefix}_shards_scan_state scan_state
2672                ON rn.commitment_tree_position >= scan_state.start_position
2673                AND rn.commitment_tree_position < scan_state.end_position_exclusive
2674             LEFT OUTER JOIN transparent_received_output_spends ros
2675                ON ros.transaction_id = t.id_tx
2676             LEFT OUTER JOIN transparent_received_outputs tro
2677                ON tro.id = ros.transparent_received_output_id
2678                AND tro.account_id = accounts.id
2679             LEFT OUTER JOIN transactions tt
2680                ON tt.id_tx = tro.transaction_id
2681             WHERE ({}) -- the transaction is unexpired
2682             AND rn.id NOT IN ({}) -- and the received note is unspent
2683             GROUP BY rn.id",
2684            common::tx_unexpired_condition("t"),
2685            common::spent_notes_clause(table_prefix),
2686        ))?;
2687
2688        let mut rows = stmt_select_notes.query(named_params![
2689            ":target_height": u32::from(target_height),
2690        ])?;
2691        while let Some(row) = rows.next()? {
2692            let account = AccountUuid(row.get::<_, Uuid>("uuid")?);
2693
2694            let value_raw = row.get::<_, i64>("value")?;
2695            let value = Zatoshis::from_nonnegative_i64(value_raw).map_err(|_| {
2696                SqliteClientError::CorruptedData(format!(
2697                    "Negative received note value: {value_raw}"
2698                ))
2699            })?;
2700
2701            let is_change = row.get::<_, bool>("is_change")?;
2702
2703            let recipient_key_scope = row
2704                .get::<_, Option<i64>>("recipient_key_scope")?
2705                .map(KeyScope::decode)
2706                .transpose()?;
2707
2708            // If `max_priority` is null, this means that the note is not positioned; the note
2709            // will not be spendable, so we assign the scan priority to `ChainTip` as a priority
2710            // that is greater than `Scanned`
2711            let max_priority_raw = row.get::<_, Option<i64>>("max_priority")?;
2712            let max_priority = max_priority_raw.map_or_else(
2713                || Ok(ScanPriority::ChainTip),
2714                |raw| {
2715                    parse_priority_code(raw).ok_or_else(|| {
2716                        SqliteClientError::CorruptedData(format!(
2717                            "Priority code {raw} not recognized."
2718                        ))
2719                    })
2720                },
2721            )?;
2722
2723            let received_height = row
2724                .get::<_, Option<u32>>("mined_height")?
2725                .map(BlockHeight::from);
2726
2727            let tx_trusted = row.get::<_, bool>("trust_status")?;
2728
2729            let max_shielding_input_height = row
2730                .get::<_, Option<u32>>("max_shielding_input_height")?
2731                .map(BlockHeight::from);
2732
2733            let tx_shielding_inputs_trusted = row.get::<_, bool>("min_shielding_input_trust")?;
2734
2735            let witness_stabilized = row.get::<_, bool>("witness_stabilized")?;
2736
2737            let is_locked = locking::is_locked_at(
2738                row.get::<_, Option<u32>>("lock_expiry_height")?,
2739                target_height,
2740            );
2741
2742            // A stabilized note is unconditionally spendable. Its originating transaction has been
2743            // confirmed well beyond any reasonable confirmation policy, and its witness data
2744            // cannot be removed by truncation.
2745            //
2746            // Non-stabilized notes require more checks: we must have enough chain tip information
2747            // to construct witnesses, the shard that the note resides in must be sufficiently
2748            // scanned that we can construct the witness for the note, and the note has enough
2749            // confirmations to be spent.
2750            let is_spendable = witness_stabilized
2751                || (any_spendable
2752                    && max_priority <= ScanPriority::Scanned
2753                    && confirmations_policy.confirmations_until_spendable(
2754                        target_height,
2755                        PoolType::Shielded(protocol),
2756                        recipient_key_scope.and_then(|k| zip32::Scope::try_from(k).ok()),
2757                        received_height,
2758                        tx_trusted,
2759                        max_shielding_input_height,
2760                        tx_shielding_inputs_trusted,
2761                    ) == 0);
2762
2763            let is_pending_change =
2764                is_change && received_height.iter().all(|h| h > &trusted_height);
2765
2766            let (
2767                spendable_value,
2768                locked_value,
2769                change_pending_confirmation,
2770                value_pending_spendability,
2771                uneconomic_value,
2772            ) = {
2773                let zero = Zatoshis::ZERO;
2774                if value <= zip317::MARGINAL_FEE {
2775                    (zero, zero, zero, zero, value)
2776                } else if is_spendable && is_locked {
2777                    // Only notes that would otherwise be spendable are counted as locked; a
2778                    // locked note that is still pending confirmations is deliberately reported
2779                    // in the pending buckets below, since locking only matters once the note
2780                    // would enter selection. This mirrors the transparent balance computation.
2781                    (zero, value, zero, zero, zero)
2782                } else if is_spendable {
2783                    (value, zero, zero, zero, zero)
2784                } else if is_pending_change {
2785                    (zero, zero, value, zero, zero)
2786                } else {
2787                    (zero, zero, zero, value, zero)
2788                }
2789            };
2790
2791            if let Some(balances) = account_balances.get_mut(&account) {
2792                with_pool_balance(
2793                    balances,
2794                    spendable_value,
2795                    locked_value,
2796                    change_pending_confirmation,
2797                    value_pending_spendability,
2798                    uneconomic_value,
2799                )?;
2800            }
2801        }
2802        Ok(())
2803    }
2804
2805    #[cfg(feature = "orchard")]
2806    {
2807        let orchard_trace = tracing::info_span!("orchard_balances").entered();
2808        with_pool_balances(
2809            tx,
2810            target_height,
2811            anchor_height,
2812            confirmations_policy,
2813            &mut account_balances,
2814            ShieldedPool::Orchard,
2815            |balances,
2816             spendable_value,
2817             locked_value,
2818             change_pending_confirmation,
2819             value_pending_spendability,
2820             uneconomic_value| {
2821                balances.with_orchard_balance_mut::<_, SqliteClientError>(|bal| {
2822                    bal.add_spendable_value(spendable_value)?;
2823                    bal.add_locked_value(locked_value)?;
2824                    bal.add_pending_change_value(change_pending_confirmation)?;
2825                    bal.add_pending_spendable_value(value_pending_spendability)?;
2826                    bal.add_uneconomic_value(uneconomic_value)?;
2827                    Ok(())
2828                })
2829            },
2830        )?;
2831        drop(orchard_trace);
2832    }
2833
2834    #[cfg(feature = "orchard")]
2835    {
2836        let ironwood_trace = tracing::info_span!("ironwood_balances").entered();
2837        with_pool_balances(
2838            tx,
2839            target_height,
2840            anchor_height,
2841            confirmations_policy,
2842            &mut account_balances,
2843            ShieldedPool::Ironwood,
2844            |balances,
2845             spendable_value,
2846             locked_value,
2847             change_pending_confirmation,
2848             value_pending_spendability,
2849             uneconomic_value| {
2850                balances.with_ironwood_balance_mut::<_, SqliteClientError>(|bal| {
2851                    bal.add_spendable_value(spendable_value)?;
2852                    bal.add_locked_value(locked_value)?;
2853                    bal.add_pending_change_value(change_pending_confirmation)?;
2854                    bal.add_pending_spendable_value(value_pending_spendability)?;
2855                    bal.add_uneconomic_value(uneconomic_value)?;
2856                    Ok(())
2857                })
2858            },
2859        )?;
2860        drop(ironwood_trace);
2861    }
2862
2863    let sapling_trace = tracing::info_span!("sapling_balances").entered();
2864    with_pool_balances(
2865        tx,
2866        target_height,
2867        anchor_height,
2868        confirmations_policy,
2869        &mut account_balances,
2870        ShieldedPool::Sapling,
2871        |balances,
2872         spendable_value,
2873         locked_value,
2874         change_pending_confirmation,
2875         value_pending_spendability,
2876         uneconomic_value| {
2877            balances.with_sapling_balance_mut::<_, SqliteClientError>(|bal| {
2878                bal.add_spendable_value(spendable_value)?;
2879                bal.add_locked_value(locked_value)?;
2880                bal.add_pending_change_value(change_pending_confirmation)?;
2881                bal.add_pending_spendable_value(value_pending_spendability)?;
2882                bal.add_uneconomic_value(uneconomic_value)?;
2883                Ok(())
2884            })
2885        },
2886    )?;
2887    drop(sapling_trace);
2888
2889    #[cfg(feature = "transparent-inputs")]
2890    transparent::add_transparent_account_balances(
2891        tx,
2892        target_height,
2893        confirmations_policy,
2894        &mut account_balances,
2895    )?;
2896
2897    // The approach used here for shielded subtree indexing was a quick hack
2898    // that has not yet been replaced. TODO: Make less hacky.
2899    // https://github.com/zcash/librustzcash/issues/1249
2900    let next_sapling_subtree_index = next_subtree_index::<::sapling::Node, SAPLING_SHARD_HEIGHT>(
2901        tx,
2902        crate::SAPLING_TABLES_PREFIX,
2903    )?;
2904
2905    #[cfg(feature = "orchard")]
2906    let next_orchard_subtree_index = next_subtree_index::<
2907        ::orchard::tree::MerkleHashOrchard,
2908        ORCHARD_SHARD_HEIGHT,
2909    >(tx, crate::ORCHARD_TABLES_PREFIX)?;
2910
2911    #[cfg(feature = "orchard")]
2912    let next_ironwood_subtree_index = next_subtree_index::<
2913        ::orchard::tree::MerkleHashOrchard,
2914        ORCHARD_SHARD_HEIGHT,
2915    >(tx, crate::IRONWOOD_TABLES_PREFIX)?;
2916
2917    let summary = WalletSummary::new(
2918        account_balances,
2919        chain_tip_height,
2920        fully_scanned_height.unwrap_or(birthday_height - 1),
2921        progress,
2922        next_sapling_subtree_index,
2923        #[cfg(feature = "orchard")]
2924        next_orchard_subtree_index,
2925        #[cfg(feature = "orchard")]
2926        next_ironwood_subtree_index,
2927    );
2928
2929    Ok(Some(summary))
2930}
2931
2932/// Returns the memo for a received note, if the note is known to the wallet.
2933pub(crate) fn get_received_memo(
2934    conn: &rusqlite::Connection,
2935    note_id: NoteId,
2936) -> Result<Option<Memo>, SqliteClientError> {
2937    let TableConstants {
2938        table_prefix,
2939        output_index_col,
2940        ..
2941    } = table_constants::<SqliteClientError>(note_id.protocol())?;
2942
2943    let memo_bytes = conn
2944        .query_row(
2945            &format!(
2946                "SELECT memo FROM {table_prefix}_received_notes
2947                JOIN transactions ON transactions.id_tx = {table_prefix}_received_notes.transaction_id
2948                WHERE transactions.txid = :txid
2949                AND {table_prefix}_received_notes.{output_index_col} = :output_index"
2950            ),
2951            named_params![
2952                ":txid": note_id.txid().as_ref(),
2953                ":output_index": note_id.output_index()
2954            ],
2955            |row| row.get::<_, Option<Vec<u8>>>(0),
2956        )
2957        .optional()?
2958        .flatten();
2959
2960    let memo = memo_bytes
2961        .map(|b| MemoBytes::from_bytes(&b).and_then(Memo::try_from))
2962        .transpose()?;
2963
2964    Ok(memo)
2965}
2966
2967fn parse_tx<P: consensus::Parameters>(
2968    params: &P,
2969    tx_bytes: &[u8],
2970    block_height: Option<BlockHeight>,
2971    expiry_height: Option<BlockHeight>,
2972) -> Result<(BlockHeight, Transaction), SqliteClientError> {
2973    // We need to provide a consensus branch ID so that pre-v5 `Transaction` structs
2974    // (which don't commit directly to one) can store it internally.
2975    // - If the transaction is mined, we use the block height to get the correct one.
2976    // - If the transaction is unmined and has a cached non-zero expiry height, we use
2977    //   that (relying on the invariant that a transaction can't be mined across a network
2978    //   upgrade boundary, so the expiry height must be in the same epoch).
2979    // - Otherwise, we use a placeholder for the initial transaction parse (as the
2980    //   consensus branch ID is not used there), and then either use its non-zero expiry
2981    //   height or return an error.
2982    if let Some(height) =
2983        block_height.or_else(|| expiry_height.filter(|h| h > &BlockHeight::from(0)))
2984    {
2985        Transaction::read(tx_bytes, BranchId::for_height(params, height))
2986            .map(|t| (height, t))
2987            .map_err(SqliteClientError::from)
2988    } else {
2989        let tx_data = Transaction::read(tx_bytes, BranchId::Sprout)
2990            .map_err(SqliteClientError::from)?
2991            .into_data();
2992
2993        let expiry_height = tx_data.expiry_height();
2994        if expiry_height > BlockHeight::from(0) {
2995            TransactionData::from_parts(
2996                tx_data.version(),
2997                BranchId::for_height(params, expiry_height),
2998                tx_data.lock_time(),
2999                expiry_height,
3000                #[cfg(all(zcash_unstable = "nu7", feature = "zip-233"))]
3001                tx_data.zip233_amount(),
3002                tx_data.transparent_bundle().cloned(),
3003                tx_data.sprout_bundle().cloned(),
3004                tx_data.sapling_bundle().cloned(),
3005                tx_data.orchard_bundle().cloned(),
3006            )
3007            .freeze()
3008            .map(|t| (expiry_height, t))
3009            .map_err(SqliteClientError::from)
3010        } else {
3011            Err(SqliteClientError::CorruptedData(
3012                "Consensus branch ID not known, cannot parse this transaction until it is mined"
3013                    .to_string(),
3014            ))
3015        }
3016    }
3017}
3018
3019/// Looks up a transaction by its [`TxId`].
3020///
3021/// Returns the decoded transaction, along with the block height that was used in its decoding.
3022/// This is either the block height at which the transaction was mined, or the expiry height if the
3023/// wallet created the transaction but the transaction has not yet been mined from the perspective
3024/// of the wallet.
3025pub(crate) fn get_transaction<P: Parameters>(
3026    conn: &rusqlite::Connection,
3027    params: &P,
3028    txid: TxId,
3029) -> Result<Option<(BlockHeight, Transaction)>, SqliteClientError> {
3030    conn.query_row(
3031        "SELECT raw, mined_height, expiry_height FROM transactions
3032        WHERE txid = ?",
3033        [txid.as_ref()],
3034        |row| {
3035            let h: Option<u32> = row.get(1)?;
3036            let expiry: Option<u32> = row.get(2)?;
3037            Ok((
3038                row.get::<_, Option<Vec<u8>>>(0)?,
3039                h.map(BlockHeight::from),
3040                expiry.map(BlockHeight::from),
3041            ))
3042        },
3043    )
3044    .optional()?
3045    .and_then(|(t_opt, b, e)| t_opt.as_ref().map(|t| parse_tx(params, t, b, e)))
3046    .transpose()
3047}
3048
3049/// Returns the memo for a sent note, if the sent note is known to the wallet.
3050pub(crate) fn get_sent_memo(
3051    conn: &rusqlite::Connection,
3052    note_id: NoteId,
3053) -> Result<Option<Memo>, SqliteClientError> {
3054    let memo_bytes: Option<Vec<_>> = conn
3055        .query_row(
3056            "SELECT memo FROM sent_notes
3057            JOIN transactions ON transactions.id_tx = sent_notes.transaction_id
3058            WHERE transactions.txid = :txid
3059            AND sent_notes.output_pool = :pool_code
3060            AND sent_notes.output_index = :output_index",
3061            named_params![
3062                ":txid": note_id.txid().as_ref(),
3063                ":pool_code": pool_code(PoolType::Shielded(note_id.protocol())),
3064                ":output_index": note_id.output_index()
3065            ],
3066            |row| row.get(0),
3067        )
3068        .optional()?
3069        .flatten();
3070
3071    memo_bytes
3072        .map(|b| {
3073            MemoBytes::from_bytes(&b)
3074                .and_then(Memo::try_from)
3075                .map_err(SqliteClientError::from)
3076        })
3077        .transpose()
3078}
3079
3080/// Returns the minimum birthday height for accounts in the wallet.
3081//
3082// TODO ORCHARD: we should consider whether we want to permit protocol-restricted accounts; if so,
3083// we would then want this method to take a protocol identifier to be able to learn the wallet's
3084// "Orchard birthday" which might be different from the overall wallet birthday.
3085pub(crate) fn wallet_birthday(
3086    conn: &rusqlite::Connection,
3087) -> Result<Option<BlockHeight>, rusqlite::Error> {
3088    conn.query_row(
3089        "SELECT MIN(birthday_height) AS wallet_birthday FROM accounts",
3090        [],
3091        |row| {
3092            row.get::<_, Option<u32>>(0)
3093                .map(|opt| opt.map(BlockHeight::from))
3094        },
3095    )
3096}
3097
3098/// Returns the maximum `recover_until` height for accounts in the wallet.
3099pub(crate) fn wallet_recover_until(
3100    conn: &rusqlite::Connection,
3101) -> Result<Option<BlockHeight>, rusqlite::Error> {
3102    conn.query_row(
3103        "SELECT MAX(recover_until_height) AS wallet_recover_until FROM accounts",
3104        [],
3105        |row| {
3106            row.get::<_, Option<u32>>(0)
3107                .map(|opt| opt.map(BlockHeight::from))
3108        },
3109    )
3110}
3111
3112pub(crate) fn account_birthday(
3113    conn: &rusqlite::Connection,
3114    account_uuid: AccountUuid,
3115) -> Result<BlockHeight, SqliteClientError> {
3116    conn.query_row(
3117        "SELECT birthday_height
3118         FROM accounts
3119         WHERE uuid = :account_uuid",
3120        named_params![":account_uuid": account_uuid.0],
3121        |row| row.get::<_, u32>(0).map(BlockHeight::from),
3122    )
3123    .optional()
3124    .map_err(SqliteClientError::from)
3125    .and_then(|opt| opt.ok_or(SqliteClientError::AccountUnknown))
3126}
3127
3128#[cfg(feature = "transparent-inputs")]
3129pub(crate) fn account_birthday_internal(
3130    conn: &rusqlite::Connection,
3131    account_ref: AccountRef,
3132) -> Result<BlockHeight, SqliteClientError> {
3133    conn.query_row(
3134        "SELECT birthday_height
3135         FROM accounts
3136         WHERE id = :account_ref",
3137        named_params![":account_ref": account_ref.0],
3138        |row| row.get::<_, u32>(0).map(BlockHeight::from),
3139    )
3140    .optional()
3141    .map_err(SqliteClientError::from)
3142    .and_then(|opt| opt.ok_or(SqliteClientError::AccountUnknown))
3143}
3144
3145/// Returns the maximum recover-until height for accounts in the wallet.
3146pub(crate) fn recover_until_height(
3147    conn: &rusqlite::Connection,
3148) -> Result<Option<BlockHeight>, rusqlite::Error> {
3149    conn.query_row(
3150        "SELECT MAX(recover_until_height) FROM accounts",
3151        [],
3152        |row| {
3153            row.get::<_, Option<u32>>(0)
3154                .map(|opt| opt.map(BlockHeight::from))
3155        },
3156    )
3157}
3158
3159/// Returns the minimum and maximum heights for blocks stored in the wallet database.
3160pub(crate) fn block_height_extrema(
3161    conn: &rusqlite::Connection,
3162) -> Result<Option<RangeInclusive<BlockHeight>>, rusqlite::Error> {
3163    conn.query_row("SELECT MIN(height), MAX(height) FROM blocks", [], |row| {
3164        let min_height: Option<u32> = row.get(0)?;
3165        let max_height: Option<u32> = row.get(1)?;
3166        Ok(min_height
3167            .zip(max_height)
3168            .map(|(min, max)| RangeInclusive::new(min.into(), max.into())))
3169    })
3170}
3171
3172pub(crate) fn get_account_ref(
3173    conn: &rusqlite::Connection,
3174    account_uuid: AccountUuid,
3175) -> Result<AccountRef, SqliteClientError> {
3176    conn.query_row(
3177        "SELECT id FROM accounts WHERE uuid = :account_uuid",
3178        named_params! {":account_uuid": account_uuid.0},
3179        |row| row.get("id").map(AccountRef),
3180    )
3181    .optional()?
3182    .ok_or(SqliteClientError::AccountUnknown)
3183}
3184
3185/// Returns whether an anchor is computable at `height` for spends from the given pool.
3186///
3187/// An anchor is computable exactly at the heights whose note commitment tree checkpoints the
3188/// wallet retains, so this is answered from the pool's checkpoints table.
3189pub(crate) fn anchor_computable(
3190    conn: &rusqlite::Connection,
3191    protocol: ShieldedPool,
3192    height: BlockHeight,
3193) -> Result<bool, SqliteClientError> {
3194    let TableConstants { table_prefix, .. } =
3195        common::table_constants::<SqliteClientError>(protocol)?;
3196    conn.query_row(
3197        &format!(
3198            "SELECT EXISTS (
3199                 SELECT 1 FROM {table_prefix}_tree_checkpoints WHERE checkpoint_id = :height
3200             )"
3201        ),
3202        named_params![":height": u32::from(height)],
3203        |row| row.get(0),
3204    )
3205    .map_err(SqliteClientError::from)
3206}
3207
3208/// Returns the maximum height of blocks in the chain which may be scanned.
3209pub(crate) fn chain_tip_height(
3210    conn: &rusqlite::Connection,
3211) -> Result<Option<BlockHeight>, rusqlite::Error> {
3212    conn.query_row("SELECT MAX(block_range_end) FROM scan_queue", [], |row| {
3213        let max_height: Option<u32> = row.get(0)?;
3214
3215        // Scan ranges are end-exclusive, so we subtract 1 from `max_height` to obtain the
3216        // height of the last known chain tip;
3217        Ok(max_height.map(|h| BlockHeight::from(h.saturating_sub(1))))
3218    })
3219}
3220
3221pub(crate) fn mempool_height(
3222    conn: &rusqlite::Connection,
3223) -> Result<Option<TargetHeight>, rusqlite::Error> {
3224    Ok(chain_tip_height(conn)?.map(|h| TargetHeight::from(h + 1)))
3225}
3226
3227pub(crate) fn get_anchor_height(
3228    conn: &rusqlite::Connection,
3229    target_height: TargetHeight,
3230    min_confirmations: NonZeroU32,
3231) -> Result<Option<BlockHeight>, SqliteClientError> {
3232    let sapling_anchor_height = get_max_checkpointed_height(
3233        conn,
3234        ShieldedPool::Sapling,
3235        target_height,
3236        min_confirmations,
3237    )?;
3238
3239    #[cfg(feature = "orchard")]
3240    let orchard_anchor_height = get_max_checkpointed_height(
3241        conn,
3242        ShieldedPool::Orchard,
3243        target_height,
3244        min_confirmations,
3245    )?;
3246
3247    #[cfg(not(feature = "orchard"))]
3248    let orchard_anchor_height: Option<BlockHeight> = None;
3249
3250    Ok(sapling_anchor_height
3251        .zip(orchard_anchor_height)
3252        .map(|(s, o)| std::cmp::min(s, o))
3253        .or(sapling_anchor_height)
3254        .or(orchard_anchor_height))
3255}
3256
3257pub(crate) fn get_target_and_anchor_heights(
3258    conn: &rusqlite::Connection,
3259    min_confirmations: NonZeroU32,
3260) -> Result<Option<(TargetHeight, BlockHeight)>, SqliteClientError> {
3261    match mempool_height(conn)? {
3262        Some(target_height) => {
3263            let anchor_height = get_anchor_height(conn, target_height, min_confirmations)?;
3264
3265            Ok(anchor_height.map(|h| (target_height, h)))
3266        }
3267        None => Ok(None),
3268    }
3269}
3270
3271/// A row of block metadata as selected by [`block_metadata`] and [`block_max_scanned`]: the block
3272/// height and hash, the Sapling commitment tree size and legacy Sapling tree, and the Orchard and
3273/// Ironwood commitment tree sizes.
3274type BlockMetadataRow = (
3275    BlockHeight,
3276    Vec<u8>,
3277    Option<u32>,
3278    Vec<u8>,
3279    Option<u32>,
3280    Option<u32>,
3281);
3282
3283fn parse_block_metadata<P: consensus::Parameters>(
3284    _params: &P,
3285    row: BlockMetadataRow,
3286) -> Result<BlockMetadata, SqliteClientError> {
3287    let (
3288        block_height,
3289        hash_data,
3290        sapling_tree_size_opt,
3291        sapling_tree,
3292        _orchard_tree_size_opt,
3293        _ironwood_tree_size_opt,
3294    ) = row;
3295    let sapling_tree_size = sapling_tree_size_opt.map_or_else(|| {
3296        if sapling_tree == BLOCK_SAPLING_FRONTIER_ABSENT {
3297            Err(SqliteClientError::CorruptedData("One of either the Sapling tree size or the legacy Sapling commitment tree must be present.".to_owned()))
3298        } else {
3299            // parse the legacy commitment tree data
3300            read_commitment_tree::<
3301                ::sapling::Node,
3302                _,
3303                { ::sapling::NOTE_COMMITMENT_TREE_DEPTH },
3304            >(Cursor::new(sapling_tree))
3305            .map(|tree| tree.size().try_into().unwrap())
3306            .map_err(SqliteClientError::from)
3307        }
3308    }, Ok)?;
3309
3310    let block_hash = BlockHash::try_from_slice(&hash_data).ok_or_else(|| {
3311        SqliteClientError::from(io::Error::new(
3312            io::ErrorKind::InvalidData,
3313            format!("Invalid block hash length: {}", hash_data.len()),
3314        ))
3315    })?;
3316
3317    Ok(BlockMetadata::from_parts(
3318        block_height,
3319        block_hash,
3320        Some(sapling_tree_size),
3321        #[cfg(feature = "orchard")]
3322        if _params
3323            .activation_height(NetworkUpgrade::Nu5)
3324            .is_some_and(|nu5_activation| block_height >= nu5_activation)
3325        {
3326            _orchard_tree_size_opt
3327        } else {
3328            Some(0)
3329        },
3330        #[cfg(feature = "orchard")]
3331        if _params
3332            .activation_height(NetworkUpgrade::Nu6_3)
3333            .is_some_and(|nu6_3_activation| block_height >= nu6_3_activation)
3334        {
3335            _ironwood_tree_size_opt
3336        } else {
3337            Some(0)
3338        },
3339    ))
3340}
3341
3342#[tracing::instrument(skip(conn, params))]
3343pub(crate) fn block_metadata<P: consensus::Parameters>(
3344    conn: &rusqlite::Connection,
3345    params: &P,
3346    block_height: BlockHeight,
3347) -> Result<Option<BlockMetadata>, SqliteClientError> {
3348    conn.query_row(
3349        "SELECT height, hash, sapling_commitment_tree_size, sapling_tree, orchard_commitment_tree_size, ironwood_commitment_tree_size
3350        FROM blocks
3351        WHERE height = :block_height",
3352        named_params![":block_height": u32::from(block_height)],
3353        |row| {
3354            let height: u32 = row.get(0)?;
3355            let block_hash: Vec<u8> = row.get(1)?;
3356            let sapling_tree_size: Option<u32> = row.get(2)?;
3357            let sapling_tree: Vec<u8> = row.get(3)?;
3358            let orchard_tree_size: Option<u32> = row.get(4)?;
3359            let ironwood_tree_size: Option<u32> = row.get(5)?;
3360            Ok((
3361                BlockHeight::from(height),
3362                block_hash,
3363                sapling_tree_size,
3364                sapling_tree,
3365                orchard_tree_size,
3366                ironwood_tree_size,
3367            ))
3368        },
3369    )
3370    .optional()
3371    .map_err(SqliteClientError::from)
3372    .and_then(|meta_row| meta_row.map(|r| parse_block_metadata(params, r)).transpose())
3373}
3374
3375/// Returns the height to which the wallet is FULLY scanned (every block from the wallet birthday
3376/// through it has been scanned), or `None` if no contiguous scanned range reaches down to the
3377/// birthday (including for a wallet with no accounts). This is the height-only computation behind
3378/// [`block_fully_scanned`], separated so callers that need no block metadata (and hold no network
3379/// parameters) can share it rather than replicate it.
3380pub(crate) fn fully_scanned_height(
3381    conn: &rusqlite::Connection,
3382) -> Result<Option<BlockHeight>, rusqlite::Error> {
3383    let Some(birthday_height) = wallet_birthday(conn)? else {
3384        return Ok(None);
3385    };
3386    // We assume that the only way we get a contiguous range of block heights in the `blocks` table
3387    // starting with the birthday block, is if all scanning operations have been performed on those
3388    // blocks. This holds because the `blocks` table is only altered by `WalletDb::put_blocks` via
3389    // `put_block`, and the effective combination of intra-range linear scanning and the nullifier
3390    // map ensures that we discover all wallet-related information within the contiguous range.
3391    //
3392    // We also assume that every contiguous range of block heights in the `blocks` table has a
3393    // single matching entry in the `scan_queue` table with priority "Scanned". This requires no
3394    // bugs in the scan queue update logic, which we have had before. However, a bug here would
3395    // mean that we return a more conservative fully-scanned height, which likely just causes a
3396    // performance regression.
3397    //
3398    // The fully-scanned height is therefore the last height that falls within the first range in
3399    // the scan queue with priority "Scanned".
3400    let calc_fully_scanned_height = |row: &rusqlite::Row| {
3401        let block_range_start = BlockHeight::from_u32(row.get(0)?);
3402        let block_range_end = BlockHeight::from_u32(row.get(1)?);
3403
3404        // If the start of the earliest scanned range is greater than
3405        // the birthday height, then there is an unscanned range between
3406        // the wallet birthday and that range, so there is no fully
3407        // scanned height.
3408        Ok(if block_range_start <= birthday_height {
3409            // Scan ranges are end-exclusive.
3410            Some(block_range_end - 1)
3411        } else {
3412            None
3413        })
3414    };
3415    Ok(conn
3416        .query_row(
3417            "SELECT block_range_start, block_range_end
3418            FROM scan_queue
3419            WHERE priority = :priority
3420            ORDER BY block_range_start ASC
3421            LIMIT 1",
3422            named_params![":priority": priority_code(&ScanPriority::Scanned)],
3423            calc_fully_scanned_height,
3424        )
3425        .optional()?
3426        .flatten())
3427}
3428
3429#[tracing::instrument(skip_all)]
3430pub(crate) fn block_fully_scanned<P: consensus::Parameters>(
3431    conn: &rusqlite::Connection,
3432    params: &P,
3433) -> Result<Option<BlockMetadata>, SqliteClientError> {
3434    match fully_scanned_height(conn)? {
3435        Some(height) => block_metadata(conn, params, height),
3436        None => Ok(None),
3437    }
3438}
3439
3440pub(crate) fn block_max_scanned<P: consensus::Parameters>(
3441    conn: &rusqlite::Connection,
3442    params: &P,
3443) -> Result<Option<BlockMetadata>, SqliteClientError> {
3444    conn.query_row(
3445        "SELECT blocks.height, hash, sapling_commitment_tree_size, sapling_tree, orchard_commitment_tree_size, ironwood_commitment_tree_size
3446         FROM blocks
3447         JOIN (SELECT MAX(height) AS height FROM blocks) blocks_max
3448         ON blocks.height = blocks_max.height",
3449        [],
3450        |row| {
3451            let height: u32 = row.get(0)?;
3452            let block_hash: Vec<u8> = row.get(1)?;
3453            let sapling_tree_size: Option<u32> = row.get(2)?;
3454            let sapling_tree: Vec<u8> = row.get(3)?;
3455            let orchard_tree_size: Option<u32> = row.get(4)?;
3456            let ironwood_tree_size: Option<u32> = row.get(5)?;
3457            Ok((
3458                BlockHeight::from(height),
3459                block_hash,
3460                sapling_tree_size,
3461                sapling_tree,
3462                orchard_tree_size,
3463                ironwood_tree_size,
3464            ))
3465        },
3466    )
3467    .optional()
3468    .map_err(SqliteClientError::from)
3469    .and_then(|meta_row| meta_row.map(|r| parse_block_metadata(params, r)).transpose())
3470}
3471
3472/// Returns the block height at which the specified transaction was mined,
3473/// if any.
3474pub(crate) fn get_tx_height(
3475    conn: &rusqlite::Connection,
3476    txid: TxId,
3477) -> Result<Option<BlockHeight>, SqliteClientError> {
3478    let chain_tip_height = chain_tip_height(conn)?.ok_or(SqliteClientError::ChainHeightUnknown)?;
3479
3480    let tx_height = conn
3481        .query_row(
3482            "SELECT mined_height FROM transactions WHERE txid = ?",
3483            [txid.as_ref()],
3484            |row| Ok(row.get::<_, Option<u32>>(0)?.map(BlockHeight::from)),
3485        )
3486        .optional()
3487        .map(|opt| opt.flatten())?;
3488
3489    Ok(tx_height.filter(|h| h <= &chain_tip_height))
3490}
3491
3492/// Returns the block hash for the block at the specified height,
3493/// if any.
3494pub(crate) fn get_block_hash(
3495    conn: &rusqlite::Connection,
3496    block_height: BlockHeight,
3497) -> Result<Option<BlockHash>, rusqlite::Error> {
3498    conn.query_row(
3499        "SELECT hash FROM blocks WHERE height = ?",
3500        [u32::from(block_height)],
3501        |row| {
3502            let row_data = row.get::<_, Vec<_>>(0)?;
3503            Ok(BlockHash::from_slice(&row_data))
3504        },
3505    )
3506    .optional()
3507}
3508
3509pub(crate) fn get_max_height_hash(
3510    conn: &rusqlite::Connection,
3511) -> Result<Option<(BlockHeight, BlockHash)>, rusqlite::Error> {
3512    conn.query_row(
3513        "SELECT height, hash FROM blocks ORDER BY height DESC LIMIT 1",
3514        [],
3515        |row| {
3516            let height = row.get::<_, u32>(0).map(BlockHeight::from)?;
3517            let row_data = row.get::<_, Vec<_>>(1)?;
3518            Ok((height, BlockHash::from_slice(&row_data)))
3519        },
3520    )
3521    .optional()
3522}
3523
3524pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters>(
3525    conn: &rusqlite::Transaction,
3526    params: &P,
3527    #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
3528    sent_tx: &SentTransaction<AccountUuid>,
3529) -> Result<(), SqliteClientError> {
3530    let tx_ref = put_tx_data(
3531        conn,
3532        sent_tx.tx(),
3533        Some(sent_tx.fee_amount()),
3534        Some(sent_tx.created()),
3535        Some(sent_tx.target_height()),
3536        sent_tx.target_height().into(),
3537    )?;
3538
3539    let mut detectable_via_scanning = false;
3540
3541    // Mark notes as spent.
3542    //
3543    // This locks the notes so they aren't selected again by a subsequent call to
3544    // create_spend_to_address() before this transaction has been mined (at which point the notes
3545    // get re-marked as spent).
3546    //
3547    // Assumes that create_spend_to_address() will never be called in parallel, which is a
3548    // reasonable assumption for a light client such as a mobile phone.
3549    if let Some(bundle) = sent_tx.tx().sapling_bundle() {
3550        for spend in bundle.shielded_spends() {
3551            detectable_via_scanning |=
3552                sapling::mark_sapling_note_spent(conn, tx_ref, spend.nullifier())?;
3553        }
3554    }
3555    if let Some(_bundle) = sent_tx.tx().orchard_bundle() {
3556        #[cfg(feature = "orchard")]
3557        {
3558            for action in _bundle.actions() {
3559                detectable_via_scanning |=
3560                    orchard::mark_orchard_note_spent(conn, tx_ref, action.nullifier())?;
3561            }
3562        }
3563
3564        #[cfg(not(feature = "orchard"))]
3565        panic!("Sent a transaction with Orchard Actions without `orchard` enabled?");
3566    }
3567    if let Some(_bundle) = sent_tx.tx().ironwood_bundle() {
3568        #[cfg(feature = "orchard")]
3569        {
3570            for action in _bundle.actions() {
3571                detectable_via_scanning |=
3572                    orchard::mark_ironwood_note_spent(conn, tx_ref, action.nullifier())?;
3573            }
3574        }
3575
3576        #[cfg(not(feature = "orchard"))]
3577        panic!("Sent a transaction with Ironwood Actions without `orchard` enabled?");
3578    }
3579
3580    #[cfg(feature = "transparent-inputs")]
3581    for utxo_outpoint in sent_tx.utxos_spent() {
3582        transparent::mark_transparent_utxo_spent(conn, tx_ref, utxo_outpoint)?;
3583    }
3584
3585    // Unlock any notes that were locked for this transaction, since the spend records
3586    // now prevent them from being selected by subsequent proposals.
3587    locking::unlock_spent_notes(conn, tx_ref)?;
3588
3589    for output in sent_tx.outputs() {
3590        insert_sent_output(conn, params, tx_ref, *sent_tx.funding_account(), output)?;
3591
3592        match output.recipient() {
3593            Recipient::External {
3594                recipient_address: _zaddr,
3595                output_pool: _pool,
3596            } => {
3597                // In the case that a transaction sends to a transparent address belonging to the
3598                // wallet (such as is the case for gap limit management transactions) then we need
3599                // to add the received transparent output to our wallet. For shielded outputs sent
3600                // back to our own addresses, we can expect to detect those by normal scanning so
3601                // it's not necessary to add them here, and we don't have the note information
3602                // needed to do so.
3603                #[cfg(feature = "transparent-inputs")]
3604                if _pool == &PoolType::Transparent {
3605                    let address = Address::try_from_zcash_address(params, _zaddr.clone())
3606                        .expect("recipient is an understood Zcash address.");
3607                    if let Some(taddr) = address.to_transparent_address()
3608                        && transparent::find_account_uuid_for_transparent_address(
3609                            conn, params, &taddr,
3610                        )?
3611                        .is_some()
3612                    {
3613                        transparent::put_transparent_output(
3614                            conn,
3615                            params,
3616                            gap_limits,
3617                            &WalletTransparentOutput::from_parts(
3618                                OutPoint::new(
3619                                    sent_tx.tx().txid().into(),
3620                                    u32::try_from(output.output_index())
3621                                        .expect("output index fits into a u32"),
3622                                ),
3623                                TxOut::new(output.value(), taddr.script().into()),
3624                                None,
3625                                None,
3626                                Some(TransparentKeyScope::EXTERNAL),
3627                                Some(*sent_tx.funding_account()),
3628                            )
3629                            .expect(
3630                                "can extract a recipient address from an internal address script",
3631                            ),
3632                            sent_tx.target_height().into(),
3633                            true,
3634                        )?;
3635                    }
3636                }
3637            }
3638            Recipient::InternalShielded {
3639                receiving_account,
3640                note,
3641                ..
3642            } => {
3643                // An internal shielded output is decryptable by this wallet during ordinary
3644                // compact-block scanning.
3645                detectable_via_scanning = true;
3646
3647                match note.as_ref() {
3648                    Note::Sapling(note) => {
3649                        sapling::put_received_note(
3650                            conn,
3651                            params,
3652                            &DecryptedOutput::new(
3653                                output.output_index(),
3654                                note.clone(),
3655                                ShieldedPool::Sapling,
3656                                *receiving_account,
3657                                output
3658                                    .memo()
3659                                    .map_or_else(MemoBytes::empty, |memo| memo.clone()),
3660                                TransferType::AccountInternal,
3661                            ),
3662                            tx_ref,
3663                            Some(sent_tx.target_height().into()),
3664                            None,
3665                        )?;
3666                    }
3667                    #[cfg(feature = "orchard")]
3668                    orchard_note @ Note::Orchard { note, pool } => {
3669                        let shielded_pool = orchard_note.pool();
3670                        orchard::put_received_note(
3671                            conn,
3672                            params,
3673                            shielded_pool,
3674                            &DecryptedOutput::new(
3675                                output.output_index(),
3676                                (*note, *pool),
3677                                shielded_pool,
3678                                *receiving_account,
3679                                output
3680                                    .memo()
3681                                    .map_or_else(MemoBytes::empty, |memo| memo.clone()),
3682                                TransferType::AccountInternal,
3683                            ),
3684                            tx_ref,
3685                            Some(sent_tx.target_height().into()),
3686                            None,
3687                        )?;
3688                    }
3689                }
3690            }
3691            #[cfg(feature = "transparent-inputs")]
3692            Recipient::EphemeralTransparent {
3693                ephemeral_address,
3694                outpoint,
3695                ..
3696            } => {
3697                // Check to verify that creation of this output does not result in reuse of
3698                // an ephemeral address.
3699                transparent::check_ephemeral_address_reuse(conn, params, ephemeral_address)?;
3700
3701                // Look up the wallet account that owns the ephemeral address.
3702                let (recipient_account, _) =
3703                    transparent::find_account_uuid_for_transparent_address(
3704                        conn,
3705                        params,
3706                        ephemeral_address,
3707                    )?
3708                    .ok_or_else(|| {
3709                        SqliteClientError::CorruptedData(format!(
3710                            "ephemeral address {} does not belong to any wallet account",
3711                            ephemeral_address.encode(params),
3712                        ))
3713                    })?;
3714
3715                transparent::put_transparent_output(
3716                    conn,
3717                    params,
3718                    gap_limits,
3719                    &WalletTransparentOutput::from_parts(
3720                        outpoint.clone(),
3721                        TxOut::new(output.value(), ephemeral_address.script().into()),
3722                        None,
3723                        Some(recipient_account),
3724                        Some(TransparentKeyScope::EPHEMERAL),
3725                        Some(*sent_tx.funding_account()),
3726                    )
3727                    .expect("can extract a recipient address from an ephemeral address script"),
3728                    sent_tx.target_height().into(),
3729                    true,
3730                )?;
3731            }
3732            #[cfg(feature = "transparent-inputs")]
3733            Recipient::InternalTransparent {
3734                receiving_account,
3735                recipient_address,
3736            } => {
3737                transparent::put_transparent_output(
3738                    conn,
3739                    params,
3740                    gap_limits,
3741                    &WalletTransparentOutput::from_parts(
3742                        OutPoint::new(
3743                            sent_tx.tx().txid().into(),
3744                            u32::try_from(output.output_index())
3745                                .expect("output index fits into a u32"),
3746                        ),
3747                        TxOut::new(output.value(), recipient_address.script().into()),
3748                        None,
3749                        Some(*receiving_account),
3750                        None,
3751                        Some(*sent_tx.funding_account()),
3752                    )
3753                    .expect("can extract a recipient address from a transparent recipient_address"),
3754                    sent_tx.target_height().into(),
3755                    true,
3756                )?;
3757            }
3758        }
3759    }
3760
3761    // Query by txid when compact-block scanning cannot observe either a wallet-owned shielded
3762    // spend or a wallet-owned shielded output. In particular, a transaction funded entirely by
3763    // transparent inputs and sending shielded funds exclusively to another wallet is not
3764    // detectable merely because it contains a shielded bundle.
3765    if !detectable_via_scanning {
3766        queue_tx_status(conn, sent_tx.tx().txid())?;
3767    }
3768
3769    Ok(())
3770}
3771
3772pub(crate) fn set_transaction_status<P: consensus::Parameters>(
3773    conn: &rusqlite::Transaction,
3774    _params: &P,
3775    #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
3776    txid: TxId,
3777    status: TransactionStatus,
3778) -> Result<(), SqliteClientError> {
3779    let chain_tip = chain_tip_height(conn)?.ok_or(SqliteClientError::ChainHeightUnknown)?;
3780
3781    match status {
3782        TransactionStatus::TxidNotRecognized | TransactionStatus::NotInMainChain => {
3783            conn.execute(
3784                "UPDATE transactions
3785                 SET confirmed_unmined_at_height = :chain_tip
3786                 WHERE txid = :txid
3787                 AND mined_height IS NULL",
3788                named_params![
3789                    ":txid": txid.as_ref(),
3790                    ":chain_tip": u32::from(chain_tip)
3791                ],
3792            )?;
3793
3794            // Enhancement is complete once the server has reported that it cannot provide the
3795            // transaction. A status-observation intent remains active until the transaction is
3796            // confirmed to be terminal.
3797            delete_retrieval_queue_entry(conn, txid, TxQueryType::Enhancement)?;
3798            conn.execute(
3799                "DELETE FROM tx_retrieval_queue
3800                 WHERE txid = :txid
3801                 AND query_type = :status_type
3802                 AND NOT EXISTS (
3803                    SELECT 1
3804                    FROM transactions t
3805                    WHERE t.txid = :txid
3806                    AND t.mined_height IS NULL
3807                    AND (
3808                        t.expiry_height = 0
3809                        OR (
3810                            t.expiry_height > 0
3811                            AND t.confirmed_unmined_at_height < t.expiry_height
3812                        )
3813                        OR (
3814                            t.expiry_height IS NULL
3815                            AND t.confirmed_unmined_at_height
3816                                < t.min_observed_height + :certainty_depth
3817                        )
3818                    )
3819                 )",
3820                named_params![
3821                    ":txid": txid.as_ref(),
3822                    ":status_type": TxQueryType::Status.code(),
3823                    ":certainty_depth": PRUNING_DEPTH + DEFAULT_TX_EXPIRY_DELTA,
3824                ],
3825            )?;
3826        }
3827        TransactionStatus::Mined(height) => {
3828            // The transaction has been mined, so we can set its mined height and associate it with
3829            // the appropriate block. A status-observation intent is retained but remains dormant
3830            // while the mined height is known, so that it automatically becomes active if a
3831            // subsequent chain rewind un-mines the transaction.
3832            let sql_args = named_params![
3833                ":txid": txid.as_ref(),
3834                ":height": u32::from(height)
3835            ];
3836
3837            conn.execute(
3838                "UPDATE transactions
3839                 SET mined_height = :height,
3840                     min_observed_height = MIN(
3841                        min_observed_height,
3842                        IFNULL(mined_height, :height),
3843                        :height
3844                     ),
3845                     confirmed_unmined_at_height = NULL
3846                 WHERE txid = :txid",
3847                sql_args,
3848            )?;
3849
3850            conn.execute(
3851                "UPDATE transactions
3852                 SET block = blocks.height
3853                 FROM blocks
3854                 WHERE txid = :txid
3855                 AND blocks.height = :height",
3856                sql_args,
3857            )?;
3858
3859            #[cfg(feature = "transparent-inputs")]
3860            transparent::update_gap_limits(conn, _params, gap_limits, txid, height)?;
3861
3862            delete_retrieval_queue_entry(conn, txid, TxQueryType::Enhancement)?;
3863        }
3864    }
3865
3866    Ok(())
3867}
3868
3869/// Returns the minimum checkpoint height that exists in all note commitment trees that contain
3870/// data. A height qualifies when every tree that has any checkpoints has a checkpoint at that
3871/// height. Returns `None` when all trees are empty.
3872fn min_shared_checkpoint_height(
3873    conn: &rusqlite::Connection,
3874) -> Result<Option<BlockHeight>, SqliteClientError> {
3875    Ok(conn
3876        .query_row(
3877            "SELECT MIN(checkpoint_id) FROM (
3878                SELECT checkpoint_id FROM sapling_tree_checkpoints
3879                UNION
3880                SELECT checkpoint_id FROM orchard_tree_checkpoints
3881                UNION
3882                SELECT checkpoint_id FROM ironwood_tree_checkpoints
3883             )
3884             WHERE (checkpoint_id IN (SELECT checkpoint_id FROM sapling_tree_checkpoints)
3885                    OR NOT EXISTS (SELECT 1 FROM sapling_tree_checkpoints))
3886             AND (checkpoint_id IN (SELECT checkpoint_id FROM orchard_tree_checkpoints)
3887                  OR NOT EXISTS (SELECT 1 FROM orchard_tree_checkpoints))
3888             AND (checkpoint_id IN (SELECT checkpoint_id FROM ironwood_tree_checkpoints)
3889                  OR NOT EXISTS (SELECT 1 FROM ironwood_tree_checkpoints))",
3890            [],
3891            |row| row.get::<_, Option<u32>>(0),
3892        )
3893        .optional()?
3894        .flatten()
3895        .map(BlockHeight::from))
3896}
3897
3898/// Returns a SQL predicate over a candidate `height` column that holds when the note
3899/// commitment tree for the pool with the given table prefix can be brought into agreement
3900/// with a truncation of the wallet to that height.
3901///
3902/// This is the SQL rendering of the classification performed by [`plan_tree_truncation`]; the
3903/// two must be kept in agreement. A height qualifies for a pool when one of the following
3904/// holds:
3905/// - the pool has a checkpoint at exactly that height ([`TreeTruncation::ToCheckpoint`]);
3906/// - the pool retains no checkpoint above that height, so its tree holds no state that the
3907///   truncation must remove ([`TreeTruncation::Unaffected`]);
3908/// - every checkpoint the pool retains lies above that height, *and* the pool has no notes
3909///   with recorded witness positions mined at or below it, so the tree can be reset to just
3910///   its completed subtree roots without destroying any witness that a rescan of the heights
3911///   above it would not re-create ([`TreeTruncation::ResetToSubtreeRoots`]).
3912///
3913/// A height that [`plan_tree_truncation`] would classify as
3914/// [`TreeTruncation::WouldDestroyWitnesses`] or [`TreeTruncation::DivergedCheckpoints`] for
3915/// the pool does not qualify.
3916fn pool_truncation_tolerance_sql(table_prefix: &str) -> String {
3917    format!(
3918        "(height IN (SELECT checkpoint_id FROM {table_prefix}_tree_checkpoints)
3919          OR NOT EXISTS (
3920              SELECT 1 FROM {table_prefix}_tree_checkpoints WHERE checkpoint_id > height)
3921          OR (NOT EXISTS (
3922                  SELECT 1 FROM {table_prefix}_tree_checkpoints WHERE checkpoint_id < height)
3923              AND NOT EXISTS (
3924                  SELECT 1 FROM {table_prefix}_received_notes rn
3925                  JOIN transactions tx ON tx.id_tx = rn.transaction_id
3926                  WHERE tx.mined_height <= height
3927                  AND rn.commitment_tree_position IS NOT NULL)))"
3928    )
3929}
3930
3931/// Determine the height at or below the requested height to which the wallet can be
3932/// truncated, if any.
3933///
3934/// A height qualifies when, for every pool, either a checkpoint exists at exactly that height
3935/// or the pool's note commitment tree can tolerate the truncation without one: because the
3936/// tree retains no checkpoint above the height (an empty or lagging tree that the truncation
3937/// leaves untouched), or because every checkpoint it retains lies above the height and no
3938/// recorded note witness would be destroyed by resetting the tree to its completed subtree
3939/// roots (a tree whose scanned state postdates the truncation point, e.g. because a
3940/// post-migration rescan has so far only reached blocks near the chain tip). The per-pool
3941/// tolerance is [`plan_tree_truncation`]'s
3942/// classification, rendered in SQL by [`pool_truncation_tolerance_sql`]; the qualifying
3943/// height must also be present in the `blocks` table. This returns the maximum qualifying
3944/// height at or below `requested_height`.
3945///
3946/// The orchard and ironwood tables exist unconditionally but are empty when the `orchard`
3947/// feature is not active, in which case their trees qualify at every height.
3948fn select_truncation_height(
3949    conn: &rusqlite::Transaction,
3950    requested_height: BlockHeight,
3951) -> Result<BlockHeight, SqliteClientError> {
3952    conn.query_row(
3953        &format!(
3954            "SELECT MAX(height) FROM blocks
3955             WHERE height <= :requested_height
3956             AND {sapling_tolerance}
3957             AND {orchard_tolerance}
3958             AND {ironwood_tolerance}",
3959            sapling_tolerance = pool_truncation_tolerance_sql(crate::SAPLING_TABLES_PREFIX),
3960            orchard_tolerance = pool_truncation_tolerance_sql(crate::ORCHARD_TABLES_PREFIX),
3961            ironwood_tolerance = pool_truncation_tolerance_sql(crate::IRONWOOD_TABLES_PREFIX),
3962        ),
3963        named_params! {":requested_height": u32::from(requested_height)},
3964        |row| row.get::<_, Option<u32>>(0),
3965    )
3966    .optional()?
3967    .flatten()
3968    .map_or_else(
3969        || {
3970            // If no height at or below the requested truncation height qualifies, query for
3971            // the minimum shared checkpoint height so that we can report a safe rewind height
3972            // to the caller. (This reports a height that is guaranteed to qualify, but under
3973            // the per-pool tolerances above it is not necessarily the minimum such height.)
3974            Err(SqliteClientError::RequestedRewindInvalid {
3975                safe_rewind_height: min_shared_checkpoint_height(conn)?,
3976                requested_height,
3977            })
3978        },
3979        |h| Ok(BlockHeight::from(h)),
3980    )
3981}
3982
3983/// Truncates the database to at most the given height.
3984///
3985/// If the requested height is greater than or equal to the height of the last scanned
3986/// block, this function does nothing.
3987///
3988/// This should only be executed inside a transactional context.
3989///
3990/// Returns the block height to which the database was truncated.
3991///
3992/// # Errors
3993///
3994/// - [`SqliteClientError::RequestedRewindInvalid`] if there is no height at or below
3995///   `max_height` at which the wallet's note commitment trees can be consistently truncated
3996///   (see [`select_truncation_height`]). The error payload reports a safe rewind height, if
3997///   one could be determined.
3998/// - [`SqliteClientError::TruncateCommitmentTree`] if truncating one of the wallet's note
3999///   commitment trees to the resolved checkpoint fails. The error payload identifies the
4000///   affected shielded pool and the target height.
4001/// - [`SqliteClientError::DbError`] if an underlying SQLite operation fails.
4002pub(crate) fn truncate_to_height<P: consensus::Parameters>(
4003    conn: &rusqlite::Transaction,
4004    params: &P,
4005    #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
4006    max_height: BlockHeight,
4007) -> Result<BlockHeight, SqliteClientError> {
4008    let truncation_height = select_truncation_height(conn, max_height)?;
4009    truncate_to_height_internal(
4010        conn,
4011        params,
4012        #[cfg(feature = "transparent-inputs")]
4013        gap_limits,
4014        truncation_height,
4015        truncation_height,
4016    )
4017}
4018
4019/// The action that a truncation of the wallet to a given block height must take for a single
4020/// pool's note commitment tree in order to leave that tree consistent with the truncated
4021/// wallet state, or the reason that no such action exists and the truncation cannot be
4022/// executed.
4023///
4024/// Which case applies is determined by [`plan_tree_truncation`] from where the tree's
4025/// retained checkpoints lie relative to the truncation height. [`select_truncation_height`]
4026/// applies the same classification in SQL (via [`pool_truncation_tolerance_sql`]) when
4027/// choosing a truncation height for [`truncate_to_height`]; the two must be kept in
4028/// agreement.
4029enum TreeTruncation {
4030    /// The tree has a checkpoint at exactly the truncation height; truncate to it.
4031    ToCheckpoint,
4032    /// The tree retains no checkpoint above the truncation height, so it holds no state that
4033    /// the truncation must remove; leave it untouched. This covers both a tree that is
4034    /// entirely empty (e.g. one whose `*_shardtree` migration has just created its tables)
4035    /// and a tree that lags the truncation height because a rescan has not yet caught up to
4036    /// it.
4037    Unaffected,
4038    /// Every checkpoint the tree retains lies above the truncation height, so a correct
4039    /// truncation discards all of the tree's scanned state. `ShardTree::truncate_to_checkpoint`
4040    /// cannot express this (there is no checkpoint at or below the target to truncate to), so
4041    /// the tree is instead reset to contain only the roots of subtrees completed at or below
4042    /// the truncation height (via [`commitment_tree::truncate_tree_to_subtree_roots`]) — those
4043    /// roots remain facts about the retained portion of the chain and are required to
4044    /// construct witnesses spanning their subtrees — and the rescan of the heights above the
4045    /// truncation point re-creates the rest. This is the state of a pool whose post-migration
4046    /// rescan has so far only reached blocks near the chain tip.
4047    ResetToSubtreeRoots,
4048    /// The truncation cannot be executed: it would discard all of the tree's scanned state
4049    /// (every checkpoint the tree retains lies above the truncation height, as for
4050    /// [`TreeTruncation::ResetToSubtreeRoots`]), but the pool has notes with recorded
4051    /// witness positions mined at or below the rescan floor, whose witness data no rescan
4052    /// following the truncation would re-create. This is an expected outcome of valid scan
4053    /// history, not evidence of corruption; the wallet simply cannot be truncated to this
4054    /// height.
4055    WouldDestroyWitnesses,
4056    /// The truncation cannot be executed: the tree retains checkpoints both above and below
4057    /// the truncation height but none at it, so there is neither a checkpoint to truncate
4058    /// to nor a whole-tree action that would leave the tree consistent with the truncated
4059    /// wallet state. This indicates that the tree's checkpoints have genuinely diverged
4060    /// from those of the pool(s) that determined the truncation height, i.e. corrupted
4061    /// wallet data.
4062    DivergedCheckpoints,
4063}
4064
4065/// Determines the [`TreeTruncation`] case that applies to the note commitment tree for the
4066/// pool with the given table prefix under a truncation of the wallet to
4067/// `truncation_height`: the action required to bring the tree into agreement with the
4068/// truncated wallet state, or the reason that the truncation cannot be executed. How each
4069/// case is reported to the caller is the caller's decision.
4070///
4071/// `rescan_floor` is the height above which the caller guarantees that blocks will be
4072/// re-scanned after the truncation: for [`rewind_to_chain_state`] this is the rewind target,
4073/// while for [`truncate_to_height`] it is the truncation height itself. Tree state for
4074/// heights at or below the rescan floor cannot be re-created by that rescan, so a truncation
4075/// that would discard such state is classified as
4076/// [`TreeTruncation::WouldDestroyWitnesses`] rather than a permitted
4077/// [`TreeTruncation::ResetToSubtreeRoots`].
4078fn plan_tree_truncation(
4079    conn: &rusqlite::Transaction,
4080    table_prefix: &'static str,
4081    truncation_height: BlockHeight,
4082    rescan_floor: BlockHeight,
4083) -> Result<TreeTruncation, rusqlite::Error> {
4084    let (has_at, has_above, has_below) = conn.query_row(
4085        &format!(
4086            "SELECT
4087             EXISTS(SELECT 1 FROM {table_prefix}_tree_checkpoints
4088                    WHERE checkpoint_id = :height),
4089             EXISTS(SELECT 1 FROM {table_prefix}_tree_checkpoints
4090                    WHERE checkpoint_id > :height),
4091             EXISTS(SELECT 1 FROM {table_prefix}_tree_checkpoints
4092                    WHERE checkpoint_id < :height)"
4093        ),
4094        named_params![":height": u32::from(truncation_height)],
4095        |row| {
4096            Ok((
4097                row.get::<_, bool>(0)?,
4098                row.get::<_, bool>(1)?,
4099                row.get::<_, bool>(2)?,
4100            ))
4101        },
4102    )?;
4103
4104    match (has_at, has_above, has_below) {
4105        (true, _, _) => Ok(TreeTruncation::ToCheckpoint),
4106        (false, false, _) => Ok(TreeTruncation::Unaffected),
4107        (false, true, false) => {
4108            let loses_witnesses = conn.query_row(
4109                &format!(
4110                    "SELECT EXISTS(
4111                         SELECT 1 FROM {table_prefix}_received_notes rn
4112                         JOIN transactions tx ON tx.id_tx = rn.transaction_id
4113                         WHERE tx.mined_height <= :height
4114                         AND rn.commitment_tree_position IS NOT NULL)"
4115                ),
4116                named_params![":height": u32::from(rescan_floor)],
4117                |row| row.get::<_, bool>(0),
4118            )?;
4119            Ok(if loses_witnesses {
4120                TreeTruncation::WouldDestroyWitnesses
4121            } else {
4122                TreeTruncation::ResetToSubtreeRoots
4123            })
4124        }
4125        (false, true, true) => Ok(TreeTruncation::DivergedCheckpoints),
4126    }
4127}
4128
4129/// Reports a [`TreeTruncation::WouldDestroyWitnesses`] classification for the given pool as
4130/// [`SqliteClientError::RequestedRewindInvalid`]: the wallet's state is valid, but it cannot
4131/// be truncated to the requested height without destroying witness data, so the caller is
4132/// directed to the minimum shared checkpoint height as a safe alternative.
4133fn witness_destroying_truncation_error(
4134    conn: &rusqlite::Connection,
4135    pool: ShieldedPool,
4136    truncation_height: BlockHeight,
4137    rescan_floor: BlockHeight,
4138) -> SqliteClientError {
4139    warn!(
4140        "truncation to height {truncation_height} would discard the scanned state of the \
4141         {pool:?} note commitment tree, destroying witness data for notes received at or \
4142         below height {rescan_floor} that no rescan would re-create"
4143    );
4144    min_shared_checkpoint_height(conn).map_or_else(
4145        |e| e,
4146        |safe_rewind_height| SqliteClientError::RequestedRewindInvalid {
4147            safe_rewind_height,
4148            requested_height: rescan_floor,
4149        },
4150    )
4151}
4152
4153/// Reports a [`TreeTruncation::DivergedCheckpoints`] classification for the given pool as
4154/// [`SqliteClientError::CorruptedData`].
4155fn diverged_checkpoints_error(
4156    pool: ShieldedPool,
4157    truncation_height: BlockHeight,
4158) -> SqliteClientError {
4159    SqliteClientError::CorruptedData(format!(
4160        "the {pool:?} note commitment tree retains checkpoints both above and below \
4161         height {truncation_height}, but none at that height to truncate to"
4162    ))
4163}
4164
4165/// Truncates the wallet to `truncation_height`, bringing each pool's note commitment tree
4166/// into agreement with the truncated state via the [`TreeTruncation`] action that
4167/// [`plan_tree_truncation`] determines for it.
4168///
4169/// `rescan_floor` is the height above which the caller guarantees that blocks will be
4170/// re-scanned after the truncation; see [`plan_tree_truncation`] for how it constrains the
4171/// permitted tree truncation actions.
4172///
4173/// A pool classified as [`TreeTruncation::WouldDestroyWitnesses`] makes the truncation
4174/// inexecutable without indicating any inconsistency in the wallet's state; this is
4175/// reported as [`SqliteClientError::RequestedRewindInvalid`]. A pool classified as
4176/// [`TreeTruncation::DivergedCheckpoints`] is reported as
4177/// [`SqliteClientError::CorruptedData`].
4178pub(crate) fn truncate_to_height_internal<P: consensus::Parameters>(
4179    conn: &rusqlite::Transaction,
4180    params: &P,
4181    #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
4182    truncation_height: BlockHeight,
4183    rescan_floor: BlockHeight,
4184) -> Result<BlockHeight, SqliteClientError> {
4185    let last_scanned_height = conn.query_row("SELECT MAX(height) FROM blocks", [], |row| {
4186        let h = row.get::<_, Option<u32>>(0)?;
4187
4188        Ok(h.map_or_else(
4189            || {
4190                params
4191                    .activation_height(NetworkUpgrade::Sapling)
4192                    // Fall back to the genesis block in regtest mode.
4193                    .map_or(BlockHeight::from_u32(0), |h| h - 1)
4194            },
4195            BlockHeight::from,
4196        ))
4197    })?;
4198
4199    // Delete from the scanning queue any range with a start height greater than the
4200    // truncation height, and then truncate any remaining range by setting the end
4201    // equal to the truncation height + 1. This sets our view of the chain tip back
4202    // to the retained height.
4203    trim_scan_queue_to(conn, truncation_height)?;
4204
4205    // Mark transparent utxos as un-mined. Since the TXO is now not mined, it would ideally be
4206    // considered to have been returned to the mempool; it _might_ be spendable in this state, but
4207    // we must also set its max_observed_unspent_height field to NULL because the transaction may
4208    // be rendered entirely invalid by a reorg that alters anchor(s) used in constructing shielded
4209    // spends in the transaction.
4210    conn.execute(
4211        "UPDATE transparent_received_outputs
4212         SET max_observed_unspent_height = CASE
4213            WHEN tx.mined_height <= :height THEN :height
4214            ELSE NULL
4215         END
4216         FROM transactions tx
4217         WHERE tx.id_tx = transaction_id
4218         AND max_observed_unspent_height > :height",
4219        named_params![":height": u32::from(truncation_height)],
4220    )?;
4221
4222    // Un-mine transactions. This must be done outside of the last_scanned_height check because
4223    // transaction entries may be created as a consequence of receiving transparent TXOs.
4224    conn.execute(
4225        "UPDATE transactions
4226         SET block = NULL, mined_height = NULL, tx_index = NULL, confirmed_unmined_at_height = NULL
4227         WHERE mined_height > :height",
4228        named_params![":height": u32::from(truncation_height)],
4229    )?;
4230
4231    // If we're removing scanned blocks, we need to truncate the note commitment tree and remove
4232    // affected block records from the database.
4233    if truncation_height < last_scanned_height {
4234        // Truncate the note commitment trees, applying to each pool's tree the action that
4235        // its checkpoint coverage of the truncation height requires.
4236        let mut wdb = WalletDb {
4237            conn: SqlTransaction(conn),
4238            params: params.clone(),
4239            clock: (),
4240            rng: (),
4241            // Truncation removes checkpoints; it never establishes them, so no anchor retention
4242            // decision is made through this handle and the interval is immaterial.
4243            anchor_retention_interval: AnchorRetentionInterval::default(),
4244            #[cfg(feature = "transparent-inputs")]
4245            gap_limits: *gap_limits,
4246        };
4247        match plan_tree_truncation(
4248            conn,
4249            crate::SAPLING_TABLES_PREFIX,
4250            truncation_height,
4251            rescan_floor,
4252        )? {
4253            TreeTruncation::ToCheckpoint => wdb.with_sapling_tree_mut(|tree| {
4254                let truncated =
4255                    tree.truncate_to_checkpoint(&truncation_height)
4256                        .map_err(|error| SqliteClientError::TruncateCommitmentTree {
4257                            pool: ShieldedPool::Sapling,
4258                            height: truncation_height,
4259                            error,
4260                        })?;
4261                if truncated {
4262                    Ok(())
4263                } else {
4264                    Err(SqliteClientError::CorruptedData(format!(
4265                        "the Sapling note commitment tree reported no checkpoint at height \
4266                         {truncation_height} to truncate to"
4267                    )))
4268                }
4269            })?,
4270            TreeTruncation::Unaffected => (),
4271            TreeTruncation::ResetToSubtreeRoots => {
4272                commitment_tree::truncate_tree_to_subtree_roots::<
4273                    ::sapling::Node,
4274                    { ::sapling::NOTE_COMMITMENT_TREE_DEPTH },
4275                    SAPLING_SHARD_HEIGHT,
4276                >(conn, crate::SAPLING_TABLES_PREFIX, truncation_height)
4277                .map_err(SqliteClientError::from)?
4278            }
4279            TreeTruncation::WouldDestroyWitnesses => {
4280                return Err(witness_destroying_truncation_error(
4281                    conn,
4282                    ShieldedPool::Sapling,
4283                    truncation_height,
4284                    rescan_floor,
4285                ));
4286            }
4287            TreeTruncation::DivergedCheckpoints => {
4288                return Err(diverged_checkpoints_error(
4289                    ShieldedPool::Sapling,
4290                    truncation_height,
4291                ));
4292            }
4293        }
4294        #[cfg(feature = "orchard")]
4295        match plan_tree_truncation(
4296            conn,
4297            crate::ORCHARD_TABLES_PREFIX,
4298            truncation_height,
4299            rescan_floor,
4300        )? {
4301            TreeTruncation::ToCheckpoint => wdb.with_orchard_tree_mut(|tree| {
4302                let truncated =
4303                    tree.truncate_to_checkpoint(&truncation_height)
4304                        .map_err(|error| SqliteClientError::TruncateCommitmentTree {
4305                            pool: ShieldedPool::Orchard,
4306                            height: truncation_height,
4307                            error,
4308                        })?;
4309                if truncated {
4310                    Ok(())
4311                } else {
4312                    Err(SqliteClientError::CorruptedData(format!(
4313                        "the Orchard note commitment tree reported no checkpoint at height \
4314                         {truncation_height} to truncate to"
4315                    )))
4316                }
4317            })?,
4318            TreeTruncation::Unaffected => (),
4319            TreeTruncation::ResetToSubtreeRoots => {
4320                commitment_tree::truncate_tree_to_subtree_roots::<
4321                    ::orchard::tree::MerkleHashOrchard,
4322                    { ::orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
4323                    ORCHARD_SHARD_HEIGHT,
4324                >(conn, crate::ORCHARD_TABLES_PREFIX, truncation_height)
4325                .map_err(SqliteClientError::from)?
4326            }
4327            TreeTruncation::WouldDestroyWitnesses => {
4328                return Err(witness_destroying_truncation_error(
4329                    conn,
4330                    ShieldedPool::Orchard,
4331                    truncation_height,
4332                    rescan_floor,
4333                ));
4334            }
4335            TreeTruncation::DivergedCheckpoints => {
4336                return Err(diverged_checkpoints_error(
4337                    ShieldedPool::Orchard,
4338                    truncation_height,
4339                ));
4340            }
4341        }
4342        #[cfg(feature = "orchard")]
4343        match plan_tree_truncation(
4344            conn,
4345            crate::IRONWOOD_TABLES_PREFIX,
4346            truncation_height,
4347            rescan_floor,
4348        )? {
4349            TreeTruncation::ToCheckpoint => {
4350                wdb.with_ironwood_tree_mut(|tree| {
4351                    let truncated =
4352                        tree.truncate_to_checkpoint(&truncation_height)
4353                            .map_err(|error| SqliteClientError::TruncateCommitmentTree {
4354                                pool: ShieldedPool::Ironwood,
4355                                height: truncation_height,
4356                                error,
4357                            })?;
4358                    if truncated {
4359                        Ok(())
4360                    } else {
4361                        Err(SqliteClientError::CorruptedData(format!(
4362                            "the Ironwood note commitment tree reported no checkpoint at \
4363                             height {truncation_height} to truncate to"
4364                        )))
4365                    }
4366                })?;
4367            }
4368            TreeTruncation::Unaffected => (),
4369            TreeTruncation::ResetToSubtreeRoots => {
4370                commitment_tree::truncate_tree_to_subtree_roots::<
4371                    ::orchard::tree::MerkleHashOrchard,
4372                    { ::orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
4373                    IRONWOOD_SHARD_HEIGHT,
4374                >(conn, crate::IRONWOOD_TABLES_PREFIX, truncation_height)
4375                .map_err(SqliteClientError::from)?
4376            }
4377            TreeTruncation::WouldDestroyWitnesses => {
4378                return Err(witness_destroying_truncation_error(
4379                    conn,
4380                    ShieldedPool::Ironwood,
4381                    truncation_height,
4382                    rescan_floor,
4383                ));
4384            }
4385            TreeTruncation::DivergedCheckpoints => {
4386                return Err(diverged_checkpoints_error(
4387                    ShieldedPool::Ironwood,
4388                    truncation_height,
4389                ));
4390            }
4391        }
4392
4393        // Do not delete sent notes; this can contain data that is not recoverable
4394        // from the chain. Wallets must continue to operate correctly in the
4395        // presence of stale sent notes that link to unmined transactions.
4396        // Also, do not delete received notes; they may contain memo data that is
4397        // not recoverable; balance APIs must ensure that un-mined received notes
4398        // do not count towards spendability or transaction balalnce.
4399
4400        // Now that they aren't depended on, delete un-mined blocks.
4401        conn.execute(
4402            "DELETE FROM blocks WHERE height > ?",
4403            [u32::from(truncation_height)],
4404        )?;
4405
4406        // Delete from the nullifier map any entries with a locator referencing a block
4407        // height greater than the truncation height.
4408        conn.execute(
4409            "DELETE FROM tx_locator_map
4410            WHERE block_height > :block_height",
4411            named_params![":block_height": u32::from(truncation_height)],
4412        )?;
4413    }
4414
4415    // Upstream rolls every stored pool migration back here, in the same transaction and at the
4416    // height actually ACHIEVED. This fork does not carry the pool-migration engine, so there are
4417    // no migration rows to roll back; the tables still exist, because their schema migrations are
4418    // kept for database compatibility, but nothing writes them.
4419
4420    Ok(truncation_height)
4421}
4422
4423/// Truncates the wallet database to a precise block height using note commitment tree frontiers
4424/// from the provided `ChainState`.
4425///
4426/// This function enables precise truncation even when the target height's checkpoint has been
4427/// pruned from the note commitment tree. It works in two cases:
4428///
4429/// - If a checkpoint exists at the target height, this behaves identically to
4430///   [`truncate_to_height`].
4431/// - If the target height is below the oldest available checkpoint, it first truncates to the
4432///   oldest checkpoint to ensure that the a checkpoint added at the provided frontier position does
4433///   not get immediately pruned, then inserts the provided frontier as a new checkpoint at the
4434///   target height, and finally truncates to that new checkpoint.
4435///
4436/// # Errors
4437///
4438/// - [`SqliteClientError::TruncateCommitmentTree`] if inserting the chain-state frontier as a
4439///   checkpoint, or truncating one of the wallet's Sapling or Orchard note commitment trees to a
4440///   checkpoint, fails. The error payload identifies the affected shielded pool and the target
4441///   height. Unlike [`truncate_to_height`], a missing checkpoint at the target height is not an
4442///   error here: it is recovered from by inserting the provided frontier as a new checkpoint.
4443/// - [`SqliteClientError::DbError`] if an underlying SQLite operation fails.
4444pub(crate) fn truncate_to_chain_state<P: consensus::Parameters, CL, R>(
4445    wdb: &mut WalletDb<SqlTransaction<'_>, P, CL, R>,
4446    chain_state: ChainState,
4447) -> Result<(), SqliteClientError> {
4448    let target_height = chain_state.block_height();
4449
4450    // Only truncate trees when the maximum scanned height is greater than the target height. When
4451    // the target height is at or above the max scanned height, we skip frontier insertion (it is
4452    // unnecessary at the max scanned height, and could introduce a subtree root discontinuity
4453    // above it; the frontier will be added naturally during scanning). We will however still need
4454    // to truncate the scan queue so that ranges above the target are removed.
4455    let truncate_trees = block_max_scanned(wdb.conn.0, &wdb.params)?
4456        .is_some_and(|meta| meta.block_height() > target_height);
4457
4458    if truncate_trees {
4459        // Try the simple case first: if a checkpoint exists at or below the target height,
4460        // truncate_to_height will succeed directly.
4461        match select_truncation_height(wdb.conn.0, target_height) {
4462            Ok(h) => {
4463                if h == target_height {
4464                    // There is a checkpoint for the requested height, we can just truncate to
4465                    // it and return.
4466                    return truncate_to_height_internal(
4467                        wdb.conn.0,
4468                        &wdb.params,
4469                        #[cfg(feature = "transparent-inputs")]
4470                        &wdb.gap_limits,
4471                        h,
4472                        h,
4473                    )
4474                    .map(|_| ());
4475                } else {
4476                    // The returned height corresponds to a checkpoint that is below the
4477                    // requested height. Inserting a checkpoint at a height *greater* than this
4478                    // returned height may cause an older checkpoint to be deleted, but that's
4479                    // fine, so we just fall through here.
4480                }
4481            }
4482            Err(SqliteClientError::RequestedRewindInvalid {
4483                safe_rewind_height, ..
4484            }) => {
4485                if let Some(min_checkpoint_height) = safe_rewind_height {
4486                    // The safe rewind height is at a position greater than the requested
4487                    // height, so we truncate wallet data and tree state to the earliest shared
4488                    // checkpoint. This removes blocks and transaction data above that height.
4489                    // Given that we always add checkpoints in pairs, if there are at least two
4490                    // checkpoints in any table then the minimum between them will result in
4491                    // checkpoints having been removed, and so there will be space for the
4492                    // checkpoint that is about to be inserted.
4493                    truncate_to_height_internal(
4494                        wdb.conn.0,
4495                        &wdb.params,
4496                        #[cfg(feature = "transparent-inputs")]
4497                        &wdb.gap_limits,
4498                        min_checkpoint_height,
4499                        min_checkpoint_height,
4500                    )?;
4501                } else {
4502                    // There are no checkpoints in either table; just continue.
4503                }
4504            }
4505            Err(e) => {
4506                return Err(e);
4507            }
4508        };
4509
4510        // Insert the frontier from the chain state, creating a checkpoint at the target
4511        // height.
4512        wdb.with_sapling_tree_mut(|tree| {
4513            tree.insert_frontier(
4514                chain_state.final_sapling_tree().clone(),
4515                Retention::Checkpoint {
4516                    id: target_height,
4517                    marking: Marking::None,
4518                },
4519            )
4520            .map_err(|error| SqliteClientError::TruncateCommitmentTree {
4521                pool: ShieldedPool::Sapling,
4522                height: target_height,
4523                error,
4524            })?;
4525            Ok::<_, SqliteClientError>(())
4526        })?;
4527
4528        #[cfg(feature = "orchard")]
4529        wdb.with_orchard_tree_mut(|tree| {
4530            tree.insert_frontier(
4531                chain_state.final_orchard_tree().clone(),
4532                Retention::Checkpoint {
4533                    id: target_height,
4534                    marking: Marking::None,
4535                },
4536            )
4537            .map_err(|error| SqliteClientError::TruncateCommitmentTree {
4538                pool: ShieldedPool::Orchard,
4539                height: target_height,
4540                error,
4541            })?;
4542            Ok::<_, SqliteClientError>(())
4543        })?;
4544        #[cfg(feature = "orchard")]
4545        wdb.with_ironwood_tree_mut(|tree| {
4546            tree.insert_frontier(
4547                chain_state.final_ironwood_tree().clone(),
4548                Retention::Checkpoint {
4549                    id: target_height,
4550                    marking: Marking::None,
4551                },
4552            )
4553            .map_err(|error| SqliteClientError::TruncateCommitmentTree {
4554                pool: ShieldedPool::Ironwood,
4555                height: target_height,
4556                error,
4557            })?;
4558            Ok::<_, SqliteClientError>(())
4559        })?;
4560    }
4561
4562    // Truncate wallet data to the target height. This always trims the scan queue so that
4563    // ranges above target_height are removed. When truncate_trees is true, it also truncates
4564    // blocks and note commitment trees (using the checkpoint created by the frontier insertion
4565    // above). We use truncate_to_height_internal directly (bypassing select_truncation_height)
4566    // because the frontier insertion created tree checkpoints at target_height but did not add
4567    // a blocks table entry, and select_truncation_height requires the height to be present in
4568    // the blocks table.
4569    let truncated_height = truncate_to_height_internal(
4570        wdb.conn.0,
4571        &wdb.params,
4572        #[cfg(feature = "transparent-inputs")]
4573        &wdb.gap_limits,
4574        target_height,
4575        target_height,
4576    )?;
4577
4578    assert_eq!(truncated_height, target_height);
4579
4580    Ok(())
4581}
4582
4583/// Rewinds the wallet to the specified chain state, preserving wallet data which has been
4584/// confirmed beyond the pruning depth, and resetting the birthday height of specified accounts
4585/// to the block following the chain state.
4586///
4587/// In contrast to [`truncate_to_chain_state`], which unconditionally removes wallet state above
4588/// `chain_state.block_height()` (transaction & note data is retained, but commitment trees,
4589/// blocks, etc. are removed to the truncation height), this rewinds blocks, note commitment
4590/// trees, transactions, transparent UTXO observations, and nullifier-map entries only as far
4591/// back as the pruning floor (`chain_tip - (PRUNING_DEPTH - 1)`). Data at or below that height
4592/// is preserved. Because `PRUNING_DEPTH` is a property of chain depth, the floor is derived
4593/// from the wallet's view of the chain tip rather than from `MAX(blocks.height)`.
4594///
4595/// The floor is clamped to an actual shard-tree checkpoint at or above the pruning floor —
4596/// the deepest such checkpoint retained by *any* pool (via
4597/// [`commitment_tree::min_checkpoint_id_at_or_above`]) — so that
4598/// [`truncate_to_height_internal`] has a real checkpoint to truncate to under non-contiguous
4599/// scan orders. A pool whose own checkpoints do not cover that height is handled by the
4600/// per-pool [`TreeTruncation`] classification: a tree with no checkpoint above the height is
4601/// left untouched, a tree whose checkpoints all lie above it is reset to its completed
4602/// subtree roots (with the requeued rescan re-creating the rest), and a tree whose
4603/// checkpoints straddle it without one at it is reported as corrupted.
4604///
4605/// The scan-queue range above the rewind target is overwritten with a `Historic` rescan range
4606/// extending up to the wallet's pre-rewind chain tip (computed from `MAX(block_range_end)` of
4607/// the scan queue prior to mutation). This forces re-scanning of any blocks above the rewind
4608/// target while preserving the wallet's view of the chain tip; existing scan-queue entries
4609/// with priority strictly greater than `Historic` (`ChainTip`, `OpenAdjacent`, `FoundNote`,
4610/// `Verify`) are preserved by the spanning-tree merge.
4611///
4612/// The birthday height is reset for an account in `reset_account_birthdays` only when the new
4613/// birthday (`chain_state.block_height() + 1`) is strictly less than the existing birthday
4614/// height; the existing birthday is never raised by this method.
4615///
4616/// Returns `Err(RewindError::RewindBeyondBirthdays(_))` only when `reset_account_birthdays` is
4617/// empty *and* every account in the wallet has a birthday greater than
4618/// `chain_state.block_height() + 1`. Returns `Err(RewindError::DataSource(_))` with a
4619/// `CorruptedData` payload if `reset_account_birthdays` contains any account UUID that is not
4620/// present in the wallet, or if a pool's note commitment tree retains checkpoints that
4621/// straddle the truncation height without one at it (see [`plan_tree_truncation`]); and with
4622/// a `RequestedRewindInvalid` payload if discarding a pool tree's scanned state would destroy
4623/// witness data for notes below the rewind target that the requeued rescan would not
4624/// re-create — a valid wallet state from which the requested rewind simply cannot be
4625/// executed.
4626pub(crate) fn rewind_to_chain_state<P: consensus::Parameters>(
4627    conn: &rusqlite::Transaction,
4628    params: &P,
4629    #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
4630    chain_state: &ChainState,
4631    reset_account_birthdays: HashSet<AccountUuid>,
4632) -> Result<(), RewindError<AccountUuid, SqliteClientError>> {
4633    // Load every account's birthday so we can validate `reset_account_birthdays` against the
4634    // wallet's accounts and check whether at least one existing birthday is at or below the
4635    // proposed new birthday floor.
4636    let account_birthdays: HashMap<AccountUuid, BlockHeight> = {
4637        let mut stmt = conn
4638            .prepare("SELECT uuid, birthday_height FROM accounts")
4639            .map_err(|e| RewindError::DataSource(e.into()))?;
4640
4641        let rows = stmt
4642            .query_map([], |row| {
4643                let uuid: Uuid = row.get(0)?;
4644                let h: u32 = row.get(1)?;
4645                Ok((AccountUuid(uuid), BlockHeight::from(h)))
4646            })
4647            .map_err(|e| RewindError::DataSource(e.into()))?;
4648
4649        rows.collect::<Result<HashMap<_, _>, _>>()
4650            .map_err(|e| RewindError::DataSource(e.into()))?
4651    };
4652
4653    let reset_valid = reset_account_birthdays
4654        .iter()
4655        .all(|uuid| account_birthdays.contains_key(uuid));
4656
4657    if !reset_valid {
4658        return Err(RewindError::DataSource(SqliteClientError::CorruptedData(
4659            "Account UUIDs provided for birthday reset do not exist in the wallet database."
4660                .to_string(),
4661        )));
4662    }
4663
4664    let target_height = chain_state.block_height();
4665    let new_birthday = target_height + 1;
4666    // An empty `reset_account_birthdays` is the caller's explicit assertion that the
4667    // requested rewind should not require lowering any account's birthday. Honor that:
4668    // if the rewind would land below every account's birthday — meaning the caller's
4669    // assertion is wrong — surface `RewindBeyondBirthdays` so the caller can decide
4670    // which accounts (if any) to acknowledge for lowering. A non-empty set is the
4671    // caller's acknowledgement that the listed accounts may be lowered.
4672    let birthday_reset_required =
4673        account_birthdays.values().all(|b| b > &new_birthday) && reset_account_birthdays.is_empty();
4674
4675    if birthday_reset_required {
4676        return Err(RewindError::RewindBeyondBirthdays(account_birthdays));
4677    }
4678
4679    // Capture the chain tip from the scan queue before any mutation; we use it as the upper
4680    // bound of the rescan range we install above the rewind target.
4681    let chain_tip = chain_tip_height(conn).map_err(|e| RewindError::DataSource(e.into()))?;
4682
4683    // Truncate wallet data above the pruning floor only when the target is below the wallet's
4684    // max scanned height; if the target is at or above the max scanned height, the wallet has
4685    // not yet scanned past the rewind point and there is nothing above it to remove.
4686    if let Some(max_scanned_height) = block_max_scanned(conn, params)
4687        .map_err(RewindError::DataSource)?
4688        .map(|m| m.block_height())
4689        && target_height < max_scanned_height
4690    {
4691        // Compute the floor height of the pruning window.
4692        let pruning_floor = max_scanned_height.saturating_sub(PRUNING_DEPTH - 1);
4693        let truncation_target = target_height.max(pruning_floor);
4694
4695        // Determine the height to which the note commitment trees can actually be truncated:
4696        // the deepest checkpoint at or above `truncation_target` retained by any pool. In a
4697        // fully-scanned wallet every pool retains the same checkpoint heights, so the floors
4698        // coincide; they diverge only when a pool's tree does not (yet) cover the window,
4699        // e.g. because a `*_shardtree` migration recently created its tables and the requeued
4700        // rescan has not caught up. `truncate_to_height_internal` classifies each pool
4701        // against the chosen height individually (see [`TreeTruncation`]), so a pool whose
4702        // checkpoints do not include that height is tolerated whenever the truncation leaves
4703        // its tree in a consistent state.
4704        let pool_table_prefixes: &[&'static str] = &[
4705            crate::SAPLING_TABLES_PREFIX,
4706            #[cfg(feature = "orchard")]
4707            crate::ORCHARD_TABLES_PREFIX,
4708            #[cfg(feature = "orchard")]
4709            crate::IRONWOOD_TABLES_PREFIX,
4710        ];
4711        let mut window_floor: Option<BlockHeight> = None;
4712        for &table_prefix in pool_table_prefixes {
4713            let pool_floor = commitment_tree::min_checkpoint_id_at_or_above(
4714                conn,
4715                table_prefix,
4716                truncation_target,
4717            )
4718            .map_err(ShardTreeError::Storage)
4719            .map_err(SqliteClientError::from)
4720            .map_err(RewindError::DataSource)?;
4721            window_floor = window_floor.into_iter().chain(pool_floor).min();
4722        }
4723
4724        let truncation_height = window_floor.unwrap_or(pruning_floor);
4725
4726        // Use `truncate_to_height_internal` to perform full truncation of data within the
4727        // pruning window. Blocks above `target_height` are re-scanned by the `Historic`
4728        // range installed below, so `target_height` is the floor below which tree state must
4729        // be preserved.
4730        truncate_to_height_internal(
4731            conn,
4732            params,
4733            #[cfg(feature = "transparent-inputs")]
4734            gap_limits,
4735            truncation_height,
4736            target_height,
4737        )
4738        .map_err(RewindError::DataSource)?;
4739    }
4740
4741    // Overwrite the scan-queue range above the rewind target with a `Historic` rescan range,
4742    // forcing re-scan of any blocks that previously appeared above the target. This both
4743    // re-queues the blocks above the truncation floor (which truncate_to_height_internal
4744    // already trimmed) and overrides any `Scanned`/`Historic` entries in the
4745    // `(target_height, truncation_height]` window that survived a deep rewind, so the sync
4746    // loop will re-scan them. With `force_rescans = true` the only entries this preserves are
4747    // those whose priority would dominate `Historic` even under a forced rescan
4748    // (`ChainTip`, `OpenAdjacent`, `FoundNote`, `Verify`); `Ignored` is the lowest priority
4749    // and cannot overwrite anything.
4750    if let Some(t) = chain_tip
4751        && target_height < t
4752    {
4753        let rescan_range = (target_height + 1)..(t + 1);
4754        replace_queue_entries::<SqliteClientError>(
4755            conn,
4756            &rescan_range,
4757            std::iter::once(ScanRange::from_parts(
4758                rescan_range.clone(),
4759                ScanPriority::Historic,
4760            )),
4761            true,
4762        )
4763        .map_err(RewindError::DataSource)?;
4764    }
4765
4766    let new_sapling_tree_size: u64 = chain_state.final_sapling_tree().tree_size();
4767    #[cfg(feature = "orchard")]
4768    let new_orchard_tree_size = Some(chain_state.final_orchard_tree().tree_size());
4769    #[cfg(not(feature = "orchard"))]
4770    let new_orchard_tree_size: Option<u64> = None;
4771
4772    for uuid in &reset_account_birthdays {
4773        conn.execute(
4774            "UPDATE accounts
4775             SET birthday_height = :new_birthday,
4776                 birthday_sapling_tree_size = :new_sapling_tree_size,
4777                 birthday_orchard_tree_size = :new_orchard_tree_size
4778             WHERE uuid = :uuid AND birthday_height > :new_birthday",
4779            named_params![
4780                ":new_birthday": u32::from(new_birthday),
4781                ":new_sapling_tree_size": new_sapling_tree_size,
4782                ":new_orchard_tree_size": new_orchard_tree_size,
4783                ":uuid": uuid.0,
4784            ],
4785        )
4786        .map_err(|e| RewindError::DataSource(e.into()))?;
4787    }
4788
4789    Ok(())
4790}
4791
4792/// Trims the `scan_queue` so that no range extends above `max_height`.
4793///
4794/// Deletes any range whose start is above `max_height`, and clamps the upper bound of any
4795/// remaining range that extends past `max_height`. Used by [`truncate_to_height_internal`] to
4796/// remove scan-queue entries above the truncation height.
4797pub(crate) fn trim_scan_queue_to(
4798    conn: &rusqlite::Transaction,
4799    max_height: BlockHeight,
4800) -> Result<(), SqliteClientError> {
4801    let new_end_height = u32::from(max_height + 1);
4802    conn.execute(
4803        "DELETE FROM scan_queue
4804         WHERE block_range_start >= :new_end_height",
4805        named_params![":new_end_height": new_end_height],
4806    )?;
4807    conn.execute(
4808        "UPDATE scan_queue
4809         SET block_range_end = :new_end_height
4810         WHERE block_range_end > :new_end_height",
4811        named_params![":new_end_height": new_end_height],
4812    )?;
4813    Ok(())
4814}
4815
4816/// Returns a vector with the IDs of all accounts known to this wallet.
4817///
4818/// Note that this is called from db migration code.
4819pub(crate) fn get_account_ids(
4820    conn: &rusqlite::Connection,
4821) -> Result<Vec<AccountUuid>, rusqlite::Error> {
4822    let mut stmt = conn.prepare("SELECT uuid FROM accounts")?;
4823    let mut rows = stmt.query([])?;
4824    let mut result = Vec::new();
4825    while let Some(row) = rows.next()? {
4826        let id = AccountUuid(row.get(0)?);
4827        result.push(id);
4828    }
4829    Ok(result)
4830}
4831
4832/// Inserts information about a scanned block into the database.
4833#[allow(clippy::too_many_arguments)]
4834pub(crate) fn put_block(
4835    conn: &rusqlite::Transaction<'_>,
4836    block_height: BlockHeight,
4837    block_hash: BlockHash,
4838    block_time: u32,
4839    sapling_commitment_tree_size: u32,
4840    sapling_output_count: u32,
4841    #[cfg(feature = "orchard")] orchard_commitment_tree_size: u32,
4842    #[cfg(feature = "orchard")] orchard_action_count: u32,
4843    #[cfg(feature = "orchard")] ironwood_commitment_tree_size: u32,
4844    #[cfg(feature = "orchard")] ironwood_action_count: u32,
4845) -> Result<(), SqliteClientError> {
4846    let block_hash_data = conn
4847        .query_row(
4848            "SELECT hash FROM blocks WHERE height = ?",
4849            [u32::from(block_height)],
4850            |row| row.get::<_, Vec<u8>>(0),
4851        )
4852        .optional()?;
4853
4854    // Ensure that in the case of an upsert, we don't overwrite block data
4855    // with information for a block with a different hash.
4856    if let Some(bytes) = block_hash_data {
4857        let expected_hash = BlockHash::try_from_slice(&bytes).ok_or_else(|| {
4858            SqliteClientError::CorruptedData(format!(
4859                "Invalid block hash at height {}",
4860                u32::from(block_height)
4861            ))
4862        })?;
4863        if expected_hash != block_hash {
4864            return Err(SqliteClientError::BlockConflict(block_height));
4865        }
4866    }
4867
4868    let mut stmt_upsert_block = conn.prepare_cached(
4869        "INSERT INTO blocks (
4870            height,
4871            hash,
4872            time,
4873            sapling_commitment_tree_size,
4874            sapling_output_count,
4875            sapling_tree,
4876            orchard_commitment_tree_size,
4877            orchard_action_count,
4878            ironwood_commitment_tree_size,
4879            ironwood_action_count
4880        )
4881        VALUES (
4882            :height,
4883            :hash,
4884            :block_time,
4885            :sapling_commitment_tree_size,
4886            :sapling_output_count,
4887            x'00',
4888            :orchard_commitment_tree_size,
4889            :orchard_action_count,
4890            :ironwood_commitment_tree_size,
4891            :ironwood_action_count
4892        )
4893        ON CONFLICT (height) DO UPDATE
4894        SET hash = :hash,
4895            time = :block_time,
4896            sapling_commitment_tree_size = :sapling_commitment_tree_size,
4897            sapling_output_count = :sapling_output_count,
4898            orchard_commitment_tree_size = :orchard_commitment_tree_size,
4899            orchard_action_count = :orchard_action_count,
4900            ironwood_commitment_tree_size = :ironwood_commitment_tree_size,
4901            ironwood_action_count = :ironwood_action_count",
4902    )?;
4903
4904    #[cfg(not(feature = "orchard"))]
4905    let orchard_commitment_tree_size: Option<u32> = None;
4906    #[cfg(not(feature = "orchard"))]
4907    let orchard_action_count: Option<u32> = None;
4908    #[cfg(not(feature = "orchard"))]
4909    let ironwood_commitment_tree_size: Option<u32> = None;
4910    #[cfg(not(feature = "orchard"))]
4911    let ironwood_action_count: Option<u32> = None;
4912
4913    stmt_upsert_block.execute(named_params![
4914        ":height": u32::from(block_height),
4915        ":hash": &block_hash.0[..],
4916        ":block_time": block_time,
4917        ":sapling_commitment_tree_size": sapling_commitment_tree_size,
4918        ":sapling_output_count": sapling_output_count,
4919        ":orchard_commitment_tree_size": orchard_commitment_tree_size,
4920        ":orchard_action_count": orchard_action_count,
4921        ":ironwood_commitment_tree_size": ironwood_commitment_tree_size,
4922        ":ironwood_action_count": ironwood_action_count,
4923    ])?;
4924
4925    // If we now have a block corresponding to a received transparent output that had not been
4926    // scanned at the time the UTXO was discovered, update the associated transaction record to
4927    // refer to that block.
4928    //
4929    // NOTE: There's a small data corruption hazard here, in that we're relying exclusively upon
4930    // the block height to associate the transaction to the block. This is because CompactBlock
4931    // values only contain CompactTx entries for transactions that contain shielded inputs or
4932    // outputs, and the GetAddressUtxosReply data does not contain the block hash. As such, it's
4933    // necessary to ensure that any chain rollback to below the received height causes that height
4934    // to be set to NULL.
4935    let mut stmt_update_transaction_block_reference = conn.prepare_cached(
4936        "UPDATE transactions
4937         SET block = :height
4938         WHERE mined_height = :height",
4939    )?;
4940
4941    stmt_update_transaction_block_reference
4942        .execute(named_params![":height": u32::from(block_height),])?;
4943
4944    Ok(())
4945}
4946
4947pub(crate) fn get_txs_spending_transparent_outputs_of<P: consensus::Parameters>(
4948    conn: &rusqlite::Connection,
4949    params: &P,
4950    tx_ref: TxRef,
4951) -> Result<Vec<(TxRef, Transaction)>, SqliteClientError> {
4952    // For each transaction that spends a transparent output of this transaction and does not
4953    // already have a known fee value.
4954    let mut spending_txs_stmt = conn.prepare(
4955        "SELECT DISTINCT t.id_tx, t.raw, t.mined_height, t.expiry_height
4956         FROM transactions t
4957         -- find transactions that spend transparent outputs of the decrypted tx
4958         LEFT OUTER JOIN transparent_received_output_spends ts
4959            ON ts.transaction_id = t.id_tx
4960         LEFT OUTER JOIN transparent_received_outputs tro
4961            ON tro.transaction_id = :transaction_id
4962            AND tro.id = ts.transparent_received_output_id
4963         WHERE t.fee IS NULL
4964         AND t.raw IS NOT NULL
4965         AND ts.transaction_id IS NOT NULL",
4966    )?;
4967
4968    spending_txs_stmt
4969        .query_and_then(named_params![":transaction_id": tx_ref.0], |row| {
4970            let spending_tx_ref = row.get(0).map(TxRef)?;
4971            let tx_bytes: Vec<u8> = row.get(1)?;
4972            let block: Option<u32> = row.get(2)?;
4973            let expiry: Option<u32> = row.get(3)?;
4974
4975            let (_, spending_tx) = parse_tx(
4976                params,
4977                &tx_bytes,
4978                block.map(BlockHeight::from),
4979                expiry.map(BlockHeight::from),
4980            )?;
4981
4982            Ok((spending_tx_ref, spending_tx))
4983        })?
4984        .collect()
4985}
4986
4987pub(crate) fn update_tx_fee(
4988    conn: &rusqlite::Transaction<'_>,
4989    tx_ref: TxRef,
4990    fee: zcash_protocol::value::Zatoshis,
4991) -> Result<(), SqliteClientError> {
4992    conn.execute(
4993        "UPDATE transactions
4994         SET fee = :fee
4995         WHERE id_tx = :transaction_id",
4996        named_params! {
4997            ":transaction_id": tx_ref.0,
4998            ":fee": u64::from(fee)
4999        },
5000    )?;
5001
5002    Ok(())
5003}
5004
5005pub(crate) fn set_tx_trust(
5006    conn: &rusqlite::Transaction,
5007    txid: TxId,
5008    trusted: bool,
5009) -> Result<(), SqliteClientError> {
5010    conn.execute(
5011        "UPDATE transactions
5012         SET trust_status = :trust_status
5013         WHERE txid = :txid",
5014        named_params! {
5015           ":txid": &txid.as_ref()[..],
5016           ":trust_status": trusted
5017        },
5018    )?;
5019
5020    Ok(())
5021}
5022
5023/// Inserts information about a mined transaction that was observed to
5024/// contain a note related to this wallet into the database.
5025pub(crate) fn put_tx_meta(
5026    conn: &rusqlite::Connection,
5027    tx: &WalletTx<AccountUuid>,
5028    height: BlockHeight,
5029) -> Result<TxRef, SqliteClientError> {
5030    // It isn't there, so insert our transaction into the database.
5031    let mut stmt_upsert_tx_meta = conn.prepare_cached(
5032        "INSERT INTO transactions (txid, block, mined_height, tx_index, min_observed_height)
5033        VALUES (:txid, :block, :block, :tx_index, :block)
5034        ON CONFLICT (txid) DO UPDATE
5035        SET block = :block,
5036            mined_height = :block,
5037            tx_index = :tx_index,
5038            min_observed_height = MIN(min_observed_height, :block),
5039            confirmed_unmined_at_height = NULL
5040        RETURNING id_tx",
5041    )?;
5042
5043    let txid_bytes = tx.txid();
5044    let tx_params = named_params![
5045        ":txid": &txid_bytes.as_ref()[..],
5046        ":block": u32::from(height),
5047        ":tx_index": u16::from(tx.block_index()),
5048    ];
5049
5050    stmt_upsert_tx_meta
5051        .query_row(tx_params, |row| row.get::<_, i64>(0).map(TxRef))
5052        .map_err(SqliteClientError::from)
5053}
5054
5055/// Returns the most likely wallet address that corresponds to the protocol-level receiver of a
5056/// note or UTXO.
5057pub(crate) fn select_receiving_address<P: consensus::Parameters>(
5058    conn: &rusqlite::Connection,
5059    _params: &P,
5060    account: AccountUuid,
5061    receiver: &Receiver,
5062) -> Result<Option<ZcashAddress>, SqliteClientError> {
5063    match receiver {
5064        #[cfg(feature = "transparent-inputs")]
5065        Receiver::Transparent(taddr) => conn
5066            .query_row(
5067                "SELECT address
5068                 FROM addresses
5069                 WHERE cached_transparent_receiver_address = :taddr",
5070                named_params! {
5071                    ":taddr": Address::Transparent(*taddr).encode(_params)
5072                },
5073                |row| row.get::<_, String>(0),
5074            )
5075            .optional()?
5076            .map(|addr_str| addr_str.parse::<ZcashAddress>())
5077            .transpose()
5078            .map_err(SqliteClientError::from),
5079        receiver => {
5080            let mut stmt = conn.prepare_cached(
5081                "SELECT address
5082                 FROM addresses
5083                 JOIN accounts ON accounts.id = addresses.account_id
5084                 WHERE accounts.uuid = :account_uuid
5085                 AND key_scope = :key_scope",
5086            )?;
5087
5088            let mut result = stmt.query(named_params! {
5089                ":account_uuid": account.0,
5090                ":key_scope": KeyScope::EXTERNAL.encode(),
5091            })?;
5092            while let Some(row) = result.next()? {
5093                let addr_str = row.get::<_, String>(0)?;
5094                let decoded = addr_str.parse::<ZcashAddress>()?;
5095                if receiver.corresponds(&decoded) {
5096                    return Ok(Some(decoded));
5097                }
5098            }
5099
5100            Ok(None)
5101        }
5102    }
5103}
5104
5105/// Inserts full transaction data into the database.
5106pub(crate) fn put_tx_data(
5107    conn: &rusqlite::Connection,
5108    tx: &Transaction,
5109    fee: Option<Zatoshis>,
5110    created_at: Option<time::OffsetDateTime>,
5111    target_height: Option<TargetHeight>,
5112    observed_height: BlockHeight,
5113) -> Result<TxRef, SqliteClientError> {
5114    let mut stmt_upsert_tx_data = conn.prepare_cached(
5115        "INSERT INTO transactions (txid, tx_index, created, expiry_height, raw, fee, target_height, min_observed_height)
5116        VALUES (:txid, :tx_index, :created_at, :expiry_height, :raw, :fee, :target_height, :observed_height)
5117        ON CONFLICT (txid) DO UPDATE
5118        SET expiry_height = :expiry_height,
5119            raw = :raw,
5120            fee = IFNULL(:fee, fee),
5121            tx_index = IFNULL(tx_index, :tx_index),
5122            min_observed_height = MIN(
5123                min_observed_height,
5124                :observed_height
5125            )
5126        RETURNING id_tx",
5127    )?;
5128
5129    let txid = tx.txid();
5130    let mut raw_tx = vec![];
5131    tx.write(&mut raw_tx)?;
5132
5133    let tx_index = tx
5134        .transparent_bundle()
5135        .and_then(|bundle| bundle.is_coinbase().then_some(0i64));
5136
5137    let tx_params = named_params![
5138        ":txid": &txid.as_ref()[..],
5139        ":tx_index": tx_index,
5140        ":created_at": created_at,
5141        ":expiry_height": u32::from(tx.expiry_height()),
5142        ":raw": raw_tx,
5143        ":fee": fee.map(u64::from),
5144        ":target_height": target_height.map(u32::from),
5145        ":observed_height": u32::from(observed_height)
5146    ];
5147
5148    stmt_upsert_tx_data
5149        .query_row(tx_params, |row| row.get::<_, i64>(0).map(TxRef))
5150        .map_err(SqliteClientError::from)
5151}
5152
5153/// Records how a transaction classifies against ZIP 318.
5154///
5155/// The column defaults to the code for "not classified", so a row this was never called for
5156/// reports as unclassified rather than as a decision that the transaction is not a migration
5157/// transaction. Rows written before this column existed keep that default, and need the
5158/// transaction rescanned before they can be labelled.
5159pub(crate) fn put_zip318_classification(
5160    conn: &rusqlite::Connection,
5161    tx_ref: TxRef,
5162    classification: zcash_protocol::zip318::Zip318Classification,
5163) -> Result<(), SqliteClientError> {
5164    conn.execute(
5165        "UPDATE transactions SET zip318_kind = :zip318_kind WHERE id_tx = :id_tx",
5166        named_params![
5167            ":zip318_kind": classification.to_code(),
5168            ":id_tx": tx_ref.0,
5169        ],
5170    )?;
5171
5172    Ok(())
5173}
5174
5175#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5176pub(crate) enum TxQueryType {
5177    Status,
5178    Enhancement,
5179}
5180
5181impl TxQueryType {
5182    pub(crate) fn code(&self) -> i64 {
5183        match self {
5184            TxQueryType::Status => 0,
5185            TxQueryType::Enhancement => 1,
5186        }
5187    }
5188
5189    pub(crate) fn from_code(code: i64) -> Option<Self> {
5190        match code {
5191            0 => Some(TxQueryType::Status),
5192            1 => Some(TxQueryType::Enhancement),
5193            _ => None,
5194        }
5195    }
5196}
5197
5198#[cfg(feature = "transparent-inputs")]
5199pub(crate) fn queue_transparent_input_retrieval<AccountId>(
5200    conn: &rusqlite::Transaction<'_>,
5201    tx_ref: TxRef,
5202    d_tx: &DecryptedTransaction<Transaction, AccountId>,
5203) -> Result<(), SqliteClientError> {
5204    if let Some(b) = d_tx.tx().transparent_bundle()
5205        && !b.is_coinbase()
5206    {
5207        // queue the transparent inputs for enhancement
5208        queue_tx_retrieval(
5209            conn,
5210            b.vin.iter().map(|txin| *txin.prevout().txid()),
5211            Some(tx_ref),
5212        )?;
5213    }
5214
5215    Ok(())
5216}
5217
5218pub(crate) fn queue_tx_retrieval(
5219    conn: &rusqlite::Transaction<'_>,
5220    txids: impl Iterator<Item = TxId>,
5221    dependent_tx_ref: Option<TxRef>,
5222) -> Result<(), SqliteClientError> {
5223    // This operation represents enhancement intent only. If complete transaction data is already
5224    // present, no request is needed. In particular, the presence of raw data must not implicitly
5225    // turn an enhancement request into a status request.
5226    let mut stmt_insert_tx = conn.prepare_cached(
5227        "INSERT INTO tx_retrieval_queue (txid, query_type, dependent_transaction_id)
5228         SELECT
5229            :txid,
5230            :enhancement_type,
5231            :dependent_transaction_id
5232         WHERE NOT EXISTS (
5233            SELECT 1 FROM transactions WHERE txid = :txid AND raw IS NOT NULL
5234         )
5235        ON CONFLICT (txid, query_type) DO UPDATE
5236        SET dependent_transaction_id =
5237            IFNULL(:dependent_transaction_id, dependent_transaction_id)",
5238    )?;
5239
5240    for txid in txids {
5241        stmt_insert_tx.execute(named_params! {
5242            ":txid": txid.as_ref(),
5243            ":enhancement_type": TxQueryType::Enhancement.code(),
5244            ":dependent_transaction_id": dependent_tx_ref.map(|r| r.0),
5245        })?;
5246    }
5247
5248    Ok(())
5249}
5250
5251/// Records that the wallet must query by txid in order to learn the mined status of a
5252/// transaction. The entry is durable across mined states so that it can become active again
5253/// following a chain rewind.
5254pub(crate) fn queue_tx_status(
5255    conn: &rusqlite::Transaction<'_>,
5256    txid: TxId,
5257) -> Result<(), SqliteClientError> {
5258    conn.execute(
5259        "INSERT INTO tx_retrieval_queue (txid, query_type)
5260         VALUES (:txid, :status_type)
5261         ON CONFLICT (txid, query_type) DO NOTHING",
5262        named_params![
5263            ":txid": txid.as_ref(),
5264            ":status_type": TxQueryType::Status.code(),
5265        ],
5266    )?;
5267
5268    Ok(())
5269}
5270
5271/// Returns the vector of [`TransactionDataRequest`]s that represents the information needed by the
5272/// wallet backend in order to be able to present a complete view of wallet history and memo data.
5273pub(crate) fn transaction_data_requests(
5274    conn: &rusqlite::Connection,
5275) -> Result<Vec<TransactionDataRequest>, SqliteClientError> {
5276    let mut tx_retrieval_stmt = conn.prepare_cached(
5277        "SELECT q.txid, q.query_type
5278         FROM tx_retrieval_queue q
5279         LEFT JOIN transactions t ON t.txid = q.txid
5280         WHERE q.query_type = :enhancement_type
5281         OR (
5282            q.query_type = :status_type
5283            AND t.mined_height IS NULL
5284            AND (
5285                t.confirmed_unmined_at_height IS NULL
5286                OR t.expiry_height = 0
5287                OR (
5288                    t.expiry_height > 0
5289                    AND t.confirmed_unmined_at_height < t.expiry_height
5290                )
5291                OR (
5292                    t.expiry_height IS NULL
5293                    AND t.confirmed_unmined_at_height
5294                        < t.min_observed_height + :certainty_depth
5295                )
5296            )
5297         )",
5298    )?;
5299
5300    let result = tx_retrieval_stmt
5301        .query_and_then(
5302            named_params![
5303                ":status_type": TxQueryType::Status.code(),
5304                ":enhancement_type": TxQueryType::Enhancement.code(),
5305                ":certainty_depth": PRUNING_DEPTH + DEFAULT_TX_EXPIRY_DELTA
5306            ],
5307            |row| {
5308                let txid = row.get(0).map(TxId::from_bytes)?;
5309                let query_type = row.get(1).map(TxQueryType::from_code)?.ok_or_else(|| {
5310                    SqliteClientError::CorruptedData(
5311                        "Unrecognized transaction data request type.".to_owned(),
5312                    )
5313                })?;
5314
5315                Ok::<TransactionDataRequest, SqliteClientError>(match query_type {
5316                    TxQueryType::Status => TransactionDataRequest::GetStatus(txid),
5317                    TxQueryType::Enhancement => TransactionDataRequest::Enhancement(txid),
5318                })
5319            },
5320        )?
5321        .collect::<Result<Vec<_>, _>>()?;
5322
5323    Ok(result)
5324}
5325
5326pub(crate) fn delete_retrieval_queue_entries(
5327    conn: &rusqlite::Transaction<'_>,
5328    txid: TxId,
5329) -> Result<(), SqliteClientError> {
5330    delete_retrieval_queue_entry(conn, txid, TxQueryType::Enhancement)
5331}
5332
5333fn delete_retrieval_queue_entry(
5334    conn: &rusqlite::Transaction<'_>,
5335    txid: TxId,
5336    query_type: TxQueryType,
5337) -> Result<(), SqliteClientError> {
5338    conn.execute(
5339        "DELETE FROM tx_retrieval_queue
5340         WHERE txid = :txid
5341         AND query_type = :query_type",
5342        named_params![
5343            ":txid": txid.as_ref(),
5344            ":query_type": query_type.code(),
5345        ],
5346    )?;
5347
5348    Ok(())
5349}
5350
5351// A utility function for creation of parameters for use in `insert_sent_output`
5352// and `put_sent_output`
5353fn recipient_params<P: consensus::Parameters>(
5354    conn: &Connection,
5355    _params: &P,
5356    from: AccountUuid,
5357    to: &Recipient<AccountUuid>,
5358) -> Result<(AccountRef, Option<String>, Option<AccountRef>, PoolType), SqliteClientError> {
5359    let from_account_id = get_account_ref(conn, from)?;
5360    match to {
5361        Recipient::External {
5362            recipient_address,
5363            output_pool,
5364            ..
5365        } => Ok((
5366            from_account_id,
5367            Some(recipient_address.encode()),
5368            None,
5369            *output_pool,
5370        )),
5371        #[cfg(feature = "transparent-inputs")]
5372        Recipient::EphemeralTransparent {
5373            receiving_account,
5374            ephemeral_address,
5375            ..
5376        } => {
5377            let to_account = get_account_ref(conn, *receiving_account)?;
5378            Ok((
5379                from_account_id,
5380                Some(ephemeral_address.encode(_params)),
5381                Some(to_account),
5382                PoolType::TRANSPARENT,
5383            ))
5384        }
5385        #[cfg(feature = "transparent-inputs")]
5386        Recipient::InternalTransparent {
5387            receiving_account,
5388            recipient_address,
5389        } => {
5390            let to_account = get_account_ref(conn, *receiving_account)?;
5391            Ok((
5392                from_account_id,
5393                Some(recipient_address.encode(_params)),
5394                Some(to_account),
5395                PoolType::TRANSPARENT,
5396            ))
5397        }
5398        Recipient::InternalShielded {
5399            receiving_account,
5400            external_address,
5401            note,
5402        } => {
5403            let to_account = get_account_ref(conn, *receiving_account)?;
5404            Ok((
5405                from_account_id,
5406                external_address.as_ref().map(|a| a.encode()),
5407                Some(to_account),
5408                PoolType::Shielded(note.pool()),
5409            ))
5410        }
5411    }
5412}
5413
5414fn flag_previously_received_change(
5415    conn: &rusqlite::Transaction,
5416    tx_ref: TxRef,
5417) -> Result<(), SqliteClientError> {
5418    let flag_received_change = |protocol| {
5419        let TableConstants { table_prefix, .. } = table_constants::<SqliteClientError>(protocol)?;
5420        conn.execute(
5421            &format!(
5422                "UPDATE {table_prefix}_received_notes
5423                 SET is_change = 1
5424                 FROM sent_notes sn
5425                 WHERE sn.transaction_id = {table_prefix}_received_notes.transaction_id
5426                 AND sn.transaction_id = :transaction_id
5427                 AND sn.from_account_id = {table_prefix}_received_notes.account_id
5428                 AND {table_prefix}_received_notes.recipient_key_scope = :internal_scope"
5429            ),
5430            named_params! {
5431                ":transaction_id": tx_ref.0,
5432                ":internal_scope": KeyScope::INTERNAL.encode()
5433            },
5434        )
5435        .map_err(SqliteClientError::from)
5436    };
5437
5438    // Every pool with a `{prefix}_received_notes` table must appear here. Omitting one is not
5439    // merely a missed opportunity to set the flag at this call: `is_change` is only ever
5440    // raised, never lowered, and nothing revisits the row afterwards, so for any note whose
5441    // spends were not linkable to the wallet at the time it was scanned the omission is
5442    // permanent.
5443    flag_received_change(ShieldedPool::Sapling)?;
5444    #[cfg(feature = "orchard")]
5445    flag_received_change(ShieldedPool::Orchard)?;
5446    #[cfg(feature = "orchard")]
5447    flag_received_change(ShieldedPool::Ironwood)?;
5448
5449    Ok(())
5450}
5451
5452/// Records information about a transaction output that your wallet created.
5453pub(crate) fn insert_sent_output<P: consensus::Parameters>(
5454    conn: &rusqlite::Transaction,
5455    params: &P,
5456    tx_ref: TxRef,
5457    from_account_uuid: AccountUuid,
5458    output: &SentTransactionOutput<AccountUuid>,
5459) -> Result<(), SqliteClientError> {
5460    let mut stmt_insert_sent_output = conn.prepare_cached(
5461        "INSERT INTO sent_notes (
5462            transaction_id, output_pool, output_index, from_account_id,
5463            to_address, to_account_id, value, memo)
5464         VALUES (
5465            :transaction_id, :output_pool, :output_index, :from_account_id,
5466            :to_address, :to_account_id, :value, :memo)",
5467    )?;
5468
5469    let (from_account_id, to_address, to_account_id, pool_type) =
5470        recipient_params(conn, params, from_account_uuid, output.recipient())?;
5471    let sql_args = named_params![
5472        ":transaction_id": tx_ref.0,
5473        ":output_pool": &pool_code(pool_type),
5474        ":output_index": &i64::try_from(output.output_index()).unwrap(),
5475        ":from_account_id": from_account_id.0,
5476        ":to_address": &to_address,
5477        ":to_account_id": to_account_id.map(|a| a.0),
5478        ":value": &i64::from(ZatBalance::from(output.value())),
5479        ":memo": memo_repr(output.memo())
5480    ];
5481
5482    stmt_insert_sent_output.execute(sql_args)?;
5483    flag_previously_received_change(conn, tx_ref)?;
5484
5485    Ok(())
5486}
5487
5488/// Records information about a transaction output that your wallet created, from the constituent
5489/// properties of that output.
5490///
5491/// - If `recipient` is a Unified address, `output_index` is an index into the outputs of the
5492///   transaction within the bundle associated with the recipient's output pool.
5493/// - If `recipient` is a Sapling address, `output_index` is an index into the Sapling outputs of
5494///   the transaction.
5495/// - If `recipient` is a transparent address, `output_index` is an index into the transparent
5496///   outputs of the transaction.
5497/// - If `recipient` is an internal account, `output_index` is an index into the outputs of
5498///   the transaction in the transaction bundle corresponding to the recipient pool.
5499#[allow(clippy::too_many_arguments)]
5500pub(crate) fn put_sent_output<P: consensus::Parameters>(
5501    conn: &rusqlite::Transaction,
5502    params: &P,
5503    from_account_uuid: AccountUuid,
5504    tx_ref: TxRef,
5505    output_index: usize,
5506    recipient: &Recipient<AccountUuid>,
5507    value: Zatoshis,
5508    memo: Option<&MemoBytes>,
5509) -> Result<(), SqliteClientError> {
5510    let mut stmt_upsert_sent_output = conn.prepare_cached(
5511        "INSERT INTO sent_notes (
5512            transaction_id, output_pool, output_index, from_account_id,
5513            to_address, to_account_id, value, memo)
5514        VALUES (
5515            :transaction_id, :output_pool, :output_index, :from_account_id,
5516            :to_address, :to_account_id, :value, :memo)
5517        ON CONFLICT (transaction_id, output_pool, output_index) DO UPDATE
5518        SET from_account_id = :from_account_id,
5519            to_address = IFNULL(to_address, :to_address),
5520            to_account_id = IFNULL(to_account_id, :to_account_id),
5521            value = :value,
5522            memo = IFNULL(:memo, memo)",
5523    )?;
5524
5525    let (from_account_id, to_address, to_account_id, pool_type) =
5526        recipient_params(conn, params, from_account_uuid, recipient)?;
5527    let sql_args = named_params![
5528        ":transaction_id": tx_ref.0,
5529        ":output_pool": &pool_code(pool_type),
5530        ":output_index": &i64::try_from(output_index).unwrap(),
5531        ":from_account_id": from_account_id.0,
5532        ":to_address": &to_address,
5533        ":to_account_id": &to_account_id.map(|a| a.0),
5534        ":value": &i64::from(ZatBalance::from(value)),
5535        ":memo": memo_repr(memo)
5536    ];
5537
5538    stmt_upsert_sent_output.execute(sql_args)?;
5539    flag_previously_received_change(conn, tx_ref)?;
5540
5541    Ok(())
5542}
5543
5544/// Inserts the given entries into the nullifier map.
5545///
5546/// Returns an error if the new entries conflict with existing ones. This indicates either
5547/// corrupted data, or that a reorg has occurred and the caller needs to repair the wallet
5548/// state with [`truncate_to_height`].
5549pub(crate) fn insert_nullifier_map<N: AsRef<[u8]>>(
5550    conn: &rusqlite::Transaction<'_>,
5551    block_height: BlockHeight,
5552    spend_pool: ShieldedPool,
5553    new_entries: &[(TxIndex, TxId, Vec<N>)],
5554) -> Result<(), SqliteClientError> {
5555    let mut stmt_select_tx_locators = conn.prepare_cached(
5556        "SELECT block_height, tx_index, txid
5557        FROM tx_locator_map
5558        WHERE (block_height = :block_height AND tx_index = :tx_index) OR txid = :txid",
5559    )?;
5560    let mut stmt_insert_tx_locator = conn.prepare_cached(
5561        "INSERT INTO tx_locator_map
5562        (block_height, tx_index, txid)
5563        VALUES (:block_height, :tx_index, :txid)",
5564    )?;
5565    let mut stmt_insert_nullifier_mapping = conn.prepare_cached(
5566        "INSERT INTO nullifier_map
5567        (spend_pool, nf, block_height, tx_index)
5568        VALUES (:spend_pool, :nf, :block_height, :tx_index)
5569        ON CONFLICT (spend_pool, nf) DO UPDATE
5570        SET block_height = :block_height,
5571            tx_index = :tx_index",
5572    )?;
5573
5574    for (tx_index, txid, nullifiers) in new_entries {
5575        let tx_args = named_params![
5576            ":block_height": u32::from(block_height),
5577            ":tx_index": u16::from(*tx_index),
5578            ":txid": txid.as_ref(),
5579        ];
5580
5581        // We cannot use an upsert here, because we use the tx locator as the foreign key
5582        // in `nullifier_map` instead of `txid` for database size efficiency. If an insert
5583        // into `tx_locator_map` were to conflict, we would need the resulting update to
5584        // cascade into `nullifier_map` as either:
5585        // - an update (if a transaction moved within a block), or
5586        // - a deletion (if the locator now points to a different transaction).
5587        //
5588        // `ON UPDATE` has `CASCADE` to always update, but has no deletion option. So we
5589        // instead set `ON UPDATE RESTRICT` on the foreign key relation, and require the
5590        // caller to manually rewind the database in this situation.
5591        let locator = stmt_select_tx_locators
5592            .query_map(tx_args, |row| {
5593                Ok((
5594                    BlockHeight::from_u32(row.get(0)?),
5595                    TxIndex::from(row.get::<_, u16>(1)?),
5596                    TxId::from_bytes(row.get(2)?),
5597                ))
5598            })?
5599            .try_fold(None, |acc, row| -> Result<_, SqliteClientError> {
5600                match (acc, row?) {
5601                    (None, rhs) => Ok(Some(Some(rhs))),
5602                    // If there was more than one row, then due to the uniqueness
5603                    // constraints on the `tx_locator_map` table, all of the rows conflict
5604                    // with the locator being inserted.
5605                    (Some(_), _) => Ok(Some(None)),
5606                }
5607            })?;
5608
5609        match locator {
5610            // If the locator in the table matches the one being inserted, do nothing.
5611            Some(Some(loc)) if loc == (block_height, *tx_index, *txid) => (),
5612            // If the locator being inserted would conflict, report it.
5613            Some(_) => Err(SqliteClientError::DbError(rusqlite::Error::SqliteFailure(
5614                rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CONSTRAINT),
5615                Some("UNIQUE constraint failed: tx_locator_map.block_height, tx_locator_map.tx_index".into()),
5616            )))?,
5617            // If the locator doesn't exist, insert it.
5618            None => stmt_insert_tx_locator.execute(tx_args).map(|_| ())?,
5619        }
5620
5621        for nf in nullifiers {
5622            // Here it is okay to use an upsert, because per above we've confirmed that
5623            // the locator points to the same transaction.
5624            let nf_args = named_params![
5625                ":spend_pool": pool_code(PoolType::Shielded(spend_pool)),
5626                ":nf": nf.as_ref(),
5627                ":block_height": u32::from(block_height),
5628                ":tx_index": u16::from(*tx_index),
5629            ];
5630            stmt_insert_nullifier_mapping.execute(nf_args)?;
5631        }
5632    }
5633
5634    Ok(())
5635}
5636
5637/// Returns the row of the `transactions` table corresponding to the transaction in which
5638/// this nullifier is revealed, if any.
5639pub(crate) fn query_nullifier_map<N: AsRef<[u8]>>(
5640    conn: &rusqlite::Transaction<'_>,
5641    spend_pool: ShieldedPool,
5642    nf: &N,
5643) -> Result<Option<TxRef>, SqliteClientError> {
5644    let mut stmt_select_locator = conn.prepare_cached(
5645        "SELECT block_height, tx_index, txid
5646        FROM nullifier_map
5647        LEFT JOIN tx_locator_map USING (block_height, tx_index)
5648        WHERE spend_pool = :spend_pool AND nf = :nf",
5649    )?;
5650
5651    let sql_args = named_params![
5652        ":spend_pool": pool_code(PoolType::Shielded(spend_pool)),
5653        ":nf": nf.as_ref(),
5654    ];
5655
5656    // Find the locator corresponding to this nullifier, if any.
5657    let locator = stmt_select_locator
5658        .query_row(sql_args, |row| {
5659            Ok((
5660                BlockHeight::from_u32(row.get(0)?),
5661                TxIndex::from(row.get::<_, u16>(1)?),
5662                TxId::from_bytes(row.get(2)?),
5663            ))
5664        })
5665        .optional()?;
5666    let (height, index, txid) = match locator {
5667        Some(res) => res,
5668        None => return Ok(None),
5669    };
5670
5671    // Find or create a corresponding row in the `transactions` table. Usually a row will
5672    // have been created during the same scan that the locator was added to the nullifier
5673    // map, but it would not happen if the transaction in question spent the note with no
5674    // change or explicit in-wallet recipient.
5675    put_tx_meta(
5676        conn,
5677        &WalletTx::new(
5678            txid,
5679            index,
5680            vec![],
5681            vec![],
5682            vec![],
5683            #[cfg(feature = "orchard")]
5684            vec![],
5685            #[cfg(feature = "orchard")]
5686            vec![],
5687            #[cfg(feature = "orchard")]
5688            vec![],
5689            #[cfg(feature = "orchard")]
5690            vec![],
5691        ),
5692        height,
5693    )
5694    .map(Some)
5695}
5696
5697/// Deletes from the nullifier map any entries with a locator referencing a block height
5698/// lower than the pruning height.
5699pub(crate) fn prune_nullifier_map(
5700    conn: &rusqlite::Transaction<'_>,
5701    block_height: BlockHeight,
5702) -> Result<(), SqliteClientError> {
5703    let mut stmt_delete_locators = conn.prepare_cached(
5704        "DELETE FROM tx_locator_map
5705        WHERE block_height < :block_height",
5706    )?;
5707
5708    stmt_delete_locators.execute(named_params![":block_height": u32::from(block_height)])?;
5709
5710    Ok(())
5711}
5712
5713pub(crate) fn get_block_range(
5714    conn: &rusqlite::Connection,
5715    protocol: ShieldedPool,
5716    commitment_tree_address: incrementalmerkletree::Address,
5717) -> Result<Option<Range<BlockHeight>>, SqliteClientError> {
5718    let prefix = match protocol {
5719        ShieldedPool::Sapling => "sapling",
5720        ShieldedPool::Orchard => "orchard",
5721        ShieldedPool::Ironwood => "ironwood",
5722    };
5723    let mut stmt = conn.prepare_cached(&format!(
5724        "SELECT MIN(height), MAX(height), MAX({prefix}_commitment_tree_size)
5725         FROM blocks
5726         WHERE {prefix}_commitment_tree_size BETWEEN :min_tree_size AND :max_tree_size"
5727    ))?;
5728
5729    stmt.query_row(
5730        // BETWEEN is inclusive on both ends. However, we are comparing commitment tree sizes
5731        // to commitment tree positions, so we must add one to the start, and we do not subtract
5732        // one from the end.
5733        named_params! {
5734            ":min_tree_size": u64::from(commitment_tree_address.position_range_start()) + 1,
5735            ":max_tree_size": u64::from(commitment_tree_address.position_range_end()),
5736        },
5737        |row| {
5738            // The first block to be scanned is known to contain the start of the address range in
5739            // question because the tree size we compared against is measured as of the end of the
5740            // block.
5741            let min_height = row.get::<_, Option<u32>>(0)?.map(BlockHeight::from_u32);
5742            let max_height_inclusive = row.get::<_, Option<u32>>(1)?.map(BlockHeight::from_u32);
5743            let end_offset = row.get::<_, Option<u64>>(2)?.map(|max_height_tree_size| {
5744                // If the tree size at the end of the max-height block is less than the
5745                // end-exclusive maximum position of the address range, this means that the end of
5746                // the subtree referred to by that address is somewhere in the next block, so we
5747                // need to rescan an extra block to ensure that we have observed all of the note
5748                // commitments that aggregate up to that address.
5749                if max_height_tree_size < u64::from(commitment_tree_address.position_range_end()) {
5750                    1
5751                } else {
5752                    0
5753                }
5754            });
5755
5756            Ok(min_height
5757                .zip(max_height_inclusive)
5758                .zip(end_offset)
5759                .map(|((min, max_inclusive), offset)| min..(max_inclusive + offset + 1)))
5760        },
5761    )
5762    .map_err(SqliteClientError::from)
5763}
5764
5765pub(crate) fn get_received_outputs(
5766    conn: &rusqlite::Connection,
5767    txid: TxId,
5768    target_height: TargetHeight,
5769    confirmations_policy: ConfirmationsPolicy,
5770) -> Result<Vec<ReceivedTransactionOutput>, SqliteClientError> {
5771    let mut stmt_received_outputs = conn.prepare_cached(
5772        "SELECT
5773             vto.output_pool,
5774             vto.output_index,
5775             vto.recipient_key_scope,
5776             vto.value,
5777             vto.tx_mined_height,
5778             IFNULL(vto.tx_trust_status, 0) AS tx_trust_status,
5779             MAX(tt.mined_height) AS max_shielding_input_height,
5780             MIN(IFNULL(tt.trust_status, 0)) AS min_shielding_input_trust
5781         FROM v_tx_outputs vto
5782         LEFT OUTER JOIN transparent_received_output_spends ros
5783            ON ros.transaction_id = vto.transaction_id
5784         LEFT OUTER JOIN transparent_received_outputs tro
5785            ON tro.id = ros.transparent_received_output_id
5786         LEFT OUTER JOIN transactions tt
5787            ON tt.id_tx = tro.transaction_id
5788         WHERE vto.txid = :txid
5789         GROUP BY vto.output_pool, vto.output_index",
5790    )?;
5791
5792    let results = stmt_received_outputs
5793        .query_and_then::<_, SqliteClientError, _, _>(
5794            named_params![":txid": txid.as_ref()],
5795            |row| {
5796                let pool_type = parse_pool_code(row.get("output_pool")?)?;
5797                let output_index = row.get("output_index")?;
5798                let value = Zatoshis::from_nonnegative_i64(row.get("value")?)?;
5799                let mined_height = row
5800                    .get::<_, Option<u32>>("tx_mined_height")?
5801                    .map(BlockHeight::from);
5802                let max_shielding_input_height = row
5803                    .get::<_, Option<u32>>("max_shielding_input_height")?
5804                    .map(BlockHeight::from);
5805                let tx_shielding_inputs_trusted =
5806                    row.get::<_, bool>("min_shielding_input_trust")?;
5807                let key_scope = row
5808                    .get::<_, Option<i64>>("recipient_key_scope")?
5809                    .map(KeyScope::decode)
5810                    .transpose()?;
5811                let tx_trusted = row.get::<_, bool>("tx_trust_status")?;
5812
5813                let confirmations_until_spendable = confirmations_policy
5814                    .confirmations_until_spendable(
5815                        target_height,
5816                        pool_type,
5817                        key_scope.and_then(|s| zip32::Scope::try_from(s).ok()),
5818                        mined_height,
5819                        tx_trusted,
5820                        max_shielding_input_height,
5821                        tx_shielding_inputs_trusted,
5822                    );
5823
5824                Ok(ReceivedTransactionOutput::from_parts(
5825                    pool_type,
5826                    output_index,
5827                    value,
5828                    confirmations_until_spendable,
5829                ))
5830            },
5831        )?
5832        .collect::<Result<Vec<_>, _>>()?;
5833
5834    Ok(results)
5835}
5836
5837/// Test utilities for wallet database assertions.
5838#[cfg(any(test, feature = "test-dependencies"))]
5839pub mod testing {
5840    use incrementalmerkletree::Position;
5841    use zcash_client_backend::data_api::testing::TransactionSummary;
5842    use zcash_primitives::transaction::TxId;
5843    use zcash_protocol::{
5844        ShieldedPool,
5845        consensus::BlockHeight,
5846        value::{ZatBalance, Zatoshis},
5847    };
5848
5849    use super::common::{TableConstants, table_constants};
5850    use crate::{AccountUuid, error::SqliteClientError};
5851
5852    pub(crate) fn get_tx_history(
5853        conn: &rusqlite::Connection,
5854    ) -> Result<Vec<TransactionSummary<AccountUuid>>, SqliteClientError> {
5855        let mut stmt = conn.prepare_cached(
5856            "SELECT accounts.uuid as account_uuid, v_transactions.*
5857             FROM v_transactions
5858             JOIN accounts ON accounts.uuid = v_transactions.account_uuid
5859             ORDER BY mined_height DESC, tx_index DESC",
5860        )?;
5861
5862        let results = stmt
5863            .query_and_then::<_, SqliteClientError, _, _>([], |row| {
5864                Ok(TransactionSummary::from_parts(
5865                    AccountUuid(row.get("account_uuid")?),
5866                    TxId::from_bytes(row.get("txid")?),
5867                    row.get::<_, Option<u32>>("expiry_height")?
5868                        .map(BlockHeight::from),
5869                    row.get::<_, Option<u32>>("mined_height")?
5870                        .map(BlockHeight::from),
5871                    ZatBalance::from_i64(row.get("account_balance_delta")?)?,
5872                    Zatoshis::from_nonnegative_i64(row.get("total_spent")?)?,
5873                    Zatoshis::from_nonnegative_i64(row.get("total_received")?)?,
5874                    row.get::<_, Option<i64>>("fee_paid")?
5875                        .map(Zatoshis::from_nonnegative_i64)
5876                        .transpose()?,
5877                    row.get("spent_note_count")?,
5878                    row.get("has_change")?,
5879                    row.get("sent_note_count")?,
5880                    row.get("received_note_count")?,
5881                    row.get("memo_count")?,
5882                    row.get("expired_unmined")?,
5883                    row.get("is_shielding")?,
5884                    row.get::<_, Option<i64>>("pool_crossing_value")?
5885                        .map(Zatoshis::from_nonnegative_i64)
5886                        .transpose()?,
5887                ))
5888            })?
5889            .collect::<Result<Vec<_>, _>>()?;
5890
5891        Ok(results)
5892    }
5893
5894    /// Returns a vector of transaction summaries
5895    #[allow(dead_code)] // used only for tests that are flagged off by default
5896    pub(crate) fn get_checkpoint_history(
5897        conn: &rusqlite::Connection,
5898        protocol: ShieldedPool,
5899    ) -> Result<Vec<(BlockHeight, Option<Position>)>, SqliteClientError> {
5900        let TableConstants { table_prefix, .. } = table_constants::<SqliteClientError>(protocol)?;
5901
5902        let mut stmt = conn.prepare_cached(&format!(
5903            "SELECT checkpoint_id, position FROM {table_prefix}_tree_checkpoints
5904             ORDER BY checkpoint_id",
5905        ))?;
5906
5907        let results = stmt
5908            .query_and_then::<_, SqliteClientError, _, _>([], |row| {
5909                Ok((
5910                    BlockHeight::from(row.get::<_, u32>(0)?),
5911                    row.get::<_, Option<u64>>(1)?.map(Position::from),
5912                ))
5913            })?
5914            .collect::<Result<Vec<_>, _>>()?;
5915
5916        Ok(results)
5917    }
5918}
5919
5920#[cfg(test)]
5921mod tests {
5922    use std::{
5923        collections::HashSet,
5924        num::{NonZeroU8, NonZeroU32},
5925    };
5926
5927    use rusqlite::{Connection, named_params};
5928    use sapling::zip32::ExtendedSpendingKey;
5929    use secrecy::{ExposeSecret, SecretVec};
5930    use uuid::Uuid;
5931    use zcash_client_backend::data_api::{
5932        Account as _, AccountSource, TransactionDataRequest, TransactionStatus, WalletRead,
5933        WalletWrite,
5934        chain::{ChainState, CommitmentTreeRoot},
5935        error::RewindError,
5936        testing::{
5937            AddressType, DataStoreFactory, FakeCompactOutput, InitialChainState, TestBuilder,
5938            TestState, pool::ShieldedPoolTester, sapling::SaplingPoolTester,
5939        },
5940        wallet::ConfirmationsPolicy,
5941    };
5942    use zcash_keys::keys::UnifiedAddressRequest;
5943    use zcash_primitives::block::BlockHash;
5944    use zcash_protocol::{
5945        TxId,
5946        consensus::{BlockHeight, NetworkUpgrade, Parameters},
5947        value::Zatoshis,
5948    };
5949
5950    use crate::{
5951        AccountUuid,
5952        error::SqliteClientError,
5953        testing::{BlockCache, db::TestDbFactory},
5954    };
5955
5956    use super::{
5957        KeyScope, ShieldedPool, TxQueryType, TxRef, account_birthday,
5958        flag_previously_received_change, min_shared_checkpoint_height, queue_tx_retrieval,
5959        select_truncation_height,
5960    };
5961
5962    use incrementalmerkletree::frontier::Frontier;
5963    #[cfg(feature = "orchard")]
5964    use {
5965        crate::testing::db::TestDb, ::orchard::tree::MerkleHashOrchard,
5966        incrementalmerkletree::Hashable as _, shardtree::error::ShardTreeError,
5967        zcash_client_backend::data_api::WalletCommitmentTrees,
5968        zcash_protocol::local_consensus::LocalNetwork,
5969    };
5970
5971    fn connection_with_checkpoint_tables() -> Connection {
5972        let conn = Connection::open_in_memory().unwrap();
5973        conn.execute_batch(
5974            "CREATE TABLE blocks (height INTEGER PRIMARY KEY);
5975             CREATE TABLE transactions (id_tx INTEGER PRIMARY KEY, mined_height INTEGER);
5976             CREATE TABLE sapling_tree_checkpoints (checkpoint_id INTEGER PRIMARY KEY);
5977             CREATE TABLE orchard_tree_checkpoints (checkpoint_id INTEGER PRIMARY KEY);
5978             CREATE TABLE ironwood_tree_checkpoints (checkpoint_id INTEGER PRIMARY KEY);
5979             CREATE TABLE sapling_received_notes (
5980                 id INTEGER PRIMARY KEY,
5981                 transaction_id INTEGER,
5982                 commitment_tree_position INTEGER);
5983             CREATE TABLE orchard_received_notes (
5984                 id INTEGER PRIMARY KEY,
5985                 transaction_id INTEGER,
5986                 commitment_tree_position INTEGER);
5987             CREATE TABLE ironwood_received_notes (
5988                 id INTEGER PRIMARY KEY,
5989                 transaction_id INTEGER,
5990                 commitment_tree_position INTEGER);",
5991        )
5992        .unwrap();
5993        conn
5994    }
5995
5996    /// A pool whose checkpoints all lie at or below the requested height tolerates a
5997    /// truncation to that height (its tree holds nothing the truncation must remove), so the
5998    /// requested height itself qualifies even though the pool has no checkpoint there.
5999    #[test]
6000    fn truncation_height_tolerates_lagging_ironwood_checkpoints() {
6001        let mut conn = connection_with_checkpoint_tables();
6002        conn.execute_batch(
6003            "INSERT INTO blocks (height) VALUES (10), (11);
6004             INSERT INTO sapling_tree_checkpoints (checkpoint_id) VALUES (10), (11);
6005             INSERT INTO orchard_tree_checkpoints (checkpoint_id) VALUES (10), (11);
6006             INSERT INTO ironwood_tree_checkpoints (checkpoint_id) VALUES (10);",
6007        )
6008        .unwrap();
6009
6010        let tx = conn.transaction().unwrap();
6011        assert_eq!(
6012            select_truncation_height(&tx, BlockHeight::from_u32(11)).unwrap(),
6013            BlockHeight::from_u32(11),
6014        );
6015    }
6016
6017    /// A pool whose checkpoints all lie *above* the requested height, and which has no notes
6018    /// whose witnesses a rescan of the heights above it would not re-create, tolerates a
6019    /// truncation to that height: the truncation empties the pool's tree.
6020    #[test]
6021    fn truncation_height_tolerates_tree_emptying_ironwood_truncation() {
6022        let mut conn = connection_with_checkpoint_tables();
6023        conn.execute_batch(
6024            "INSERT INTO blocks (height) VALUES (10), (11);
6025             INSERT INTO sapling_tree_checkpoints (checkpoint_id) VALUES (10), (11);
6026             INSERT INTO orchard_tree_checkpoints (checkpoint_id) VALUES (10), (11);
6027             INSERT INTO ironwood_tree_checkpoints (checkpoint_id) VALUES (12), (13);",
6028        )
6029        .unwrap();
6030
6031        let tx = conn.transaction().unwrap();
6032        assert_eq!(
6033            select_truncation_height(&tx, BlockHeight::from_u32(11)).unwrap(),
6034            BlockHeight::from_u32(11),
6035        );
6036    }
6037
6038    /// A pool with checkpoints both above and below a candidate height but none at it cannot
6039    /// be truncated to that height; the next-lower height at which every pool's checkpoint
6040    /// coverage is consistent is selected instead.
6041    #[test]
6042    fn truncation_height_rejects_straddling_ironwood_checkpoints() {
6043        let mut conn = connection_with_checkpoint_tables();
6044        conn.execute_batch(
6045            "INSERT INTO blocks (height) VALUES (9), (10), (11);
6046             INSERT INTO sapling_tree_checkpoints (checkpoint_id) VALUES (9), (10), (11);
6047             INSERT INTO orchard_tree_checkpoints (checkpoint_id) VALUES (9), (10), (11);
6048             INSERT INTO ironwood_tree_checkpoints (checkpoint_id) VALUES (9), (11);",
6049        )
6050        .unwrap();
6051
6052        let tx = conn.transaction().unwrap();
6053        assert_eq!(
6054            select_truncation_height(&tx, BlockHeight::from_u32(10)).unwrap(),
6055            BlockHeight::from_u32(9),
6056        );
6057    }
6058
6059    /// A truncation that would empty a pool's tree does not qualify when the pool has notes
6060    /// with recorded witness positions at or below the truncation height: emptying the tree
6061    /// would destroy witnesses that no rescan would re-create.
6062    #[test]
6063    fn truncation_height_rejects_witness_destroying_ironwood_truncation() {
6064        let mut conn = connection_with_checkpoint_tables();
6065        conn.execute_batch(
6066            "INSERT INTO blocks (height) VALUES (10), (11);
6067             INSERT INTO transactions (id_tx, mined_height) VALUES (1, 10);
6068             INSERT INTO sapling_tree_checkpoints (checkpoint_id) VALUES (10), (11);
6069             INSERT INTO orchard_tree_checkpoints (checkpoint_id) VALUES (10), (11);
6070             INSERT INTO ironwood_tree_checkpoints (checkpoint_id) VALUES (12), (13);
6071             INSERT INTO ironwood_received_notes (id, transaction_id, commitment_tree_position)
6072                 VALUES (1, 1, 5);",
6073        )
6074        .unwrap();
6075
6076        let tx = conn.transaction().unwrap();
6077        assert_matches!(
6078            select_truncation_height(&tx, BlockHeight::from_u32(11)),
6079            Err(SqliteClientError::RequestedRewindInvalid {
6080                safe_rewind_height: None,
6081                ..
6082            })
6083        );
6084    }
6085
6086    #[test]
6087    fn safe_rewind_height_requires_an_ironwood_checkpoint() {
6088        let conn = connection_with_checkpoint_tables();
6089        conn.execute_batch(
6090            "INSERT INTO sapling_tree_checkpoints (checkpoint_id) VALUES (10);
6091             INSERT INTO orchard_tree_checkpoints (checkpoint_id) VALUES (10);
6092             INSERT INTO ironwood_tree_checkpoints (checkpoint_id) VALUES (11);",
6093        )
6094        .unwrap();
6095
6096        assert_eq!(min_shared_checkpoint_height(&conn).unwrap(), None);
6097    }
6098
6099    #[test]
6100    fn empty_database_has_no_balance() {
6101        let st = TestBuilder::new()
6102            .with_data_store_factory(TestDbFactory::default())
6103            .with_account_from_sapling_activation(BlockHash([0; 32]))
6104            .build();
6105        let account = st.test_account().unwrap();
6106
6107        // The account should have no summary information
6108        assert_eq!(st.get_wallet_summary(ConfirmationsPolicy::MIN), None);
6109
6110        // We can't get an anchor height, as we have not scanned any blocks.
6111        assert_eq!(
6112            st.wallet()
6113                .get_target_and_anchor_heights(NonZeroU32::new(10).unwrap())
6114                .unwrap(),
6115            None
6116        );
6117
6118        // The default address is set for the test account
6119        assert_matches!(
6120            st.wallet().get_last_generated_address_matching(
6121                account.id(),
6122                UnifiedAddressRequest::AllAvailableKeys
6123            ),
6124            Ok(Some(_))
6125        );
6126
6127        // No default address is set for an un-initialized account
6128        assert_matches!(
6129            st.wallet().get_last_generated_address_matching(
6130                AccountUuid(Uuid::nil()),
6131                UnifiedAddressRequest::AllAvailableKeys
6132            ),
6133            Err(SqliteClientError::AccountUnknown)
6134        );
6135    }
6136
6137    #[test]
6138    fn status_intent_persists_until_the_transaction_is_terminal() {
6139        const TEST_VALUE: Zatoshis = Zatoshis::const_from_u64(10_000);
6140        const FUTURE_EXPIRY_OFFSET: u32 = 10;
6141        const UNEXPIRED_TXID_BYTES: [u8; 32] = [1; 32];
6142        const EXPIRED_TXID_BYTES: [u8; 32] = [2; 32];
6143
6144        let mut st = TestBuilder::new()
6145            .with_data_store_factory(TestDbFactory::default())
6146            .with_block_cache(BlockCache::new())
6147            .with_account_from_sapling_activation(BlockHash([0; 32]))
6148            .build();
6149
6150        let dfvk = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
6151        let tip = st.sapling_activation_height();
6152        st.generate_block_at(
6153            tip,
6154            BlockHash([0; 32]),
6155            &[FakeCompactOutput::new(
6156                &dfvk,
6157                AddressType::DefaultExternal,
6158                TEST_VALUE,
6159            )],
6160            0,
6161            0,
6162            0,
6163            false,
6164        );
6165        st.scan_cached_blocks(tip, 1);
6166
6167        let unexpired_txid = TxId::from_bytes(UNEXPIRED_TXID_BYTES);
6168        let expired_txid = TxId::from_bytes(EXPIRED_TXID_BYTES);
6169        for (txid, expiry_height) in [
6170            (unexpired_txid, u32::from(tip) + FUTURE_EXPIRY_OFFSET),
6171            (expired_txid, u32::from(tip)),
6172        ] {
6173            st.wallet()
6174                .conn()
6175                .execute(
6176                    "INSERT INTO transactions (txid, expiry_height, min_observed_height)
6177                     VALUES (:txid, :expiry_height, :min_observed_height)",
6178                    named_params![
6179                        ":txid": txid.as_ref(),
6180                        ":expiry_height": expiry_height,
6181                        ":min_observed_height": u32::from(tip),
6182                    ],
6183                )
6184                .unwrap();
6185            st.wallet()
6186                .conn()
6187                .execute(
6188                    "INSERT INTO tx_retrieval_queue (txid, query_type)
6189                     VALUES (:txid, :query_type)",
6190                    named_params![
6191                        ":txid": txid.as_ref(),
6192                        ":query_type": TxQueryType::Status.code(),
6193                    ],
6194                )
6195                .unwrap();
6196        }
6197
6198        for txid in [unexpired_txid, expired_txid] {
6199            st.wallet_mut()
6200                .set_transaction_status(txid, TransactionStatus::NotInMainChain)
6201                .unwrap();
6202        }
6203
6204        let requests = st.wallet().transaction_data_requests().unwrap();
6205        assert!(requests.contains(&TransactionDataRequest::GetStatus(unexpired_txid)));
6206        assert!(!requests.contains(&TransactionDataRequest::GetStatus(expired_txid)));
6207
6208        let db_tx = st.wallet().conn().unchecked_transaction().unwrap();
6209        queue_tx_retrieval(&db_tx, std::iter::once(unexpired_txid), None).unwrap();
6210        db_tx.commit().unwrap();
6211
6212        let requests = st.wallet().transaction_data_requests().unwrap();
6213        assert!(requests.contains(&TransactionDataRequest::GetStatus(unexpired_txid)));
6214        assert!(requests.contains(&TransactionDataRequest::Enhancement(unexpired_txid)));
6215    }
6216
6217    #[test]
6218    fn get_default_account_index() {
6219        let st = TestBuilder::new()
6220            .with_data_store_factory(TestDbFactory::default())
6221            .with_account_from_sapling_activation(BlockHash([0; 32]))
6222            .build();
6223        let account_id = st.test_account().unwrap().id();
6224        let account_parameters = st.wallet().get_account(account_id).unwrap().unwrap();
6225
6226        let expected_account_index = zip32::AccountId::try_from(0).unwrap();
6227        assert_matches!(
6228            account_parameters.kind,
6229            AccountSource::Derived{derivation, ..} if derivation.account_index() == expected_account_index
6230        );
6231    }
6232
6233    #[test]
6234    fn get_account_ids() {
6235        let mut st = TestBuilder::new()
6236            .with_data_store_factory(TestDbFactory::default())
6237            .with_account_from_sapling_activation(BlockHash([0; 32]))
6238            .build();
6239
6240        let seed = SecretVec::new(st.test_seed().unwrap().expose_secret().clone());
6241        let birthday = st.test_account().unwrap().birthday().clone();
6242
6243        st.wallet_mut()
6244            .create_account("", &seed, &birthday, None)
6245            .unwrap();
6246
6247        for acct_id in st.wallet().get_account_ids().unwrap() {
6248            assert_matches!(st.wallet().get_account(acct_id), Ok(Some(_)))
6249        }
6250    }
6251
6252    #[test]
6253    fn block_fully_scanned() {
6254        check_block_fully_scanned(TestDbFactory::default())
6255    }
6256
6257    fn check_block_fully_scanned<DsF: DataStoreFactory>(dsf: DsF) {
6258        let mut st = TestBuilder::new()
6259            .with_data_store_factory(dsf)
6260            .with_block_cache(BlockCache::new())
6261            .with_account_from_sapling_activation(BlockHash([0; 32]))
6262            .build();
6263
6264        let block_fully_scanned = |st: &TestState<_, DsF::DataStore, _>| {
6265            st.wallet()
6266                .block_fully_scanned()
6267                .unwrap()
6268                .map(|meta| meta.block_height())
6269        };
6270
6271        // A fresh wallet should have no fully-scanned block.
6272        assert_eq!(block_fully_scanned(&st), None);
6273
6274        // Scan a block above the wallet's birthday height.
6275        let not_our_key = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
6276        let not_our_value = Zatoshis::const_from_u64(10000);
6277        let start_height = st.sapling_activation_height();
6278        let _ = st.generate_block_at(
6279            start_height,
6280            BlockHash([0; 32]),
6281            &[FakeCompactOutput::new(
6282                &not_our_key,
6283                AddressType::DefaultExternal,
6284                not_our_value,
6285            )],
6286            0,
6287            0,
6288            0,
6289            false,
6290        );
6291        let (mid_height, _, _) =
6292            st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
6293        let (end_height, _, _) =
6294            st.generate_next_block(&not_our_key, AddressType::DefaultExternal, not_our_value);
6295
6296        // Scan the last block first
6297        st.scan_cached_blocks(end_height, 1);
6298
6299        // The wallet should still have no fully-scanned block, as no scanned block range
6300        // overlaps the wallet's birthday.
6301        assert_eq!(block_fully_scanned(&st), None);
6302
6303        // Scan the block at the wallet's birthday height.
6304        st.scan_cached_blocks(start_height, 1);
6305
6306        // The fully-scanned height should now be that of the scanned block.
6307        assert_eq!(block_fully_scanned(&st), Some(start_height));
6308
6309        // Scan the block in between the two previous blocks.
6310        st.scan_cached_blocks(mid_height, 1);
6311
6312        // The fully-scanned height should now be the latest block, as the two disjoint
6313        // ranges have been connected.
6314        assert_eq!(block_fully_scanned(&st), Some(end_height));
6315    }
6316
6317    #[test]
6318    fn test_account_birthday() {
6319        let st = TestBuilder::new()
6320            .with_data_store_factory(TestDbFactory::default())
6321            .with_block_cache(BlockCache::new())
6322            .with_account_from_sapling_activation(BlockHash([0; 32]))
6323            .build();
6324
6325        let account_id = st.test_account().unwrap().id();
6326        assert_matches!(
6327            account_birthday(st.wallet().conn(), account_id),
6328            Ok(birthday) if birthday == st.sapling_activation_height()
6329        )
6330    }
6331
6332    #[test]
6333    fn rewound_birthday_does_not_falsely_report_complete_recovery() {
6334        // Configure a prior chain state with three complete sapling subtrees plus a
6335        // partial frontier. The subtree roots are imported into `tree_shards` (with
6336        // their `subtree_end_height` populated, per the wallet invariant), but the
6337        // wallet has never seen a block below the chain-state height -- those notes
6338        // exist only as imported roots, not as `blocks` rows.
6339        let prior_block_hash = BlockHash([0; 32]);
6340        let initial_sapling_tree_size: u32 = (0x1 << 16) * 3 + 5;
6341        let initial_orchard_tree_size: u32 = (0x1 << 16) * 3 + 5;
6342        let initial_height_offset: u32 = 310;
6343
6344        let mut st = TestBuilder::new()
6345            .with_data_store_factory(TestDbFactory::default())
6346            .with_block_cache(BlockCache::new())
6347            .with_initial_chain_state(|rng, network| {
6348                let sapling_activation_height =
6349                    network.activation_height(NetworkUpgrade::Sapling).unwrap();
6350                let (prior_sapling_roots, sapling_initial_tree) =
6351                    Frontier::random_with_prior_subtree_roots(
6352                        rng,
6353                        initial_sapling_tree_size.into(),
6354                        NonZeroU8::new(16).unwrap(),
6355                    );
6356                let prior_sapling_roots = prior_sapling_roots
6357                    .into_iter()
6358                    .zip(1u32..)
6359                    .map(|(root, i)| {
6360                        CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
6361                    })
6362                    .collect::<Vec<_>>();
6363
6364                #[cfg(feature = "orchard")]
6365                let (prior_orchard_roots, orchard_initial_tree) =
6366                    Frontier::random_with_prior_subtree_roots(
6367                        rng,
6368                        initial_orchard_tree_size.into(),
6369                        NonZeroU8::new(16).unwrap(),
6370                    );
6371                #[cfg(feature = "orchard")]
6372                let prior_orchard_roots = prior_orchard_roots
6373                    .into_iter()
6374                    .zip(1u32..)
6375                    .map(|(root, i)| {
6376                        CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
6377                    })
6378                    .collect::<Vec<_>>();
6379
6380                // No Ironwood notes are involved in this test, so its chain state carries an
6381
6382                // empty Ironwood tree.
6383
6384                #[cfg(feature = "orchard")]
6385                let ironwood_initial_tree = Frontier::empty();
6386
6387                InitialChainState {
6388                    chain_state: ChainState::new(
6389                        sapling_activation_height + initial_height_offset - 1,
6390                        prior_block_hash,
6391                        sapling_initial_tree,
6392                        #[cfg(feature = "orchard")]
6393                        orchard_initial_tree,
6394                        #[cfg(feature = "orchard")]
6395                        ironwood_initial_tree,
6396                    ),
6397                    prior_sapling_roots,
6398                    #[cfg(feature = "orchard")]
6399                    prior_orchard_roots,
6400                }
6401            })
6402            .with_account_having_current_birthday()
6403            .build();
6404
6405        let sapling_activation_height = st.sapling_activation_height();
6406        let dfvk = SaplingPoolTester::test_account_fvk(&st);
6407        let initial_height = sapling_activation_height + initial_height_offset;
6408
6409        // Generate and scan ten blocks above the imported subtree state. Each
6410        // block contributes one sapling output, so `blocks` contains exactly
6411        // `[initial_height, initial_height + 10)` with one output per row.
6412        st.generate_block_at(
6413            initial_height,
6414            prior_block_hash,
6415            &[FakeCompactOutput::new(
6416                &dfvk,
6417                AddressType::DefaultExternal,
6418                Zatoshis::const_from_u64(50000),
6419            )],
6420            initial_sapling_tree_size,
6421            initial_orchard_tree_size,
6422            0,
6423            false,
6424        );
6425        for _ in 1..10 {
6426            st.generate_next_block(
6427                &dfvk,
6428                AddressType::DefaultExternal,
6429                Zatoshis::const_from_u64(10000),
6430            );
6431        }
6432        st.scan_cached_blocks(initial_height, 10);
6433
6434        let chain_tip_height = initial_height + 9;
6435        let recover_until_height = initial_height + 5;
6436
6437        // Simulate a rewind that drops the effective birthday below every
6438        // height the wallet has scanned. The wallet has never scanned
6439        // `[sapling_activation_height, initial_height)`; any notes there
6440        // exist only in the imported subtree roots, so recovery cannot
6441        // legitimately report 100% completion.
6442        let progress = super::subtree_scan_progress(
6443            st.wallet().conn(),
6444            st.network(),
6445            ShieldedPool::Sapling,
6446            sapling_activation_height,
6447            sapling_activation_height,
6448            Some(recover_until_height),
6449            chain_tip_height,
6450        )
6451        .expect("subtree_scan_progress must not error")
6452        .expect("a Progress value should be returned");
6453
6454        let recovery = progress
6455            .recovery()
6456            .expect("recovery progress should be reported");
6457
6458        // The recovery range `[sapling_activation_height, recover_until_height)`
6459        // covers at least `initial_sapling_tree_size` outputs that the wallet
6460        // has never scanned. A correct denominator must reflect those, so
6461        // recovery cannot report 100% completion.
6462        assert!(
6463            recovery.numerator() < recovery.denominator(),
6464            "recovery wrongly reports {n}/{d} after a rewind to a birthday \
6465             below all scanned blocks; at least {unscanned} outputs in \
6466             [{birthday:?}, {first:?}) live only in imported subtree roots and \
6467             have never been scanned",
6468            n = recovery.numerator(),
6469            d = recovery.denominator(),
6470            unscanned = u64::from(initial_sapling_tree_size),
6471            birthday = sapling_activation_height,
6472            first = initial_height,
6473        );
6474    }
6475
6476    #[test]
6477    fn rewound_birthday_recovery_denominator_includes_imported_subtrees() {
6478        // Same imported-subtrees + small scanned tail setup as the previous
6479        // rewound-birthday test. In addition to checking that recovery is
6480        // not falsely reported as 100% complete, this test asserts that the
6481        // recovery denominator accounts for the outputs of the imported
6482        // subtree roots that fall within the recovery range, so the ratio
6483        // remains meaningful across the rewind point.
6484        let prior_block_hash = BlockHash([0; 32]);
6485        let initial_sapling_tree_size: u32 = (0x1 << 16) * 3 + 5;
6486        let initial_orchard_tree_size: u32 = (0x1 << 16) * 3 + 5;
6487        let initial_height_offset: u32 = 310;
6488
6489        let mut st = TestBuilder::new()
6490            .with_data_store_factory(TestDbFactory::default())
6491            .with_block_cache(BlockCache::new())
6492            .with_initial_chain_state(|rng, network| {
6493                let sapling_activation_height =
6494                    network.activation_height(NetworkUpgrade::Sapling).unwrap();
6495                let (prior_sapling_roots, sapling_initial_tree) =
6496                    Frontier::random_with_prior_subtree_roots(
6497                        rng,
6498                        initial_sapling_tree_size.into(),
6499                        NonZeroU8::new(16).unwrap(),
6500                    );
6501                let prior_sapling_roots = prior_sapling_roots
6502                    .into_iter()
6503                    .zip(1u32..)
6504                    .map(|(root, i)| {
6505                        CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
6506                    })
6507                    .collect::<Vec<_>>();
6508
6509                #[cfg(feature = "orchard")]
6510                let (prior_orchard_roots, orchard_initial_tree) =
6511                    Frontier::random_with_prior_subtree_roots(
6512                        rng,
6513                        initial_orchard_tree_size.into(),
6514                        NonZeroU8::new(16).unwrap(),
6515                    );
6516                #[cfg(feature = "orchard")]
6517                let prior_orchard_roots = prior_orchard_roots
6518                    .into_iter()
6519                    .zip(1u32..)
6520                    .map(|(root, i)| {
6521                        CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
6522                    })
6523                    .collect::<Vec<_>>();
6524
6525                // No Ironwood notes are involved in this test, so its chain state carries an
6526
6527                // empty Ironwood tree.
6528
6529                #[cfg(feature = "orchard")]
6530                let ironwood_initial_tree = Frontier::empty();
6531
6532                InitialChainState {
6533                    chain_state: ChainState::new(
6534                        sapling_activation_height + initial_height_offset - 1,
6535                        prior_block_hash,
6536                        sapling_initial_tree,
6537                        #[cfg(feature = "orchard")]
6538                        orchard_initial_tree,
6539                        #[cfg(feature = "orchard")]
6540                        ironwood_initial_tree,
6541                    ),
6542                    prior_sapling_roots,
6543                    #[cfg(feature = "orchard")]
6544                    prior_orchard_roots,
6545                }
6546            })
6547            .with_account_having_current_birthday()
6548            .build();
6549
6550        let sapling_activation_height = st.sapling_activation_height();
6551        let dfvk = SaplingPoolTester::test_account_fvk(&st);
6552        let initial_height = sapling_activation_height + initial_height_offset;
6553
6554        st.generate_block_at(
6555            initial_height,
6556            prior_block_hash,
6557            &[FakeCompactOutput::new(
6558                &dfvk,
6559                AddressType::DefaultExternal,
6560                Zatoshis::const_from_u64(50000),
6561            )],
6562            initial_sapling_tree_size,
6563            initial_orchard_tree_size,
6564            0,
6565            false,
6566        );
6567        for _ in 1..10 {
6568            st.generate_next_block(
6569                &dfvk,
6570                AddressType::DefaultExternal,
6571                Zatoshis::const_from_u64(10000),
6572            );
6573        }
6574        st.scan_cached_blocks(initial_height, 10);
6575
6576        let chain_tip_height = initial_height + 9;
6577        let recover_until_height = initial_height + 5;
6578
6579        let progress = super::subtree_scan_progress(
6580            st.wallet().conn(),
6581            st.network(),
6582            ShieldedPool::Sapling,
6583            sapling_activation_height,
6584            sapling_activation_height,
6585            Some(recover_until_height),
6586            chain_tip_height,
6587        )
6588        .expect("subtree_scan_progress must not error")
6589        .expect("a Progress value should be returned");
6590
6591        let recovery = progress
6592            .recovery()
6593            .expect("recovery progress should be reported");
6594
6595        // Sanity: scanned outputs cannot exceed total outputs in the recovery range.
6596        assert!(
6597            recovery.numerator() <= recovery.denominator(),
6598            "recovery numerator {n} exceeds denominator {d} in the \
6599             rewound-birthday scenario",
6600            n = recovery.numerator(),
6601            d = recovery.denominator(),
6602        );
6603
6604        // The wallet has never scanned `[sapling_activation_height, initial_height)`,
6605        // which contains at least `initial_sapling_tree_size` outputs from the
6606        // imported subtree roots. Recovery progress must therefore not report
6607        // 100% complete.
6608        assert!(
6609            recovery.numerator() < recovery.denominator(),
6610            "recovery wrongly reports {n}/{d} after a rewind to a birthday \
6611             below all scanned blocks; at least {unscanned} outputs in \
6612             [{birthday:?}, {first:?}) live only in imported subtree roots \
6613             and have never been scanned",
6614            n = recovery.numerator(),
6615            d = recovery.denominator(),
6616            unscanned = u64::from(initial_sapling_tree_size),
6617            birthday = sapling_activation_height,
6618            first = initial_height,
6619        );
6620
6621        // The denominator must reflect the imported subtree contents in the
6622        // recovery range -- otherwise the ratio is meaningless across the
6623        // rewind point.
6624        assert!(
6625            *recovery.denominator() >= u64::from(initial_sapling_tree_size),
6626            "recovery denominator {d} fails to account for the {imported} \
6627             outputs of the imported subtree roots that fall within \
6628             [{birthday:?}, {recover:?})",
6629            d = recovery.denominator(),
6630            imported = u64::from(initial_sapling_tree_size),
6631            birthday = sapling_activation_height,
6632            recover = recover_until_height,
6633        );
6634    }
6635
6636    #[test]
6637    fn recover_until_above_chain_tip_does_not_overshoot_tip_size() {
6638        // Reproduces the wild scenario in which one of the wallet's accounts has
6639        // `recover_until_height` slightly above the current chain tip (e.g. UFVK1
6640        // was registered with `recover_until` a few blocks past the then chain
6641        // tip, and the chain hasn't yet caught up). Then `recover_until_size`
6642        // is computed by linear extrapolation, and because `recovery_range >
6643        // total_range` the integer extrapolation overshoots `tip_tree_size`.
6644        // The unclamped subtraction `tip_tree_size - recover_until_size` then
6645        // underflows in the scan denominator.
6646        let prior_block_hash = BlockHash([0; 32]);
6647        let initial_sapling_tree_size: u32 = (0x1 << 16) * 3 + 5;
6648        let initial_orchard_tree_size: u32 = (0x1 << 16) * 3 + 5;
6649        let initial_height_offset: u32 = 310;
6650
6651        let mut st = TestBuilder::new()
6652            .with_data_store_factory(TestDbFactory::default())
6653            .with_block_cache(BlockCache::new())
6654            .with_initial_chain_state(|rng, network| {
6655                let sapling_activation_height =
6656                    network.activation_height(NetworkUpgrade::Sapling).unwrap();
6657                let (prior_sapling_roots, sapling_initial_tree) =
6658                    Frontier::random_with_prior_subtree_roots(
6659                        rng,
6660                        initial_sapling_tree_size.into(),
6661                        NonZeroU8::new(16).unwrap(),
6662                    );
6663                let prior_sapling_roots = prior_sapling_roots
6664                    .into_iter()
6665                    .zip(1u32..)
6666                    .map(|(root, i)| {
6667                        CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
6668                    })
6669                    .collect::<Vec<_>>();
6670
6671                #[cfg(feature = "orchard")]
6672                let (prior_orchard_roots, orchard_initial_tree) =
6673                    Frontier::random_with_prior_subtree_roots(
6674                        rng,
6675                        initial_orchard_tree_size.into(),
6676                        NonZeroU8::new(16).unwrap(),
6677                    );
6678                #[cfg(feature = "orchard")]
6679                let prior_orchard_roots = prior_orchard_roots
6680                    .into_iter()
6681                    .zip(1u32..)
6682                    .map(|(root, i)| {
6683                        CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root)
6684                    })
6685                    .collect::<Vec<_>>();
6686
6687                // No Ironwood notes are involved in this test, so its chain state carries an
6688
6689                // empty Ironwood tree.
6690
6691                #[cfg(feature = "orchard")]
6692                let ironwood_initial_tree = Frontier::empty();
6693
6694                InitialChainState {
6695                    chain_state: ChainState::new(
6696                        sapling_activation_height + initial_height_offset - 1,
6697                        prior_block_hash,
6698                        sapling_initial_tree,
6699                        #[cfg(feature = "orchard")]
6700                        orchard_initial_tree,
6701                        #[cfg(feature = "orchard")]
6702                        ironwood_initial_tree,
6703                    ),
6704                    prior_sapling_roots,
6705                    #[cfg(feature = "orchard")]
6706                    prior_orchard_roots,
6707                }
6708            })
6709            .with_account_having_current_birthday()
6710            .build();
6711
6712        let sapling_activation_height = st.sapling_activation_height();
6713        let dfvk = SaplingPoolTester::test_account_fvk(&st);
6714        let initial_height = sapling_activation_height + initial_height_offset;
6715
6716        st.generate_block_at(
6717            initial_height,
6718            prior_block_hash,
6719            &[FakeCompactOutput::new(
6720                &dfvk,
6721                AddressType::DefaultExternal,
6722                Zatoshis::const_from_u64(50000),
6723            )],
6724            initial_sapling_tree_size,
6725            initial_orchard_tree_size,
6726            0,
6727            false,
6728        );
6729        for _ in 1..10 {
6730            st.generate_next_block(
6731                &dfvk,
6732                AddressType::DefaultExternal,
6733                Zatoshis::const_from_u64(10000),
6734            );
6735        }
6736        st.scan_cached_blocks(initial_height, 10);
6737
6738        let chain_tip_height = initial_height + 9;
6739        // Recover-until sits a handful of blocks *above* the chain tip, mimicking
6740        // the wild scenario where UFVK1's `recover_until` was set with a buffer
6741        // and the chain has not caught up.
6742        let recover_until_height = chain_tip_height + 5;
6743
6744        // We expect this to return a Progress whose scan denominator has not
6745        // underflowed. With the current (unfixed) code, the call panics in
6746        // debug builds on `tip_tree_size - start_size` because the linear
6747        // extrapolation produces `recover_until_size > tip_tree_size`.
6748        let progress = super::subtree_scan_progress(
6749            st.wallet().conn(),
6750            st.network(),
6751            ShieldedPool::Sapling,
6752            sapling_activation_height,
6753            sapling_activation_height,
6754            Some(recover_until_height),
6755            chain_tip_height,
6756        )
6757        .expect("subtree_scan_progress must not error")
6758        .expect("a Progress value should be returned");
6759
6760        let scan = progress.scan();
6761
6762        // The scan denominator must be a sane count of outputs in the chain-tip
6763        // segment, never the result of a u64 underflow.
6764        assert!(
6765            *scan.denominator() <= u64::from(initial_sapling_tree_size) + 1_000,
6766            "scan denominator {d} appears to have underflowed (raw u64); \
6767             tip_tree_size and recover_until_size disagree about which is \
6768             upper-bound",
6769            d = scan.denominator(),
6770        );
6771        // And of course no ratio should report scanned > total.
6772        assert!(
6773            scan.numerator() <= scan.denominator(),
6774            "scan numerator {n} exceeds denominator {d}",
6775            n = scan.numerator(),
6776            d = scan.denominator(),
6777        );
6778    }
6779
6780    /// `rewind_to_chain_state` must return `RewindBeyondBirthdays` when the rewind would
6781    /// land below every account's birthday and the caller has not provided any accounts in
6782    /// `reset_account_birthdays` to acknowledge the lowering.
6783    #[test]
6784    fn rewind_to_chain_state_below_all_birthdays_with_empty_reset_returns_error() {
6785        let mut st = TestBuilder::new()
6786            .with_data_store_factory(TestDbFactory::default())
6787            .with_account_from_sapling_activation(BlockHash([0; 32]))
6788            .build();
6789
6790        let account_id = st.test_account().unwrap().id();
6791        let original_birthday = st.test_account().unwrap().birthday().height();
6792        // Pick a target whose `new_birthday = target + 1` is strictly below every
6793        // account's birthday, so the safeguard fires when the caller hasn't
6794        // acknowledged any reset.
6795        let target_height = original_birthday - 10;
6796
6797        let result = st.wallet_mut().rewind_to_chain_state(
6798            ChainState::empty(target_height, BlockHash([0; 32])),
6799            HashSet::new(),
6800        );
6801
6802        assert_matches!(
6803            result,
6804            Err(RewindError::RewindBeyondBirthdays(birthdays))
6805                if birthdays.get(&account_id) == Some(&original_birthday)
6806        );
6807    }
6808
6809    /// When the rewind target is below every account's birthday but the caller acknowledges
6810    /// the lowering by including the account in `reset_account_birthdays`, the rewind
6811    /// proceeds and the listed account's birthday is lowered to the new floor.
6812    #[test]
6813    fn rewind_to_chain_state_below_all_birthdays_with_account_in_reset_succeeds() {
6814        let mut st = TestBuilder::new()
6815            .with_data_store_factory(TestDbFactory::default())
6816            .with_account_from_sapling_activation(BlockHash([0; 32]))
6817            .build();
6818
6819        let account_id = st.test_account().unwrap().id();
6820        let original_birthday = st.test_account().unwrap().birthday().height();
6821        // Pick a target whose `new_birthday = target + 1` is strictly below every
6822        // account's birthday, so the safeguard fires when the caller hasn't
6823        // acknowledged any reset.
6824        let target_height = original_birthday - 10;
6825
6826        st.wallet_mut()
6827            .rewind_to_chain_state(
6828                ChainState::empty(target_height, BlockHash([0; 32])),
6829                HashSet::from([account_id]),
6830            )
6831            .expect("rewind_to_chain_state should succeed when the account is in reset");
6832
6833        // The account's birthday is now lowered to `target_height + 1`.
6834        assert_matches!(
6835            account_birthday(st.wallet().conn(), account_id),
6836            Ok(b) if b == target_height + 1
6837        );
6838    }
6839
6840    /// `rewind_to_chain_state` must reject `reset_account_birthdays` containing an
6841    /// `AccountUuid` that does not correspond to an account in the wallet, surfacing the
6842    /// error via `RewindError::DataSource(CorruptedData)`.
6843    #[test]
6844    fn rewind_to_chain_state_with_unknown_uuid_in_reset_returns_data_source_error() {
6845        let mut st = TestBuilder::new()
6846            .with_data_store_factory(TestDbFactory::default())
6847            .with_account_from_sapling_activation(BlockHash([0; 32]))
6848            .build();
6849
6850        let original_birthday = st.test_account().unwrap().birthday().height();
6851        // Pick a target whose `new_birthday = target + 1` is strictly below every
6852        // account's birthday, so the safeguard fires when the caller hasn't
6853        // acknowledged any reset.
6854        let target_height = original_birthday - 10;
6855
6856        let bogus_uuid = AccountUuid(Uuid::from_u128(0xDEADBEEF));
6857        let result = st.wallet_mut().rewind_to_chain_state(
6858            ChainState::empty(target_height, BlockHash([0; 32])),
6859            HashSet::from([bogus_uuid]),
6860        );
6861
6862        assert_matches!(
6863            result,
6864            Err(RewindError::DataSource(SqliteClientError::CorruptedData(_)))
6865        );
6866    }
6867
6868    /// Creates a test wallet with an account at Sapling activation and five scanned blocks
6869    /// containing Sapling outputs, returning the test state and the height of the first
6870    /// scanned block. Scanning checkpoints every pool's note commitment tree at each scanned
6871    /// height, so the wallet's Sapling, Orchard, and Ironwood checkpoint tables all cover
6872    /// heights `start..start + 5` on return.
6873    #[cfg(feature = "orchard")]
6874    fn wallet_with_scanned_blocks() -> (TestState<BlockCache, TestDb, LocalNetwork>, BlockHeight) {
6875        let mut st = TestBuilder::new()
6876            .with_data_store_factory(TestDbFactory::default())
6877            .with_block_cache(BlockCache::new())
6878            .with_account_from_sapling_activation(BlockHash([0; 32]))
6879            .build();
6880
6881        let dfvk = ExtendedSpendingKey::master(&[]).to_diversifiable_full_viewing_key();
6882        let value = Zatoshis::const_from_u64(10000);
6883        let start_height = st.sapling_activation_height();
6884
6885        st.generate_block_at(
6886            start_height,
6887            BlockHash([0; 32]),
6888            &[FakeCompactOutput::new(
6889                &dfvk,
6890                AddressType::DefaultExternal,
6891                value,
6892            )],
6893            0,
6894            0,
6895            0,
6896            false,
6897        );
6898        for _ in 1..5 {
6899            st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
6900        }
6901        st.scan_cached_blocks(start_height, 5);
6902
6903        (st, start_height)
6904    }
6905
6906    #[cfg(feature = "orchard")]
6907    fn table_row_count(conn: &Connection, table: &str) -> u32 {
6908        conn.query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |row| {
6909            row.get(0)
6910        })
6911        .unwrap()
6912    }
6913
6914    #[cfg(feature = "orchard")]
6915    fn max_block_height(conn: &Connection) -> Option<BlockHeight> {
6916        conn.query_row("SELECT MAX(height) FROM blocks", [], |row| {
6917            row.get::<_, Option<u32>>(0)
6918        })
6919        .unwrap()
6920        .map(BlockHeight::from)
6921    }
6922
6923    #[cfg(feature = "orchard")]
6924    fn rescan_queued_from(conn: &Connection, height: BlockHeight) -> bool {
6925        conn.query_row(
6926            "SELECT EXISTS(SELECT 1 FROM scan_queue WHERE block_range_start = ?)",
6927            [u32::from(height)],
6928            |row| row.get(0),
6929        )
6930        .unwrap()
6931    }
6932
6933    /// `rewind_to_chain_state` must not report `CorruptedData` when the Ironwood tree is
6934    /// empty, e.g. because the `ironwood_shardtree` migration just created its tables on an
6935    /// upgraded wallet (mirroring how `orchard_shardtree` did before it). An empty tree holds
6936    /// no state the truncation must remove, so the rewind must proceed and leave the tree
6937    /// untouched; the missing Ironwood checkpoints are re-established by the queued rescan.
6938    #[test]
6939    #[cfg(feature = "orchard")]
6940    fn rewind_to_chain_state_with_empty_ironwood_tree_succeeds() {
6941        let (mut st, start_height) = wallet_with_scanned_blocks();
6942
6943        // Simulate the post-migration state: the Ironwood tables exist but are empty, even
6944        // though scanning populated the Sapling (and Orchard) checkpoints.
6945        st.wallet()
6946            .conn()
6947            .execute_batch(
6948                "DELETE FROM ironwood_tree_checkpoints;
6949                 DELETE FROM ironwood_tree_shards;
6950                 DELETE FROM ironwood_tree_cap;",
6951            )
6952            .unwrap();
6953
6954        let target_height = start_height + 2;
6955        let result = st.wallet_mut().rewind_to_chain_state(
6956            ChainState::empty(target_height, BlockHash([0; 32])),
6957            HashSet::new(),
6958        );
6959        assert_matches!(result, Ok(()));
6960
6961        // The rewind actually performed the truncation: blocks above the target are gone and
6962        // a rescan starting just above it has been queued. The empty Ironwood tree is
6963        // untouched.
6964        assert_eq!(max_block_height(st.wallet().conn()), Some(target_height));
6965        assert!(rescan_queued_from(st.wallet().conn(), target_height + 1));
6966        assert_eq!(
6967            table_row_count(st.wallet().conn(), "ironwood_tree_checkpoints"),
6968            0
6969        );
6970        assert_eq!(
6971            table_row_count(st.wallet().conn(), "ironwood_tree_shards"),
6972            0
6973        );
6974    }
6975
6976    /// `rewind_to_chain_state` must not report `CorruptedData` when the Orchard tree is
6977    /// empty: the Orchard arm of the per-pool truncation tolerance must behave identically
6978    /// to the Ironwood arm exercised by the other tests here.
6979    #[test]
6980    #[cfg(feature = "orchard")]
6981    fn rewind_to_chain_state_with_empty_orchard_tree_succeeds() {
6982        let (mut st, start_height) = wallet_with_scanned_blocks();
6983
6984        // Simulate the post-migration state: the Orchard tables exist but are empty, even
6985        // though scanning populated the Sapling (and Ironwood) checkpoints.
6986        st.wallet()
6987            .conn()
6988            .execute_batch(
6989                "DELETE FROM orchard_tree_checkpoints;
6990                 DELETE FROM orchard_tree_shards;
6991                 DELETE FROM orchard_tree_cap;",
6992            )
6993            .unwrap();
6994
6995        let target_height = start_height + 2;
6996        let result = st.wallet_mut().rewind_to_chain_state(
6997            ChainState::empty(target_height, BlockHash([0; 32])),
6998            HashSet::new(),
6999        );
7000        assert_matches!(result, Ok(()));
7001
7002        assert_eq!(max_block_height(st.wallet().conn()), Some(target_height));
7003        assert_eq!(
7004            table_row_count(st.wallet().conn(), "orchard_tree_checkpoints"),
7005            0
7006        );
7007    }
7008
7009    /// An Ironwood tree with checkpoints both above and below the truncation height but none
7010    /// at it must still be treated as corruption: the tree cannot be truncated to the height
7011    /// consistently, and its state genuinely diverges from the pools that determined that
7012    /// height.
7013    #[test]
7014    #[cfg(feature = "orchard")]
7015    fn rewind_to_chain_state_with_straddling_ironwood_checkpoints_errors() {
7016        let (mut st, start_height) = wallet_with_scanned_blocks();
7017        let target_height = start_height + 2;
7018
7019        // Remove just the Ironwood checkpoint at the target height, leaving checkpoint rows
7020        // both above and below it in place. The Ironwood checkpoint coverage now genuinely
7021        // diverges from Sapling's.
7022        st.wallet()
7023            .conn()
7024            .execute(
7025                "DELETE FROM ironwood_tree_checkpoints WHERE checkpoint_id = ?",
7026                [u32::from(target_height)],
7027            )
7028            .unwrap();
7029
7030        let result = st.wallet_mut().rewind_to_chain_state(
7031            ChainState::empty(target_height, BlockHash([0; 32])),
7032            HashSet::new(),
7033        );
7034
7035        assert_matches!(
7036            result,
7037            Err(RewindError::DataSource(SqliteClientError::CorruptedData(_)))
7038        );
7039    }
7040
7041    /// `rewind_to_chain_state` must not report `CorruptedData` when the Ironwood tree is
7042    /// non-empty but lags the truncation height: the state of a wallet whose NU6.3 rescan has
7043    /// begun backfilling Ironwood from activation but has not yet reached the rewind target.
7044    /// A lagging tree holds no state above the truncation height, so the rewind must proceed
7045    /// and preserve the tree's existing (below-target) data, which no rescan would re-create.
7046    #[test]
7047    #[cfg(feature = "orchard")]
7048    fn rewind_to_chain_state_with_lagging_ironwood_tree_succeeds() {
7049        let (mut st, start_height) = wallet_with_scanned_blocks();
7050
7051        // Simulate an in-progress NU6.3 rescan: truncate *only* the Ironwood tree back to an
7052        // early checkpoint, so both its checkpoint and shard rows lag behind Sapling and
7053        // Orchard (which remain scanned to the tip).
7054        let ironwood_lag_height = start_height + 1;
7055        st.wallet_mut()
7056            .with_ironwood_tree_mut(|tree| {
7057                assert!(tree.truncate_to_checkpoint(&ironwood_lag_height)?);
7058                Ok::<_, ShardTreeError<crate::wallet::commitment_tree::Error>>(())
7059            })
7060            .unwrap();
7061
7062        let target_height = start_height + 2;
7063        let result = st.wallet_mut().rewind_to_chain_state(
7064            ChainState::empty(target_height, BlockHash([0; 32])),
7065            HashSet::new(),
7066        );
7067        assert_matches!(result, Ok(()));
7068
7069        // Blocks were truncated to the target, and the lagging Ironwood tree's data below
7070        // the target was preserved.
7071        assert_eq!(max_block_height(st.wallet().conn()), Some(target_height));
7072        assert_eq!(
7073            st.wallet()
7074                .conn()
7075                .query_row(
7076                    "SELECT MAX(checkpoint_id) FROM ironwood_tree_checkpoints",
7077                    [],
7078                    |row| row.get::<_, Option<u32>>(0),
7079                )
7080                .unwrap()
7081                .map(BlockHeight::from),
7082            Some(ironwood_lag_height),
7083        );
7084    }
7085
7086    /// `rewind_to_chain_state` must not report `CorruptedData` when every Ironwood checkpoint
7087    /// lies *above* the rewind target: the state of an upgraded wallet whose post-migration
7088    /// rescan has so far only scanned tip-priority blocks near the chain tip. The truncation
7089    /// empties the Ironwood tree (its entire scanned contents postdate the target, and this
7090    /// wallet holds no completed subtree roots — for those, see
7091    /// `rewind_preserves_ironwood_subtree_roots_at_or_below_target`), and the queued rescan
7092    /// re-creates it.
7093    #[test]
7094    #[cfg(feature = "orchard")]
7095    fn rewind_to_chain_state_with_tip_only_ironwood_tree_empties_it() {
7096        let (mut st, start_height) = wallet_with_scanned_blocks();
7097        let target_height = start_height + 2;
7098
7099        // Simulate the state after a tip-priority rescan on a freshly-migrated wallet: the
7100        // Ironwood table retains checkpoints only above the rewind target.
7101        st.wallet()
7102            .conn()
7103            .execute(
7104                "DELETE FROM ironwood_tree_checkpoints WHERE checkpoint_id <= ?",
7105                [u32::from(target_height)],
7106            )
7107            .unwrap();
7108
7109        let result = st.wallet_mut().rewind_to_chain_state(
7110            ChainState::empty(target_height, BlockHash([0; 32])),
7111            HashSet::new(),
7112        );
7113        assert_matches!(result, Ok(()));
7114
7115        // The Ironwood tree was emptied (no checkpoint at or below the target exists to
7116        // truncate to), and the rescan that re-creates it has been queued.
7117        assert_eq!(max_block_height(st.wallet().conn()), Some(target_height));
7118        assert!(rescan_queued_from(st.wallet().conn(), target_height + 1));
7119        assert_eq!(
7120            table_row_count(st.wallet().conn(), "ironwood_tree_checkpoints"),
7121            0
7122        );
7123        assert_eq!(
7124            table_row_count(st.wallet().conn(), "ironwood_tree_shards"),
7125            0
7126        );
7127        assert_eq!(table_row_count(st.wallet().conn(), "ironwood_tree_cap"), 0);
7128    }
7129
7130    /// A rewind that would discard a pool tree's scanned state is refused when the pool has
7131    /// notes with recorded witness positions at or below the rewind target, since the
7132    /// requeued rescan would not re-create their witness data. This is a valid wallet state,
7133    /// not corruption, so it must surface as `RequestedRewindInvalid` rather than
7134    /// `CorruptedData`.
7135    #[test]
7136    #[cfg(feature = "orchard")]
7137    fn rewind_to_chain_state_with_witness_destroying_truncation_errors() {
7138        let mut st = TestBuilder::new()
7139            .with_data_store_factory(TestDbFactory::default())
7140            .with_block_cache(BlockCache::new())
7141            .with_account_from_sapling_activation(BlockHash([0; 32]))
7142            .build();
7143
7144        // Pay the wallet's own account so that scanning records a Sapling note with a
7145        // witness position at `start_height`.
7146        let dfvk = st.test_account_sapling().unwrap().clone();
7147        let value = Zatoshis::const_from_u64(10000);
7148        let start_height = st.sapling_activation_height();
7149        st.generate_block_at(
7150            start_height,
7151            BlockHash([0; 32]),
7152            &[FakeCompactOutput::new(
7153                &dfvk,
7154                AddressType::DefaultExternal,
7155                value,
7156            )],
7157            0,
7158            0,
7159            0,
7160            false,
7161        );
7162        for _ in 1..5 {
7163            st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
7164        }
7165        st.scan_cached_blocks(start_height, 5);
7166
7167        // Leave the Sapling tree with checkpoints only above the rewind target, so the
7168        // rewind would have to discard its scanned state — including the witness of the
7169        // note received at `start_height`.
7170        let target_height = start_height + 2;
7171        st.wallet()
7172            .conn()
7173            .execute(
7174                "DELETE FROM sapling_tree_checkpoints WHERE checkpoint_id <= ?",
7175                [u32::from(target_height)],
7176            )
7177            .unwrap();
7178
7179        let result = st.wallet_mut().rewind_to_chain_state(
7180            ChainState::empty(target_height, BlockHash([0; 32])),
7181            HashSet::new(),
7182        );
7183
7184        assert_matches!(
7185            result,
7186            Err(RewindError::DataSource(
7187                SqliteClientError::RequestedRewindInvalid { .. }
7188            ))
7189        );
7190    }
7191
7192    /// When a truncation must discard a pool tree's scanned state, roots of subtrees
7193    /// completed at or below the truncation height (as downloaded during fast sync) are
7194    /// preserved: discarding them would leave the wallet unable to construct witnesses
7195    /// spanning those subtrees until they had been re-downloaded.
7196    #[test]
7197    #[cfg(feature = "orchard")]
7198    fn rewind_preserves_ironwood_subtree_roots_at_or_below_target() {
7199        let (mut st, start_height) = wallet_with_scanned_blocks();
7200        let target_height = start_height + 2;
7201
7202        // Simulate the post-migration state, then a fast-sync download of the root of a
7203        // subtree completed at or below the rewind target, followed by a tip-priority rescan
7204        // that has established a checkpoint only above the target.
7205        st.wallet()
7206            .conn()
7207            .execute_batch(
7208                "DELETE FROM ironwood_tree_checkpoints;
7209                 DELETE FROM ironwood_tree_shards;
7210                 DELETE FROM ironwood_tree_cap;",
7211            )
7212            .unwrap();
7213        st.wallet_mut()
7214            .put_ironwood_subtree_roots(
7215                0,
7216                &[CommitmentTreeRoot::from_parts(
7217                    start_height,
7218                    MerkleHashOrchard::empty_leaf(),
7219                )],
7220            )
7221            .unwrap();
7222        st.wallet()
7223            .conn()
7224            .execute(
7225                "INSERT INTO ironwood_tree_checkpoints (checkpoint_id, position)
7226                 VALUES (?, NULL)",
7227                [u32::from(target_height + 1)],
7228            )
7229            .unwrap();
7230
7231        let result = st.wallet_mut().rewind_to_chain_state(
7232            ChainState::empty(target_height, BlockHash([0; 32])),
7233            HashSet::new(),
7234        );
7235        assert_matches!(result, Ok(()));
7236
7237        // The above-target checkpoint is gone, but the completed subtree root (and the cap
7238        // built from it) survives the reset.
7239        assert_eq!(
7240            table_row_count(st.wallet().conn(), "ironwood_tree_checkpoints"),
7241            0
7242        );
7243        assert_eq!(
7244            st.wallet()
7245                .conn()
7246                .query_row(
7247                    "SELECT shard_index, subtree_end_height, root_hash IS NOT NULL
7248                     FROM ironwood_tree_shards",
7249                    [],
7250                    |row| {
7251                        Ok((
7252                            row.get::<_, u64>(0)?,
7253                            row.get::<_, u32>(1)?,
7254                            row.get::<_, bool>(2)?,
7255                        ))
7256                    },
7257                )
7258                .unwrap(),
7259            (0, u32::from(start_height), true),
7260        );
7261        assert_eq!(table_row_count(st.wallet().conn(), "ironwood_tree_cap"), 1);
7262    }
7263
7264    /// `truncate_to_height` applies the same per-pool truncation tolerances as
7265    /// `rewind_to_chain_state` (via `select_truncation_height`), so a wallet state that the
7266    /// rewind path tolerates must not remain wedged when truncating via this entry point.
7267    #[test]
7268    #[cfg(feature = "orchard")]
7269    fn truncate_to_height_with_tip_only_ironwood_tree_empties_it() {
7270        let (mut st, start_height) = wallet_with_scanned_blocks();
7271        let target_height = start_height + 2;
7272
7273        st.wallet()
7274            .conn()
7275            .execute(
7276                "DELETE FROM ironwood_tree_checkpoints WHERE checkpoint_id <= ?",
7277                [u32::from(target_height)],
7278            )
7279            .unwrap();
7280
7281        let result = st.wallet_mut().truncate_to_height(target_height);
7282        assert_matches!(result, Ok(h) if h == target_height);
7283
7284        assert_eq!(max_block_height(st.wallet().conn()), Some(target_height));
7285        assert_eq!(
7286            table_row_count(st.wallet().conn(), "ironwood_tree_checkpoints"),
7287            0
7288        );
7289        assert_eq!(
7290            table_row_count(st.wallet().conn(), "ironwood_tree_shards"),
7291            0
7292        );
7293    }
7294
7295    /// The name of the received-note table for a pool, written out rather than derived from
7296    /// `table_constants`, so that these tests do not assert through the same mapping the code
7297    /// under test uses.
7298    fn received_notes_table(pool: ShieldedPool) -> &'static str {
7299        match pool {
7300            ShieldedPool::Sapling => "sapling_received_notes",
7301            #[cfg(feature = "orchard")]
7302            ShieldedPool::Orchard => "orchard_received_notes",
7303            #[cfg(feature = "orchard")]
7304            ShieldedPool::Ironwood => "ironwood_received_notes",
7305            #[cfg(not(feature = "orchard"))]
7306            other => panic!("pool {other:?} is unsupported without the `orchard` feature"),
7307        }
7308    }
7309
7310    /// Reproduces the state a wallet is left in when it scans a note before it can link the
7311    /// transaction's spends to itself: one transaction, one received note recorded with
7312    /// `is_change = 0` under `key_scope`, and one `sent_notes` row recording that
7313    /// `funding_account` paid for the transaction.
7314    ///
7315    /// Returns the row id of the transaction.
7316    fn seed_unflagged_received_note(
7317        conn: &rusqlite::Connection,
7318        pool: ShieldedPool,
7319        receiving_account: i64,
7320        funding_account: i64,
7321        key_scope: KeyScope,
7322    ) -> i64 {
7323        // Placeholders for columns the repair statement never reads. They exist only to
7324        // satisfy the tables' NOT NULL constraints, so any well-formed value will do.
7325        const TX_ROW_ID: i64 = 1;
7326        const TXID: [u8; 32] = [7; 32];
7327        const OBSERVED_HEIGHT: i64 = 0;
7328        const OUTPUT_INDEX: i64 = 0;
7329        const DIVERSIFIER: [u8; 11] = [0; 11];
7330        const NOTE_VALUE_ZATS: i64 = 1;
7331        const NOTE_COMPONENT: [u8; 32] = [0; 32];
7332        // `orchard_received_notes.note_version` defaults, but the Ironwood column does not,
7333        // so it is supplied explicitly for both.
7334        #[cfg(feature = "orchard")]
7335        const NOTE_VERSION: i64 = 2;
7336        // The pool a `sent_notes` row is attributed to is irrelevant here: the repair
7337        // statement correlates on transaction and account only.
7338        const SENT_OUTPUT_POOL: i64 = 0;
7339
7340        conn.execute(
7341            "INSERT INTO transactions (id_tx, txid, min_observed_height)
7342             VALUES (:id_tx, :txid, :min_observed_height)",
7343            named_params! {
7344                ":id_tx": TX_ROW_ID,
7345                ":txid": &TXID[..],
7346                ":min_observed_height": OBSERVED_HEIGHT,
7347            },
7348        )
7349        .unwrap();
7350
7351        match pool {
7352            ShieldedPool::Sapling => {
7353                conn.execute(
7354                    "INSERT INTO sapling_received_notes
7355                     (transaction_id, output_index, account_id, diversifier, value, rcm,
7356                      is_change, recipient_key_scope)
7357                     VALUES (:tx, :output_index, :account, :diversifier, :value,
7358                             :note_component, :is_change, :key_scope)",
7359                    named_params! {
7360                        ":tx": TX_ROW_ID,
7361                        ":output_index": OUTPUT_INDEX,
7362                        ":account": receiving_account,
7363                        ":diversifier": &DIVERSIFIER[..],
7364                        ":value": NOTE_VALUE_ZATS,
7365                        ":note_component": &NOTE_COMPONENT[..],
7366                        ":is_change": false,
7367                        ":key_scope": key_scope.encode(),
7368                    },
7369                )
7370                .unwrap();
7371            }
7372            // Ironwood notes are Orchard-shaped, so the two tables take the same columns.
7373            #[cfg(feature = "orchard")]
7374            ShieldedPool::Orchard | ShieldedPool::Ironwood => {
7375                conn.execute(
7376                    &format!(
7377                        "INSERT INTO {} (transaction_id, action_index, account_id, diversifier,
7378                                         value, rho, rseed, note_version, is_change,
7379                                         recipient_key_scope)
7380                         VALUES (:tx, :output_index, :account, :diversifier, :value,
7381                                 :note_component, :note_component, :note_version, :is_change,
7382                                 :key_scope)",
7383                        received_notes_table(pool)
7384                    ),
7385                    named_params! {
7386                        ":tx": TX_ROW_ID,
7387                        ":output_index": OUTPUT_INDEX,
7388                        ":account": receiving_account,
7389                        ":diversifier": &DIVERSIFIER[..],
7390                        ":value": NOTE_VALUE_ZATS,
7391                        ":note_component": &NOTE_COMPONENT[..],
7392                        ":note_version": NOTE_VERSION,
7393                        ":is_change": false,
7394                        ":key_scope": key_scope.encode(),
7395                    },
7396                )
7397                .unwrap();
7398            }
7399            #[cfg(not(feature = "orchard"))]
7400            other => panic!("pool {other:?} is unsupported without the `orchard` feature"),
7401        }
7402
7403        conn.execute(
7404            "INSERT INTO sent_notes
7405             (transaction_id, output_pool, output_index, from_account_id, value)
7406             VALUES (:tx, :output_pool, :output_index, :from_account, :value)",
7407            named_params! {
7408                ":tx": TX_ROW_ID,
7409                ":output_pool": SENT_OUTPUT_POOL,
7410                ":output_index": OUTPUT_INDEX,
7411                ":from_account": funding_account,
7412                ":value": NOTE_VALUE_ZATS,
7413            },
7414        )
7415        .unwrap();
7416
7417        TX_ROW_ID
7418    }
7419
7420    fn only_account_id(conn: &rusqlite::Connection) -> i64 {
7421        conn.query_row("SELECT id FROM accounts", [], |row| row.get::<_, i64>(0))
7422            .unwrap()
7423    }
7424
7425    fn is_change(conn: &rusqlite::Connection, pool: ShieldedPool) -> bool {
7426        conn.query_row(
7427            &format!("SELECT is_change FROM {}", received_notes_table(pool)),
7428            [],
7429            |row| row.get::<_, bool>(0),
7430        )
7431        .unwrap()
7432    }
7433
7434    /// A note received on the account's internal address, in a transaction that same account
7435    /// funded, is change. The wallet cannot always know this when the note is first recorded,
7436    /// because linking the transaction's spends requires the spent notes to already be
7437    /// present, so `flag_previously_received_change` back-fills the classification when the
7438    /// `sent_notes` rows are written.
7439    ///
7440    /// This must hold for every pool that has a received-note table. A pool left out of the
7441    /// repair keeps the wrong classification forever, since `is_change` is only ever raised
7442    /// and nothing revisits the row: the note is then reported as an ordinary received output
7443    /// by `v_transactions` and `v_tx_outputs`, which surfaces the account's own change to the
7444    /// user as a recipient of their own transaction.
7445    fn assert_internal_scope_note_becomes_change(pool: ShieldedPool) {
7446        let mut st = TestBuilder::new()
7447            .with_data_store_factory(TestDbFactory::default())
7448            .with_account_from_sapling_activation(BlockHash([0; 32]))
7449            .build();
7450
7451        let account_id = only_account_id(st.wallet().conn());
7452        let tx = st.wallet_mut().conn_mut().transaction().unwrap();
7453
7454        let tx_row_id =
7455            seed_unflagged_received_note(&tx, pool, account_id, account_id, KeyScope::INTERNAL);
7456        assert!(
7457            !is_change(&tx, pool),
7458            "{pool:?}: precondition, the note starts out unflagged"
7459        );
7460
7461        flag_previously_received_change(&tx, TxRef(tx_row_id)).unwrap();
7462
7463        assert!(
7464            is_change(&tx, pool),
7465            "{pool:?}: an internal-scope note in a self-funded transaction must be flagged \
7466             as change"
7467        );
7468    }
7469
7470    #[test]
7471    fn flags_previously_received_sapling_change() {
7472        assert_internal_scope_note_becomes_change(ShieldedPool::Sapling);
7473    }
7474
7475    #[test]
7476    #[cfg(feature = "orchard")]
7477    fn flags_previously_received_orchard_change() {
7478        assert_internal_scope_note_becomes_change(ShieldedPool::Orchard);
7479    }
7480
7481    #[test]
7482    #[cfg(feature = "orchard")]
7483    fn flags_previously_received_ironwood_change() {
7484        assert_internal_scope_note_becomes_change(ShieldedPool::Ironwood);
7485    }
7486
7487    /// The repair is restricted to the internal key scope. A note received on the account's
7488    /// external address is a payment the user made to themselves, not change, even though the
7489    /// same account funded the transaction, and it must keep its classification so that it
7490    /// remains visible as an output of the transaction.
7491    #[test]
7492    #[cfg(feature = "orchard")]
7493    fn does_not_flag_external_scope_notes_as_change() {
7494        let pool = ShieldedPool::Ironwood;
7495        let mut st = TestBuilder::new()
7496            .with_data_store_factory(TestDbFactory::default())
7497            .with_account_from_sapling_activation(BlockHash([0; 32]))
7498            .build();
7499
7500        let account_id = only_account_id(st.wallet().conn());
7501        let tx = st.wallet_mut().conn_mut().transaction().unwrap();
7502
7503        let tx_row_id =
7504            seed_unflagged_received_note(&tx, pool, account_id, account_id, KeyScope::EXTERNAL);
7505
7506        flag_previously_received_change(&tx, TxRef(tx_row_id)).unwrap();
7507
7508        assert!(
7509            !is_change(&tx, pool),
7510            "an external-scope note must not be reclassified as change"
7511        );
7512    }
7513}