zcash_client_sqlite/
lib.rs

1//! *An SQLite-based Zcash light client.*
2//!
3//! `zcash_client_sqlite` contains complete SQLite-based implementations of the [`WalletRead`],
4//! [`WalletWrite`], and [`BlockSource`] traits from the [`zcash_client_backend`] crate. In
5//! combination with [`zcash_client_backend`], it provides a full implementation of a SQLite-backed
6//! client for the Zcash network.
7//!
8//! # Design
9//!
10//! The light client is built around two SQLite databases:
11//!
12//! - A cache database, used to inform the light client about new [`CompactBlock`]s. It is
13//!   read-only within all light client APIs *except* for [`init_cache_database`] which
14//!   can be used to initialize the database.
15//!
16//! - A data database, where the light client's state is stored. It is read-write within
17//!   the light client APIs, and **assumed to be read-only outside these APIs**. Callers
18//!   **MUST NOT** write to the database without using these APIs. Callers **MAY** read
19//!   the database directly in order to extract information for display to users.
20//!
21//! ## Feature flags
22#![doc = document_features::document_features!()]
23//!
24//! [`WalletRead`]: zcash_client_backend::data_api::WalletRead
25//! [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite
26//! [`BlockSource`]: zcash_client_backend::data_api::chain::BlockSource
27//! [`CompactBlock`]: zcash_client_backend::proto::compact_formats::CompactBlock
28//! [`init_cache_database`]: crate::chain::init::init_cache_database
29
30#![cfg_attr(docsrs, feature(doc_cfg))]
31#![cfg_attr(docsrs, feature(doc_auto_cfg))]
32// Catch documentation errors caused by code changes.
33#![deny(rustdoc::broken_intra_doc_links)]
34
35use incrementalmerkletree::{Marking, Position, Retention};
36use nonempty::NonEmpty;
37use rusqlite::{self, Connection};
38use secrecy::{ExposeSecret, SecretVec};
39use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
40use std::{
41    borrow::{Borrow, BorrowMut},
42    cmp::{max, min},
43    collections::HashMap,
44    convert::AsRef,
45    fmt,
46    num::NonZeroU32,
47    ops::Range,
48    path::Path,
49};
50use subtle::ConditionallySelectable;
51use tracing::{debug, trace, warn};
52use util::Clock;
53use uuid::Uuid;
54
55use zcash_client_backend::{
56    data_api::{
57        self,
58        chain::{BlockSource, ChainState, CommitmentTreeRoot},
59        scanning::{ScanPriority, ScanRange},
60        Account, AccountBirthday, AccountMeta, AccountPurpose, AccountSource, AddressInfo,
61        BlockMetadata, DecryptedTransaction, InputSource, NoteFilter, NullifierQuery, ScannedBlock,
62        SeedRelevance, SentTransaction, SpendableNotes, TransactionDataRequest,
63        WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, Zip32Derivation,
64        SAPLING_SHARD_HEIGHT,
65    },
66    proto::compact_formats::CompactBlock,
67    wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput},
68    TransferType,
69};
70use zcash_keys::{
71    address::UnifiedAddress,
72    keys::{ReceiverRequirement, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
73};
74use zcash_primitives::{
75    block::BlockHash,
76    transaction::{Transaction, TxId},
77};
78use zcash_protocol::{
79    consensus::{self, BlockHeight},
80    memo::Memo,
81    value::Zatoshis,
82    ShieldedProtocol,
83};
84use zip32::{fingerprint::SeedFingerprint, DiversifierIndex};
85
86use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
87use wallet::{
88    commitment_tree::{self, put_shard_roots},
89    common::spendable_notes_meta,
90    scanning::replace_queue_entries,
91    upsert_address, SubtreeProgressEstimator,
92};
93
94#[cfg(feature = "orchard")]
95use {
96    incrementalmerkletree::frontier::Frontier, shardtree::store::Checkpoint,
97    std::collections::BTreeMap, zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT,
98};
99
100#[cfg(feature = "transparent-inputs")]
101use {
102    crate::wallet::transparent::ephemeral::schedule_ephemeral_address_checks,
103    ::transparent::{address::TransparentAddress, bundle::OutPoint, keys::NonHardenedChildIndex},
104    std::collections::BTreeSet,
105    zcash_client_backend::wallet::TransparentAddressMetadata,
106    zcash_keys::encoding::AddressCodec,
107};
108
109#[cfg(feature = "multicore")]
110use maybe_rayon::{
111    prelude::{IndexedParallelIterator, ParallelIterator},
112    slice::ParallelSliceMut,
113};
114
115#[cfg(any(test, feature = "test-dependencies"))]
116use {
117    rusqlite::named_params,
118    zcash_client_backend::data_api::{testing::TransactionSummary, OutputOfSentTx, WalletTest},
119    zcash_keys::address::Address,
120};
121
122#[cfg(any(test, feature = "test-dependencies", feature = "transparent-inputs"))]
123use crate::wallet::encoding::KeyScope;
124
125#[cfg(any(test, feature = "test-dependencies", not(feature = "orchard")))]
126use zcash_protocol::PoolType;
127
128/// `maybe-rayon` doesn't provide this as a fallback, so we have to.
129#[cfg(not(feature = "multicore"))]
130trait ParallelSliceMut<T> {
131    fn par_chunks_mut(&mut self, chunk_size: usize) -> std::slice::ChunksMut<'_, T>;
132}
133#[cfg(not(feature = "multicore"))]
134impl<T> ParallelSliceMut<T> for [T] {
135    fn par_chunks_mut(&mut self, chunk_size: usize) -> std::slice::ChunksMut<'_, T> {
136        self.chunks_mut(chunk_size)
137    }
138}
139
140#[cfg(feature = "unstable")]
141use {
142    crate::chain::{fsblockdb_with_blocks, BlockMeta},
143    std::path::PathBuf,
144    std::{fs, io},
145};
146
147pub mod chain;
148pub mod error;
149pub mod util;
150pub mod wallet;
151
152#[cfg(test)]
153mod testing;
154
155/// The maximum number of blocks the wallet is allowed to rewind. This is
156/// consistent with the bound in zcashd, and allows block data deeper than
157/// this delta from the chain tip to be pruned.
158pub(crate) const PRUNING_DEPTH: u32 = 100;
159
160/// The number of blocks to verify ahead when the chain tip is updated.
161pub(crate) const VERIFY_LOOKAHEAD: u32 = 10;
162
163pub(crate) const SAPLING_TABLES_PREFIX: &str = "sapling";
164
165#[cfg(feature = "orchard")]
166pub(crate) const ORCHARD_TABLES_PREFIX: &str = "orchard";
167
168#[cfg(not(feature = "orchard"))]
169pub(crate) const UA_ORCHARD: ReceiverRequirement = ReceiverRequirement::Omit;
170#[cfg(feature = "orchard")]
171pub(crate) const UA_ORCHARD: ReceiverRequirement = ReceiverRequirement::Require;
172
173#[cfg(not(feature = "transparent-inputs"))]
174pub(crate) const UA_TRANSPARENT: ReceiverRequirement = ReceiverRequirement::Omit;
175#[cfg(feature = "transparent-inputs")]
176pub(crate) const UA_TRANSPARENT: ReceiverRequirement = ReceiverRequirement::Require;
177
178/// Unique identifier for a specific account tracked by a [`WalletDb`].
179///
180/// Account identifiers are "one-way stable": a given identifier always points to a
181/// specific viewing key within a specific [`WalletDb`] instance, but the same viewing key
182/// may have multiple account identifiers over time. In particular, this crate upholds the
183/// following properties:
184///
185/// - When an account starts being tracked within a [`WalletDb`] instance (via APIs like
186///   [`WalletWrite::create_account`], [`WalletWrite::import_account_hd`], or
187///   [`WalletWrite::import_account_ufvk`]), a new `AccountUuid` is generated.
188/// - If an `AccountUuid` is present within a [`WalletDb`], it always points to the same
189///   account.
190///
191/// What this means is that account identifiers are not stable across "wallet recreation
192/// events". Examples of these include:
193/// - Restoring a wallet from a backed-up seed.
194/// - Importing the same viewing key into two different wallet instances.
195#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
196#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
197pub struct AccountUuid(#[cfg_attr(feature = "serde", serde(with = "uuid::serde::compact"))] Uuid);
198
199impl ConditionallySelectable for AccountUuid {
200    fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
201        AccountUuid(Uuid::from_u128(
202            ConditionallySelectable::conditional_select(&a.0.as_u128(), &b.0.as_u128(), choice),
203        ))
204    }
205}
206
207impl AccountUuid {
208    /// Constructs an `AccountUuid` from a bare [`Uuid`] value.
209    ///
210    /// The resulting identifier is not guaranteed to correspond to any account stored in
211    /// a [`WalletDb`].
212    pub fn from_uuid(value: Uuid) -> Self {
213        AccountUuid(value)
214    }
215
216    /// Exposes the opaque account identifier from its typesafe wrapper.
217    pub fn expose_uuid(&self) -> Uuid {
218        self.0
219    }
220}
221
222/// A typesafe wrapper for the primary key identifier for a row in the `accounts` table.
223///
224/// This is an ephemeral value for efficiently and generically working with accounts in a
225/// [`WalletDb`]. To reference accounts in external contexts, use [`AccountUuid`].
226#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
227pub(crate) struct AccountRef(i64);
228
229/// This implementation is retained under `#[cfg(test)]` for pre-AccountUuid testing.
230#[cfg(test)]
231impl ConditionallySelectable for AccountRef {
232    fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
233        AccountRef(ConditionallySelectable::conditional_select(
234            &a.0, &b.0, choice,
235        ))
236    }
237}
238
239/// An opaque type for received note identifiers.
240#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
241pub struct ReceivedNoteId(pub(crate) ShieldedProtocol, pub(crate) i64);
242
243impl fmt::Display for ReceivedNoteId {
244    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
245        match self {
246            ReceivedNoteId(protocol, id) => write!(f, "Received {:?} Note: {}", protocol, id),
247        }
248    }
249}
250
251/// A newtype wrapper for sqlite primary key values for the utxos table.
252#[derive(Debug, Copy, Clone, PartialEq, Eq)]
253pub struct UtxoId(pub i64);
254
255/// A newtype wrapper for sqlite primary key values for the transactions table.
256#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
257struct TxRef(pub i64);
258
259/// A newtype wrapper for sqlite primary key values for the addresses table.
260#[derive(Debug, Copy, Clone, PartialEq, Eq)]
261struct AddressRef(pub(crate) i64);
262
263/// A data structure that can be used to configure custom gap limits for use in transparent address
264/// rotation.
265#[derive(Debug, Copy, Clone, PartialEq, Eq)]
266#[cfg(feature = "transparent-inputs")]
267pub struct GapLimits {
268    external: u32,
269    internal: u32,
270    ephemeral: u32,
271}
272
273#[cfg(feature = "transparent-inputs")]
274impl GapLimits {
275    /// Constructs a new `GapLimits` value from its constituent parts.
276    ///
277    /// The gap limits recommended for use with this crate are supplied by the [`Default`]
278    /// implementation for this type.
279    ///
280    /// This constructor is only available under the `unstable` feature, as it is not recommended
281    /// for general use.
282    #[cfg(any(test, feature = "test-dependencies", feature = "unstable"))]
283    pub fn from_parts(external: u32, internal: u32, ephemeral: u32) -> Self {
284        Self {
285            external,
286            internal,
287            ephemeral,
288        }
289    }
290
291    pub(crate) fn external(&self) -> u32 {
292        self.external
293    }
294
295    pub(crate) fn internal(&self) -> u32 {
296        self.internal
297    }
298
299    pub(crate) fn ephemeral(&self) -> u32 {
300        self.ephemeral
301    }
302}
303
304/// The default gap limits supported by this implementation are:
305///
306/// - external addresses: 10
307/// - transparent internal (change) addresses: 5
308/// - ephemeral addresses: 5
309///
310/// These limits are chosen with the following rationale:
311/// - At present, many wallets query light wallet servers with a set of addresses, because querying
312///   for each address independently and in a fashion that is not susceptible to clustering via
313///   timing correlation leads to undesirable delays in discovery of received funds. As such, it is
314///   desirable to minimize the number of addresses that can be "linked", i.e. understood by the
315///   light wallet server to all belong to the same wallet.
316/// - For transparent change addresses and ephemeral addresses, it is always expected that an
317///   address will receive funds immediately following its generation except in the case of wallet
318///   failure.
319/// - For externally-scoped transparent addresses, it is desirable to use a slightly larger gap
320///   limit to account for addresses that were shared with counterparties never having been used.
321///   However, we don't want to use the full 20-address gap limit space because it's possible that
322///   in the future, changes to the light wallet protocol will obviate the need to query for UTXOs
323///   in a fashion that links those addresses. In such a circumstance, the gap limit will be
324///   adjusted upward and address rotation should then choose an address that is outside the
325///   current gap limit; after that change, newly generated addresses will not be exposed as
326///   linked in the view of the light wallet server.
327#[cfg(feature = "transparent-inputs")]
328impl Default for GapLimits {
329    fn default() -> Self {
330        Self {
331            external: 10,
332            internal: 5,
333            ephemeral: 5,
334        }
335    }
336}
337
338#[cfg(all(
339    any(test, feature = "test-dependencies"),
340    feature = "transparent-inputs"
341))]
342impl From<GapLimits> for zcash_client_backend::data_api::testing::transparent::GapLimits {
343    fn from(value: GapLimits) -> Self {
344        zcash_client_backend::data_api::testing::transparent::GapLimits::new(
345            value.external,
346            value.internal,
347            value.ephemeral,
348        )
349    }
350}
351
352#[cfg(all(
353    any(test, feature = "test-dependencies"),
354    feature = "transparent-inputs"
355))]
356impl From<zcash_client_backend::data_api::testing::transparent::GapLimits> for GapLimits {
357    fn from(value: zcash_client_backend::data_api::testing::transparent::GapLimits) -> Self {
358        GapLimits::from_parts(value.external(), value.internal(), value.ephemeral())
359    }
360}
361
362/// A wrapper for the SQLite connection to the wallet database, along with a capability to read the
363/// system from the clock. A `WalletDb` encapsulates the full set of capabilities that are required
364/// in order to implement the [`WalletRead`], [`WalletWrite`] and [`WalletCommitmentTrees`] traits.
365pub struct WalletDb<C, P, CL, R> {
366    conn: C,
367    params: P,
368    clock: CL,
369    rng: R,
370    #[cfg(feature = "transparent-inputs")]
371    gap_limits: GapLimits,
372}
373
374/// A wrapper for a SQLite transaction affecting the wallet database.
375pub struct SqlTransaction<'conn>(pub(crate) &'conn rusqlite::Transaction<'conn>);
376
377impl Borrow<rusqlite::Connection> for SqlTransaction<'_> {
378    fn borrow(&self) -> &rusqlite::Connection {
379        self.0
380    }
381}
382
383impl<P, CL, R> WalletDb<Connection, P, CL, R> {
384    /// Construct a [`WalletDb`] instance that connects to the wallet database stored at the
385    /// specified path.
386    ///
387    /// ## Parameters
388    /// - `path`: The path to the SQLite database used to store wallet data.
389    /// - `params`: Parameters associated with the Zcash network that the wallet will connect to.
390    /// - `clock`: The clock to use in the case that the backend needs access to the system time.
391    /// - `rng`: The random number generation capability to be exposed by the created `WalletDb`
392    ///   instance.
393    pub fn for_path<F: AsRef<Path>>(
394        path: F,
395        params: P,
396        clock: CL,
397        rng: R,
398    ) -> Result<Self, rusqlite::Error> {
399        Connection::open(path).and_then(move |conn| {
400            rusqlite::vtab::array::load_module(&conn)?;
401            Ok(WalletDb {
402                conn,
403                params,
404                clock,
405                rng,
406                #[cfg(feature = "transparent-inputs")]
407                gap_limits: GapLimits::default(),
408            })
409        })
410    }
411}
412
413#[cfg(feature = "transparent-inputs")]
414impl<C, P, CL, R> WalletDb<C, P, CL, R> {
415    /// Sets the gap limits to be used by the wallet in transparent address generation.
416    pub fn with_gap_limits(mut self, gap_limits: GapLimits) -> Self {
417        self.gap_limits = gap_limits;
418        self
419    }
420}
421
422impl<C: Borrow<rusqlite::Connection>, P, CL, R> WalletDb<C, P, CL, R> {
423    /// Constructs a new wrapper around the given connection.
424    ///
425    /// This is provided for use cases such as connection pooling, where `conn` may be an
426    /// `&mut rusqlite::Connection`.
427    ///
428    /// The caller must ensure that [`rusqlite::vtab::array::load_module`] has been called
429    /// on the connection.
430    ///
431    /// ## Parameters
432    /// - `conn`: A connection to the wallet database.
433    /// - `params`: Parameters associated with the Zcash network that the wallet will connect to.
434    /// - `clock`: The clock to use in the case that the backend needs access to the system time.
435    /// - `rng`: The random number generation capability to be exposed by the created `WalletDb`
436    ///   instance.
437    pub fn from_connection(conn: C, params: P, clock: CL, rng: R) -> Self {
438        WalletDb {
439            conn,
440            params,
441            clock,
442            rng,
443            #[cfg(feature = "transparent-inputs")]
444            gap_limits: GapLimits::default(),
445        }
446    }
447}
448
449impl<C: BorrowMut<Connection>, P, CL, R> WalletDb<C, P, CL, R> {
450    pub fn transactionally<F, A, E: From<rusqlite::Error>>(&mut self, f: F) -> Result<A, E>
451    where
452        F: FnOnce(&mut WalletDb<SqlTransaction<'_>, &P, &CL, &mut R>) -> Result<A, E>,
453    {
454        let tx = self.conn.borrow_mut().transaction()?;
455        let mut wdb = WalletDb {
456            conn: SqlTransaction(&tx),
457            params: &self.params,
458            clock: &self.clock,
459            rng: &mut self.rng,
460            #[cfg(feature = "transparent-inputs")]
461            gap_limits: self.gap_limits,
462        };
463        let result = f(&mut wdb)?;
464        tx.commit()?;
465        Ok(result)
466    }
467
468    /// Attempts to construct a witness for each note belonging to the wallet that is believed by
469    /// the wallet to currently be spendable, and returns a vector of the ranges that must be
470    /// rescanned in order to correct missing witness data.
471    ///
472    /// This method is intended for repairing wallets that broke due to bugs in `shardtree`.
473    pub fn check_witnesses(&mut self) -> Result<Vec<Range<BlockHeight>>, SqliteClientError> {
474        self.transactionally(|wdb| wallet::commitment_tree::check_witnesses(wdb.conn.0))
475    }
476
477    /// Updates the scan queue by inserting scan ranges for the given range of block heights, with
478    /// the specified scanning priority.
479    pub fn queue_rescans(
480        &mut self,
481        rescan_ranges: NonEmpty<Range<BlockHeight>>,
482        priority: ScanPriority,
483    ) -> Result<(), SqliteClientError> {
484        let query_range = rescan_ranges
485            .iter()
486            .fold(None, |acc: Option<Range<BlockHeight>>, scan_range| {
487                if let Some(range) = acc {
488                    Some(min(range.start, scan_range.start)..max(range.end, scan_range.end))
489                } else {
490                    Some(scan_range.clone())
491                }
492            })
493            .expect("rescan_ranges is nonempty");
494
495        self.transactionally::<_, _, SqliteClientError>(|wdb| {
496            replace_queue_entries(
497                wdb.conn.0,
498                &query_range,
499                rescan_ranges
500                    .into_iter()
501                    .map(|r| ScanRange::from_parts(r, priority)),
502                true,
503            )
504        })?;
505
506        Ok(())
507    }
508}
509
510#[cfg(feature = "transparent-inputs")]
511impl<C: BorrowMut<Connection>, P, CL: Clock, R: rand::RngCore> WalletDb<C, P, CL, R> {
512    /// For each ephemeral address in the wallet, ensure that the transaction data request queue
513    /// contains a request for the wallet to check for UTXOs belonging to that address at some time
514    /// during the next 24-hour period.
515    ///
516    /// We use randomized scheduling of ephemeral address checks to ensure that a
517    /// lightwalletd-compromising adversary cannot use temporal clustering to determine what
518    /// ephemeral addresses belong to a given wallet.
519    pub fn schedule_ephemeral_address_checks(&mut self) -> Result<(), SqliteClientError> {
520        self.borrow_mut().transactionally(|wdb| {
521            schedule_ephemeral_address_checks(wdb.conn.0, wdb.clock, &mut wdb.rng)
522        })
523    }
524}
525
526impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> InputSource
527    for WalletDb<C, P, CL, R>
528{
529    type Error = SqliteClientError;
530    type NoteRef = ReceivedNoteId;
531    type AccountId = AccountUuid;
532
533    fn get_spendable_note(
534        &self,
535        txid: &TxId,
536        protocol: ShieldedProtocol,
537        index: u32,
538    ) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
539        match protocol {
540            ShieldedProtocol::Sapling => wallet::sapling::get_spendable_sapling_note(
541                self.conn.borrow(),
542                &self.params,
543                txid,
544                index,
545            )
546            .map(|opt| opt.map(|n| n.map_note(Note::Sapling))),
547            ShieldedProtocol::Orchard => {
548                #[cfg(feature = "orchard")]
549                return wallet::orchard::get_spendable_orchard_note(
550                    self.conn.borrow(),
551                    &self.params,
552                    txid,
553                    index,
554                )
555                .map(|opt| opt.map(|n| n.map_note(Note::Orchard)));
556
557                #[cfg(not(feature = "orchard"))]
558                return Err(SqliteClientError::UnsupportedPoolType(PoolType::ORCHARD));
559            }
560        }
561    }
562
563    fn select_spendable_notes(
564        &self,
565        account: Self::AccountId,
566        target_value: Zatoshis,
567        sources: &[ShieldedProtocol],
568        anchor_height: BlockHeight,
569        exclude: &[Self::NoteRef],
570    ) -> Result<SpendableNotes<Self::NoteRef>, Self::Error> {
571        Ok(SpendableNotes::new(
572            if sources.contains(&ShieldedProtocol::Sapling) {
573                wallet::sapling::select_spendable_sapling_notes(
574                    self.conn.borrow(),
575                    &self.params,
576                    account,
577                    target_value,
578                    anchor_height,
579                    exclude,
580                )?
581            } else {
582                vec![]
583            },
584            #[cfg(feature = "orchard")]
585            if sources.contains(&ShieldedProtocol::Orchard) {
586                wallet::orchard::select_spendable_orchard_notes(
587                    self.conn.borrow(),
588                    &self.params,
589                    account,
590                    target_value,
591                    anchor_height,
592                    exclude,
593                )?
594            } else {
595                vec![]
596            },
597        ))
598    }
599
600    #[cfg(feature = "transparent-inputs")]
601    fn get_unspent_transparent_output(
602        &self,
603        outpoint: &OutPoint,
604    ) -> Result<Option<WalletTransparentOutput>, Self::Error> {
605        wallet::transparent::get_wallet_transparent_output(self.conn.borrow(), outpoint, false)
606    }
607
608    #[cfg(feature = "transparent-inputs")]
609    fn get_spendable_transparent_outputs(
610        &self,
611        address: &TransparentAddress,
612        target_height: BlockHeight,
613        min_confirmations: u32,
614    ) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
615        wallet::transparent::get_spendable_transparent_outputs(
616            self.conn.borrow(),
617            &self.params,
618            address,
619            target_height,
620            min_confirmations,
621        )
622    }
623
624    /// Returns metadata for the spendable notes in the wallet.
625    fn get_account_metadata(
626        &self,
627        account_id: Self::AccountId,
628        selector: &NoteFilter,
629        exclude: &[Self::NoteRef],
630    ) -> Result<AccountMeta, Self::Error> {
631        let chain_tip_height = wallet::chain_tip_height(self.conn.borrow())?
632            .ok_or(SqliteClientError::ChainHeightUnknown)?;
633
634        let sapling_pool_meta = spendable_notes_meta(
635            self.conn.borrow(),
636            ShieldedProtocol::Sapling,
637            chain_tip_height,
638            account_id,
639            selector,
640            exclude,
641        )?;
642
643        #[cfg(feature = "orchard")]
644        let orchard_pool_meta = spendable_notes_meta(
645            self.conn.borrow(),
646            ShieldedProtocol::Orchard,
647            chain_tip_height,
648            account_id,
649            selector,
650            exclude,
651        )?;
652        #[cfg(not(feature = "orchard"))]
653        let orchard_pool_meta = None;
654
655        Ok(AccountMeta::new(sapling_pool_meta, orchard_pool_meta))
656    }
657}
658
659impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletRead
660    for WalletDb<C, P, CL, R>
661{
662    type Error = SqliteClientError;
663    type AccountId = AccountUuid;
664    type Account = wallet::Account;
665
666    fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error> {
667        Ok(wallet::get_account_ids(self.conn.borrow())?)
668    }
669
670    fn get_account(
671        &self,
672        account_id: Self::AccountId,
673    ) -> Result<Option<Self::Account>, Self::Error> {
674        wallet::get_account(self.conn.borrow(), &self.params, account_id)
675    }
676
677    fn get_derived_account(
678        &self,
679        seed: &SeedFingerprint,
680        account_id: zip32::AccountId,
681    ) -> Result<Option<Self::Account>, Self::Error> {
682        wallet::get_derived_account(self.conn.borrow(), &self.params, seed, account_id)
683    }
684
685    fn validate_seed(
686        &self,
687        account_id: Self::AccountId,
688        seed: &SecretVec<u8>,
689    ) -> Result<bool, Self::Error> {
690        if let Some(account) = self.get_account(account_id)? {
691            if let AccountSource::Derived { derivation, .. } = account.source() {
692                wallet::seed_matches_derived_account(
693                    &self.params,
694                    seed,
695                    derivation.seed_fingerprint(),
696                    derivation.account_index(),
697                    &account.uivk(),
698                )
699            } else {
700                Err(SqliteClientError::UnknownZip32Derivation)
701            }
702        } else {
703            // Missing account is documented to return false.
704            Ok(false)
705        }
706    }
707
708    fn seed_relevance_to_derived_accounts(
709        &self,
710        seed: &SecretVec<u8>,
711    ) -> Result<SeedRelevance<Self::AccountId>, Self::Error> {
712        let mut has_accounts = false;
713        let mut has_derived = false;
714        let mut relevant_account_ids = vec![];
715
716        for account_id in self.get_account_ids()? {
717            has_accounts = true;
718            let account = self.get_account(account_id)?.expect("account ID exists");
719
720            // If the account is imported, the seed _might_ be relevant, but the only
721            // way we could determine that is by brute-forcing the ZIP 32 account
722            // index space, which we're not going to do. The method name indicates to
723            // the caller that we only check derived accounts.
724            if let AccountSource::Derived { derivation, .. } = account.source() {
725                has_derived = true;
726
727                if wallet::seed_matches_derived_account(
728                    &self.params,
729                    seed,
730                    derivation.seed_fingerprint(),
731                    derivation.account_index(),
732                    &account.uivk(),
733                )? {
734                    // The seed is relevant to this account.
735                    relevant_account_ids.push(account_id);
736                }
737            }
738        }
739
740        Ok(
741            if let Some(account_ids) = NonEmpty::from_vec(relevant_account_ids) {
742                SeedRelevance::Relevant { account_ids }
743            } else if has_derived {
744                SeedRelevance::NotRelevant
745            } else if has_accounts {
746                SeedRelevance::NoDerivedAccounts
747            } else {
748                SeedRelevance::NoAccounts
749            },
750        )
751    }
752
753    fn get_account_for_ufvk(
754        &self,
755        ufvk: &UnifiedFullViewingKey,
756    ) -> Result<Option<Self::Account>, Self::Error> {
757        wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk)
758    }
759
760    fn list_addresses(&self, account: Self::AccountId) -> Result<Vec<AddressInfo>, Self::Error> {
761        wallet::list_addresses(self.conn.borrow(), &self.params, account)
762    }
763
764    fn get_last_generated_address_matching(
765        &self,
766        account: Self::AccountId,
767        request: UnifiedAddressRequest,
768    ) -> Result<Option<UnifiedAddress>, Self::Error> {
769        wallet::get_last_generated_address_matching(
770            self.conn.borrow(),
771            &self.params,
772            account,
773            request,
774        )
775        .map(|res| res.map(|(addr, _)| addr))
776    }
777
778    fn get_account_birthday(&self, account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
779        wallet::account_birthday(self.conn.borrow(), account)
780    }
781
782    fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error> {
783        wallet::wallet_birthday(self.conn.borrow()).map_err(SqliteClientError::from)
784    }
785
786    fn get_wallet_summary(
787        &self,
788        min_confirmations: u32,
789    ) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error> {
790        // This will return a runtime error if we call `get_wallet_summary` from two
791        // threads at the same time, as transactions cannot nest.
792        wallet::get_wallet_summary(
793            &self.conn.borrow().unchecked_transaction()?,
794            &self.params,
795            min_confirmations,
796            &SubtreeProgressEstimator,
797        )
798    }
799
800    fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
801        wallet::chain_tip_height(self.conn.borrow()).map_err(SqliteClientError::from)
802    }
803
804    fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
805        wallet::get_block_hash(self.conn.borrow(), block_height).map_err(SqliteClientError::from)
806    }
807
808    fn block_metadata(&self, height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error> {
809        wallet::block_metadata(self.conn.borrow(), &self.params, height)
810    }
811
812    fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
813        wallet::block_fully_scanned(self.conn.borrow(), &self.params)
814    }
815
816    fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error> {
817        wallet::get_max_height_hash(self.conn.borrow()).map_err(SqliteClientError::from)
818    }
819
820    fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
821        wallet::block_max_scanned(self.conn.borrow(), &self.params)
822    }
823
824    fn suggest_scan_ranges(&self) -> Result<Vec<ScanRange>, Self::Error> {
825        wallet::scanning::suggest_scan_ranges(self.conn.borrow(), ScanPriority::Historic)
826    }
827
828    fn get_target_and_anchor_heights(
829        &self,
830        min_confirmations: NonZeroU32,
831    ) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
832        wallet::get_target_and_anchor_heights(self.conn.borrow(), min_confirmations)
833            .map_err(SqliteClientError::from)
834    }
835
836    fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
837        wallet::get_tx_height(self.conn.borrow(), txid).map_err(SqliteClientError::from)
838    }
839
840    fn get_unified_full_viewing_keys(
841        &self,
842    ) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error> {
843        wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params)
844    }
845
846    fn get_memo(&self, note_id: NoteId) -> Result<Option<Memo>, Self::Error> {
847        let sent_memo = wallet::get_sent_memo(self.conn.borrow(), note_id)?;
848        if sent_memo.is_some() {
849            Ok(sent_memo)
850        } else {
851            wallet::get_received_memo(self.conn.borrow(), note_id)
852        }
853    }
854
855    fn get_transaction(&self, txid: TxId) -> Result<Option<Transaction>, Self::Error> {
856        wallet::get_transaction(self.conn.borrow(), &self.params, txid)
857            .map(|res| res.map(|(_, tx)| tx))
858    }
859
860    fn get_sapling_nullifiers(
861        &self,
862        query: NullifierQuery,
863    ) -> Result<Vec<(Self::AccountId, sapling::Nullifier)>, Self::Error> {
864        wallet::sapling::get_sapling_nullifiers(self.conn.borrow(), query)
865    }
866
867    #[cfg(feature = "orchard")]
868    fn get_orchard_nullifiers(
869        &self,
870        query: NullifierQuery,
871    ) -> Result<Vec<(Self::AccountId, orchard::note::Nullifier)>, Self::Error> {
872        wallet::orchard::get_orchard_nullifiers(self.conn.borrow(), query)
873    }
874
875    #[cfg(feature = "transparent-inputs")]
876    fn get_transparent_receivers(
877        &self,
878        account: Self::AccountId,
879        include_change: bool,
880    ) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, Self::Error> {
881        let key_scopes: &[KeyScope] = if include_change {
882            &[KeyScope::EXTERNAL, KeyScope::INTERNAL]
883        } else {
884            &[KeyScope::EXTERNAL]
885        };
886
887        wallet::transparent::get_transparent_receivers(
888            self.conn.borrow(),
889            &self.params,
890            account,
891            key_scopes,
892        )
893    }
894
895    #[cfg(feature = "transparent-inputs")]
896    fn get_transparent_balances(
897        &self,
898        account: Self::AccountId,
899        max_height: BlockHeight,
900    ) -> Result<HashMap<TransparentAddress, Zatoshis>, Self::Error> {
901        wallet::transparent::get_transparent_balances(
902            self.conn.borrow(),
903            &self.params,
904            account,
905            max_height,
906        )
907    }
908
909    #[cfg(feature = "transparent-inputs")]
910    fn get_transparent_address_metadata(
911        &self,
912        account: Self::AccountId,
913        address: &TransparentAddress,
914    ) -> Result<Option<TransparentAddressMetadata>, Self::Error> {
915        wallet::transparent::get_transparent_address_metadata(
916            self.conn.borrow(),
917            &self.params,
918            account,
919            address,
920        )
921    }
922
923    #[cfg(feature = "transparent-inputs")]
924    fn utxo_query_height(&self, account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
925        let account_ref = wallet::get_account_ref(self.conn.borrow(), account)?;
926        wallet::transparent::utxo_query_height(self.conn.borrow(), account_ref, &self.gap_limits)
927    }
928
929    #[cfg(feature = "transparent-inputs")]
930    fn get_known_ephemeral_addresses(
931        &self,
932        account: Self::AccountId,
933        index_range: Option<Range<NonHardenedChildIndex>>,
934    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, Self::Error> {
935        let account_id = wallet::get_account_ref(self.conn.borrow(), account)?;
936        wallet::transparent::ephemeral::get_known_ephemeral_addresses(
937            self.conn.borrow(),
938            &self.params,
939            account_id,
940            index_range,
941        )
942    }
943
944    #[cfg(feature = "transparent-inputs")]
945    fn find_account_for_ephemeral_address(
946        &self,
947        address: &TransparentAddress,
948    ) -> Result<Option<Self::AccountId>, Self::Error> {
949        wallet::transparent::ephemeral::find_account_for_ephemeral_address_str(
950            self.conn.borrow(),
951            &address.encode(&self.params),
952        )
953    }
954
955    fn transaction_data_requests(&self) -> Result<Vec<TransactionDataRequest>, Self::Error> {
956        let iter = wallet::transaction_data_requests(self.conn.borrow())?.into_iter();
957
958        #[cfg(feature = "transparent-inputs")]
959        let iter = iter.chain(wallet::transparent::transaction_data_requests(
960            self.conn.borrow(),
961            &self.params,
962        )?);
963
964        Ok(iter.collect())
965    }
966}
967
968#[cfg(any(test, feature = "test-dependencies"))]
969impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletTest
970    for WalletDb<C, P, CL, R>
971{
972    fn get_tx_history(
973        &self,
974    ) -> Result<Vec<TransactionSummary<<Self as WalletRead>::AccountId>>, <Self as WalletRead>::Error>
975    {
976        wallet::testing::get_tx_history(self.conn.borrow())
977    }
978
979    fn get_sent_note_ids(
980        &self,
981        txid: &TxId,
982        protocol: ShieldedProtocol,
983    ) -> Result<Vec<NoteId>, <Self as WalletRead>::Error> {
984        use crate::wallet::encoding::pool_code;
985
986        let mut stmt_sent_notes = self.conn.borrow().prepare(
987            "SELECT output_index
988             FROM sent_notes
989             JOIN transactions ON transactions.id_tx = sent_notes.tx
990             WHERE transactions.txid = :txid
991             AND sent_notes.output_pool = :pool_code",
992        )?;
993
994        let note_ids = stmt_sent_notes
995            .query_map(
996                named_params! {
997                    ":txid": txid.as_ref(),
998                    ":pool_code": pool_code(PoolType::Shielded(protocol)),
999                },
1000                |row| Ok(NoteId::new(*txid, protocol, row.get(0)?)),
1001            )?
1002            .collect::<Result<_, _>>()?;
1003
1004        Ok(note_ids)
1005    }
1006
1007    fn get_sent_outputs(
1008        &self,
1009        txid: &TxId,
1010    ) -> Result<Vec<OutputOfSentTx>, <Self as WalletRead>::Error> {
1011        let mut stmt_sent = self.conn.borrow().prepare(
1012            "SELECT value, to_address,
1013                    a.cached_transparent_receiver_address, a.transparent_child_index
1014             FROM sent_notes
1015             JOIN transactions t ON t.id_tx = sent_notes.tx
1016             LEFT JOIN transparent_received_outputs tro ON tro.transaction_id = t.id_tx
1017             LEFT JOIN addresses a ON a.id = tro.address_id AND a.key_scope = :key_scope
1018             WHERE t.txid = :txid
1019             ORDER BY value",
1020        )?;
1021
1022        let sends = stmt_sent
1023            .query_map(
1024                named_params![
1025                    ":txid": txid.as_ref(),
1026                    ":key_scope": KeyScope::Ephemeral.encode()
1027                ],
1028                |row| {
1029                    let v = row.get(0)?;
1030                    let to_address = row
1031                        .get::<_, Option<String>>(1)?
1032                        .and_then(|s| Address::decode(&self.params, &s));
1033                    let ephemeral_address = row
1034                        .get::<_, Option<String>>(2)?
1035                        .and_then(|s| Address::decode(&self.params, &s));
1036                    let address_index: Option<u32> = row.get(3)?;
1037                    Ok((v, to_address, ephemeral_address.zip(address_index)))
1038                },
1039            )?
1040            .map(|res| {
1041                let (amount, external_recipient, ephemeral_address) = res?;
1042                Ok::<_, <Self as WalletRead>::Error>(OutputOfSentTx::from_parts(
1043                    Zatoshis::from_u64(amount)?,
1044                    external_recipient,
1045                    ephemeral_address,
1046                ))
1047            })
1048            .collect::<Result<_, _>>()?;
1049
1050        Ok(sends)
1051    }
1052
1053    fn get_checkpoint_history(
1054        &self,
1055        protocol: &ShieldedProtocol,
1056    ) -> Result<
1057        Vec<(BlockHeight, Option<incrementalmerkletree::Position>)>,
1058        <Self as WalletRead>::Error,
1059    > {
1060        wallet::testing::get_checkpoint_history(self.conn.borrow(), protocol)
1061    }
1062
1063    #[cfg(feature = "transparent-inputs")]
1064    fn get_transparent_output(
1065        &self,
1066        outpoint: &OutPoint,
1067        allow_unspendable: bool,
1068    ) -> Result<Option<WalletTransparentOutput>, <Self as InputSource>::Error> {
1069        wallet::transparent::get_wallet_transparent_output(
1070            self.conn.borrow(),
1071            outpoint,
1072            allow_unspendable,
1073        )
1074    }
1075
1076    fn get_notes(
1077        &self,
1078        protocol: ShieldedProtocol,
1079    ) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, <Self as InputSource>::Error> {
1080        let (table_prefix, index_col, _) = wallet::common::per_protocol_names(protocol);
1081        let mut stmt_received_notes = self.conn.borrow().prepare(&format!(
1082            "SELECT txid, {index_col}
1083             FROM {table_prefix}_received_notes rn
1084             INNER JOIN transactions ON transactions.id_tx = rn.tx
1085             WHERE transactions.block IS NOT NULL
1086             AND recipient_key_scope IS NOT NULL
1087             AND nf IS NOT NULL
1088             AND commitment_tree_position IS NOT NULL"
1089        ))?;
1090
1091        let result = stmt_received_notes
1092            .query_map([], |row| {
1093                let txid: [u8; 32] = row.get(0)?;
1094                let output_index: u32 = row.get(1)?;
1095                let note = self
1096                    .get_spendable_note(&TxId::from_bytes(txid), protocol, output_index)
1097                    .unwrap()
1098                    .unwrap();
1099                Ok(note)
1100            })?
1101            .collect::<Result<Vec<_>, _>>()?;
1102
1103        Ok(result)
1104    }
1105}
1106
1107impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL: Clock, R> WalletWrite
1108    for WalletDb<C, P, CL, R>
1109{
1110    type UtxoRef = UtxoId;
1111
1112    fn create_account(
1113        &mut self,
1114        account_name: &str,
1115        seed: &SecretVec<u8>,
1116        birthday: &AccountBirthday,
1117        key_source: Option<&str>,
1118    ) -> Result<(Self::AccountId, UnifiedSpendingKey), Self::Error> {
1119        self.borrow_mut().transactionally(|wdb| {
1120            let seed_fingerprint =
1121                SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
1122                    SqliteClientError::BadAccountData(
1123                        "Seed must be between 32 and 252 bytes in length.".to_owned(),
1124                    )
1125                })?;
1126            let zip32_account_index =
1127                wallet::max_zip32_account_index(wdb.conn.0, &seed_fingerprint)?
1128                    .map(|a| {
1129                        a.next()
1130                            .ok_or(SqliteClientError::Zip32AccountIndexOutOfRange)
1131                    })
1132                    .transpose()?
1133                    .unwrap_or(zip32::AccountId::ZERO);
1134
1135            let usk = UnifiedSpendingKey::from_seed(
1136                &wdb.params,
1137                seed.expose_secret(),
1138                zip32_account_index,
1139            )
1140            .map_err(|_| SqliteClientError::KeyDerivationError(zip32_account_index))?;
1141            let ufvk = usk.to_unified_full_viewing_key();
1142
1143            let account = wallet::add_account(
1144                wdb.conn.0,
1145                &wdb.params,
1146                account_name,
1147                &AccountSource::Derived {
1148                    derivation: Zip32Derivation::new(seed_fingerprint, zip32_account_index),
1149                    key_source: key_source.map(|s| s.to_owned()),
1150                },
1151                wallet::ViewingKey::Full(Box::new(ufvk)),
1152                birthday,
1153                #[cfg(feature = "transparent-inputs")]
1154                &wdb.gap_limits,
1155            )?;
1156
1157            Ok((account.id(), usk))
1158        })
1159    }
1160
1161    fn import_account_hd(
1162        &mut self,
1163        account_name: &str,
1164        seed: &SecretVec<u8>,
1165        account_index: zip32::AccountId,
1166        birthday: &AccountBirthday,
1167        key_source: Option<&str>,
1168    ) -> Result<(Self::Account, UnifiedSpendingKey), Self::Error> {
1169        self.transactionally(|wdb| {
1170            let seed_fingerprint =
1171                SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
1172                    SqliteClientError::BadAccountData(
1173                        "Seed must be between 32 and 252 bytes in length.".to_owned(),
1174                    )
1175                })?;
1176
1177            let usk =
1178                UnifiedSpendingKey::from_seed(&wdb.params, seed.expose_secret(), account_index)
1179                    .map_err(|_| SqliteClientError::KeyDerivationError(account_index))?;
1180            let ufvk = usk.to_unified_full_viewing_key();
1181
1182            let account = wallet::add_account(
1183                wdb.conn.0,
1184                &wdb.params,
1185                account_name,
1186                &AccountSource::Derived {
1187                    derivation: Zip32Derivation::new(seed_fingerprint, account_index),
1188                    key_source: key_source.map(|s| s.to_owned()),
1189                },
1190                wallet::ViewingKey::Full(Box::new(ufvk)),
1191                birthday,
1192                #[cfg(feature = "transparent-inputs")]
1193                &wdb.gap_limits,
1194            )?;
1195
1196            Ok((account, usk))
1197        })
1198    }
1199
1200    fn import_account_ufvk(
1201        &mut self,
1202        account_name: &str,
1203        ufvk: &UnifiedFullViewingKey,
1204        birthday: &AccountBirthday,
1205        purpose: AccountPurpose,
1206        key_source: Option<&str>,
1207    ) -> Result<Self::Account, Self::Error> {
1208        self.transactionally(|wdb| {
1209            wallet::add_account(
1210                wdb.conn.0,
1211                &wdb.params,
1212                account_name,
1213                &AccountSource::Imported {
1214                    purpose,
1215                    key_source: key_source.map(|s| s.to_owned()),
1216                },
1217                wallet::ViewingKey::Full(Box::new(ufvk.to_owned())),
1218                birthday,
1219                #[cfg(feature = "transparent-inputs")]
1220                &wdb.gap_limits,
1221            )
1222        })
1223    }
1224
1225    fn get_next_available_address(
1226        &mut self,
1227        account_uuid: Self::AccountId,
1228        request: UnifiedAddressRequest,
1229    ) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, Self::Error> {
1230        self.transactionally(|wdb| {
1231            wallet::get_next_available_address(
1232                wdb.conn.0,
1233                &wdb.params,
1234                &wdb.clock,
1235                account_uuid,
1236                request,
1237                #[cfg(feature = "transparent-inputs")]
1238                &wdb.gap_limits,
1239            )
1240        })
1241    }
1242
1243    fn get_address_for_index(
1244        &mut self,
1245        account: Self::AccountId,
1246        diversifier_index: DiversifierIndex,
1247        request: UnifiedAddressRequest,
1248    ) -> Result<Option<UnifiedAddress>, Self::Error> {
1249        if let Some(account) = self.get_account(account)? {
1250            use zcash_keys::keys::AddressGenerationError::*;
1251
1252            match account.uivk().address(diversifier_index, request) {
1253                Ok(address) => {
1254                    let chain_tip_height = wallet::chain_tip_height(self.conn.borrow())?;
1255                    upsert_address(
1256                        self.conn.borrow(),
1257                        &self.params,
1258                        account.internal_id(),
1259                        diversifier_index,
1260                        &address,
1261                        Some(chain_tip_height.unwrap_or(account.birthday())),
1262                        true,
1263                    )?;
1264
1265                    Ok(Some(address))
1266                }
1267                #[cfg(feature = "transparent-inputs")]
1268                Err(InvalidTransparentChildIndex(_)) => Ok(None),
1269                Err(InvalidSaplingDiversifierIndex(_)) => Ok(None),
1270                Err(e) => Err(SqliteClientError::AddressGeneration(e)),
1271            }
1272        } else {
1273            Err(SqliteClientError::AccountUnknown)
1274        }
1275    }
1276
1277    fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error> {
1278        let tx = self.conn.borrow_mut().transaction()?;
1279        wallet::scanning::update_chain_tip(&tx, &self.params, tip_height)?;
1280        tx.commit()?;
1281        Ok(())
1282    }
1283
1284    #[tracing::instrument(skip_all, fields(height = blocks.first().map(|b| u32::from(b.height())), count = blocks.len()))]
1285    #[allow(clippy::type_complexity)]
1286    fn put_blocks(
1287        &mut self,
1288        from_state: &ChainState,
1289        blocks: Vec<ScannedBlock<Self::AccountId>>,
1290    ) -> Result<(), Self::Error> {
1291        struct BlockPositions {
1292            height: BlockHeight,
1293            sapling_start_position: Position,
1294            #[cfg(feature = "orchard")]
1295            orchard_start_position: Position,
1296        }
1297
1298        if blocks.is_empty() {
1299            return Ok(());
1300        }
1301
1302        self.transactionally(|wdb| {
1303            let initial_block = blocks.first().expect("blocks is known to be nonempty");
1304            assert!(from_state.block_height() + 1 == initial_block.height());
1305
1306            let start_positions = BlockPositions {
1307                height: initial_block.height(),
1308                sapling_start_position: Position::from(
1309                    u64::from(initial_block.sapling().final_tree_size())
1310                        - u64::try_from(initial_block.sapling().commitments().len()).unwrap(),
1311                ),
1312                #[cfg(feature = "orchard")]
1313                orchard_start_position: Position::from(
1314                    u64::from(initial_block.orchard().final_tree_size())
1315                        - u64::try_from(initial_block.orchard().commitments().len()).unwrap(),
1316                ),
1317            };
1318
1319            let mut sapling_commitments = vec![];
1320            #[cfg(feature = "orchard")]
1321            let mut orchard_commitments = vec![];
1322            let mut last_scanned_height = None;
1323            let mut note_positions = vec![];
1324
1325            #[cfg(feature = "transparent-inputs")]
1326            let mut tx_refs = BTreeSet::new();
1327
1328            for block in blocks.into_iter() {
1329                if last_scanned_height
1330                    .iter()
1331                    .any(|prev| block.height() != *prev + 1)
1332                {
1333                    return Err(SqliteClientError::NonSequentialBlocks);
1334                }
1335
1336                // Insert the block into the database.
1337                wallet::put_block(
1338                    wdb.conn.0,
1339                    block.height(),
1340                    block.block_hash(),
1341                    block.block_time(),
1342                    block.sapling().final_tree_size(),
1343                    block.sapling().commitments().len().try_into().unwrap(),
1344                    #[cfg(feature = "orchard")]
1345                    block.orchard().final_tree_size(),
1346                    #[cfg(feature = "orchard")]
1347                    block.orchard().commitments().len().try_into().unwrap(),
1348                )?;
1349
1350                for tx in block.transactions() {
1351                    let tx_ref = wallet::put_tx_meta(wdb.conn.0, tx, block.height())?;
1352
1353                    #[cfg(feature = "transparent-inputs")]
1354                    tx_refs.insert(tx_ref);
1355
1356                    wallet::queue_tx_retrieval(wdb.conn.0, std::iter::once(tx.txid()), None)?;
1357
1358                    // Mark notes as spent and remove them from the scanning cache
1359                    for spend in tx.sapling_spends() {
1360                        wallet::sapling::mark_sapling_note_spent(wdb.conn.0, tx_ref, spend.nf())?;
1361                    }
1362                    #[cfg(feature = "orchard")]
1363                    for spend in tx.orchard_spends() {
1364                        wallet::orchard::mark_orchard_note_spent(wdb.conn.0, tx_ref, spend.nf())?;
1365                    }
1366
1367                    for output in tx.sapling_outputs() {
1368                        // Check whether this note was spent in a later block range that
1369                        // we previously scanned.
1370                        let spent_in = output
1371                            .nf()
1372                            .map(|nf| {
1373                                wallet::query_nullifier_map(
1374                                    wdb.conn.0,
1375                                    ShieldedProtocol::Sapling,
1376                                    nf,
1377                                )
1378                            })
1379                            .transpose()?
1380                            .flatten();
1381
1382                        wallet::sapling::put_received_note(
1383                            wdb.conn.0,
1384                            &wdb.params,
1385                            output,
1386                            tx_ref,
1387                            Some(block.height()),
1388                            spent_in,
1389                        )?;
1390                    }
1391                    #[cfg(feature = "orchard")]
1392                    for output in tx.orchard_outputs() {
1393                        // Check whether this note was spent in a later block range that
1394                        // we previously scanned.
1395                        let spent_in = output
1396                            .nf()
1397                            .map(|nf| {
1398                                wallet::query_nullifier_map(
1399                                    wdb.conn.0,
1400                                    ShieldedProtocol::Orchard,
1401                                    &nf.to_bytes(),
1402                                )
1403                            })
1404                            .transpose()?
1405                            .flatten();
1406
1407                        wallet::orchard::put_received_note(
1408                            wdb.conn.0,
1409                            &wdb.params,
1410                            output,
1411                            tx_ref,
1412                            Some(block.height()),
1413                            spent_in,
1414                        )?;
1415                    }
1416                }
1417
1418                // Insert the new nullifiers from this block into the nullifier map.
1419                wallet::insert_nullifier_map(
1420                    wdb.conn.0,
1421                    block.height(),
1422                    ShieldedProtocol::Sapling,
1423                    block.sapling().nullifier_map(),
1424                )?;
1425                #[cfg(feature = "orchard")]
1426                wallet::insert_nullifier_map(
1427                    wdb.conn.0,
1428                    block.height(),
1429                    ShieldedProtocol::Orchard,
1430                    &block
1431                        .orchard()
1432                        .nullifier_map()
1433                        .iter()
1434                        .map(|(txid, idx, nfs)| {
1435                            (*txid, *idx, nfs.iter().map(|nf| nf.to_bytes()).collect())
1436                        })
1437                        .collect::<Vec<_>>(),
1438                )?;
1439
1440                note_positions.extend(block.transactions().iter().flat_map(|wtx| {
1441                    let iter = wtx.sapling_outputs().iter().map(|out| {
1442                        (
1443                            ShieldedProtocol::Sapling,
1444                            out.note_commitment_tree_position(),
1445                        )
1446                    });
1447                    #[cfg(feature = "orchard")]
1448                    let iter = iter.chain(wtx.orchard_outputs().iter().map(|out| {
1449                        (
1450                            ShieldedProtocol::Orchard,
1451                            out.note_commitment_tree_position(),
1452                        )
1453                    }));
1454
1455                    iter
1456                }));
1457
1458                last_scanned_height = Some(block.height());
1459                let block_commitments = block.into_commitments();
1460                trace!(
1461                    "Sapling commitments for {:?}: {:?}",
1462                    last_scanned_height,
1463                    block_commitments
1464                        .sapling
1465                        .iter()
1466                        .map(|(_, r)| *r)
1467                        .collect::<Vec<_>>()
1468                );
1469                #[cfg(feature = "orchard")]
1470                trace!(
1471                    "Orchard commitments for {:?}: {:?}",
1472                    last_scanned_height,
1473                    block_commitments
1474                        .orchard
1475                        .iter()
1476                        .map(|(_, r)| *r)
1477                        .collect::<Vec<_>>()
1478                );
1479
1480                sapling_commitments.extend(block_commitments.sapling.into_iter().map(Some));
1481                #[cfg(feature = "orchard")]
1482                orchard_commitments.extend(block_commitments.orchard.into_iter().map(Some));
1483            }
1484
1485            #[cfg(feature = "transparent-inputs")]
1486            for (account_id, key_scope) in wallet::involved_accounts(wdb.conn.0, tx_refs)? {
1487                use ReceiverRequirement::*;
1488                wallet::transparent::generate_gap_addresses(
1489                    wdb.conn.0,
1490                    &wdb.params,
1491                    account_id,
1492                    key_scope,
1493                    &wdb.gap_limits,
1494                    UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
1495                    false,
1496                )?;
1497            }
1498
1499            // Prune the nullifier map of entries we no longer need.
1500            if let Some(meta) = wdb.block_fully_scanned()? {
1501                wallet::prune_nullifier_map(
1502                    wdb.conn.0,
1503                    meta.block_height().saturating_sub(PRUNING_DEPTH),
1504                )?;
1505            }
1506
1507            // We will have a start position and a last scanned height in all cases where
1508            // `blocks` is non-empty.
1509            if let Some(last_scanned_height) = last_scanned_height {
1510                // Create subtrees from the note commitments in parallel.
1511                const CHUNK_SIZE: usize = 1024;
1512                let sapling_subtrees = sapling_commitments
1513                    .par_chunks_mut(CHUNK_SIZE)
1514                    .enumerate()
1515                    .filter_map(|(i, chunk)| {
1516                        let start =
1517                            start_positions.sapling_start_position + (i * CHUNK_SIZE) as u64;
1518                        let end = start + chunk.len() as u64;
1519
1520                        shardtree::LocatedTree::from_iter(
1521                            start..end,
1522                            SAPLING_SHARD_HEIGHT.into(),
1523                            chunk.iter_mut().map(|n| n.take().expect("always Some")),
1524                        )
1525                    })
1526                    .map(|res| (res.subtree, res.checkpoints))
1527                    .collect::<Vec<_>>();
1528
1529                #[cfg(feature = "orchard")]
1530                let orchard_subtrees = orchard_commitments
1531                    .par_chunks_mut(CHUNK_SIZE)
1532                    .enumerate()
1533                    .filter_map(|(i, chunk)| {
1534                        let start =
1535                            start_positions.orchard_start_position + (i * CHUNK_SIZE) as u64;
1536                        let end = start + chunk.len() as u64;
1537
1538                        shardtree::LocatedTree::from_iter(
1539                            start..end,
1540                            ORCHARD_SHARD_HEIGHT.into(),
1541                            chunk.iter_mut().map(|n| n.take().expect("always Some")),
1542                        )
1543                    })
1544                    .map(|res| (res.subtree, res.checkpoints))
1545                    .collect::<Vec<_>>();
1546
1547                // Collect the complete set of Sapling checkpoints
1548                #[cfg(feature = "orchard")]
1549                let sapling_checkpoint_positions: BTreeMap<BlockHeight, Position> =
1550                    sapling_subtrees
1551                        .iter()
1552                        .flat_map(|(_, checkpoints)| checkpoints.iter())
1553                        .map(|(k, v)| (*k, *v))
1554                        .collect();
1555
1556                #[cfg(feature = "orchard")]
1557                let orchard_checkpoint_positions: BTreeMap<BlockHeight, Position> =
1558                    orchard_subtrees
1559                        .iter()
1560                        .flat_map(|(_, checkpoints)| checkpoints.iter())
1561                        .map(|(k, v)| (*k, *v))
1562                        .collect();
1563
1564                #[cfg(feature = "orchard")]
1565                fn ensure_checkpoints<
1566                    'a,
1567                    H,
1568                    I: Iterator<Item = &'a BlockHeight>,
1569                    const DEPTH: u8,
1570                >(
1571                    // An iterator of checkpoints heights for which we wish to ensure that
1572                    // checkpoints exists.
1573                    ensure_heights: I,
1574                    // The map of checkpoint positions from which we will draw note commitment tree
1575                    // position information for the newly created checkpoints.
1576                    existing_checkpoint_positions: &BTreeMap<BlockHeight, Position>,
1577                    // The frontier whose position will be used for an inserted checkpoint when
1578                    // there is no preceding checkpoint in existing_checkpoint_positions.
1579                    state_final_tree: &Frontier<H, DEPTH>,
1580                ) -> Vec<(BlockHeight, Checkpoint)> {
1581                    ensure_heights
1582                        .flat_map(|ensure_height| {
1583                            existing_checkpoint_positions
1584                                .range::<BlockHeight, _>(..=*ensure_height)
1585                                .last()
1586                                .map_or_else(
1587                                    || {
1588                                        Some((
1589                                            *ensure_height,
1590                                            state_final_tree
1591                                                .value()
1592                                                .map_or_else(Checkpoint::tree_empty, |t| {
1593                                                    Checkpoint::at_position(t.position())
1594                                                }),
1595                                        ))
1596                                    },
1597                                    |(existing_checkpoint_height, position)| {
1598                                        if *existing_checkpoint_height < *ensure_height {
1599                                            Some((
1600                                                *ensure_height,
1601                                                Checkpoint::at_position(*position),
1602                                            ))
1603                                        } else {
1604                                            // The checkpoint already exists, so we don't need to
1605                                            // do anything.
1606                                            None
1607                                        }
1608                                    },
1609                                )
1610                                .into_iter()
1611                        })
1612                        .collect::<Vec<_>>()
1613                }
1614
1615                #[cfg(feature = "orchard")]
1616                let (missing_sapling_checkpoints, missing_orchard_checkpoints) = (
1617                    ensure_checkpoints(
1618                        orchard_checkpoint_positions.keys(),
1619                        &sapling_checkpoint_positions,
1620                        from_state.final_sapling_tree(),
1621                    ),
1622                    ensure_checkpoints(
1623                        sapling_checkpoint_positions.keys(),
1624                        &orchard_checkpoint_positions,
1625                        from_state.final_orchard_tree(),
1626                    ),
1627                );
1628
1629                // Update the Sapling note commitment tree with all newly read note commitments
1630                {
1631                    let mut sapling_subtrees_iter = sapling_subtrees.into_iter();
1632                    wdb.with_sapling_tree_mut::<_, _, Self::Error>(|sapling_tree| {
1633                        debug!(
1634                            "Sapling initial tree size at {:?}: {:?}",
1635                            from_state.block_height(),
1636                            from_state.final_sapling_tree().tree_size()
1637                        );
1638                        // We insert the frontier with `Checkpoint` retention because we need to be
1639                        // able to truncate the tree back to this point.
1640                        sapling_tree.insert_frontier(
1641                            from_state.final_sapling_tree().clone(),
1642                            Retention::Checkpoint {
1643                                id: from_state.block_height(),
1644                                marking: Marking::Reference,
1645                            },
1646                        )?;
1647
1648                        for (tree, checkpoints) in &mut sapling_subtrees_iter {
1649                            sapling_tree.insert_tree(tree, checkpoints)?;
1650                        }
1651
1652                        // Ensure we have a Sapling checkpoint for each checkpointed Orchard block height.
1653                        // We skip all checkpoints below the minimum retained checkpoint in the
1654                        // Sapling tree, because branches below this height may be pruned.
1655                        #[cfg(feature = "orchard")]
1656                        {
1657                            let min_checkpoint_height = sapling_tree
1658                                .store()
1659                                .min_checkpoint_id()
1660                                .map_err(ShardTreeError::Storage)?
1661                                .expect(
1662                                    "At least one checkpoint was inserted (by insert_frontier)",
1663                                );
1664
1665                            for (height, checkpoint) in &missing_sapling_checkpoints {
1666                                if *height > min_checkpoint_height {
1667                                    sapling_tree
1668                                        .store_mut()
1669                                        .add_checkpoint(*height, checkpoint.clone())
1670                                        .map_err(ShardTreeError::Storage)?;
1671                                }
1672                            }
1673                        }
1674
1675                        Ok(())
1676                    })?;
1677                }
1678
1679                // Update the Orchard note commitment tree with all newly read note commitments
1680                #[cfg(feature = "orchard")]
1681                {
1682                    let mut orchard_subtrees = orchard_subtrees.into_iter();
1683                    wdb.with_orchard_tree_mut::<_, _, Self::Error>(|orchard_tree| {
1684                        debug!(
1685                            "Orchard initial tree size at {:?}: {:?}",
1686                            from_state.block_height(),
1687                            from_state.final_orchard_tree().tree_size()
1688                        );
1689                        // We insert the frontier with `Checkpoint` retention because we need to be
1690                        // able to truncate the tree back to this point.
1691                        orchard_tree.insert_frontier(
1692                            from_state.final_orchard_tree().clone(),
1693                            Retention::Checkpoint {
1694                                id: from_state.block_height(),
1695                                marking: Marking::Reference,
1696                            },
1697                        )?;
1698
1699                        for (tree, checkpoints) in &mut orchard_subtrees {
1700                            orchard_tree.insert_tree(tree, checkpoints)?;
1701                        }
1702
1703                        // Ensure we have an Orchard checkpoint for each checkpointed Sapling block height.
1704                        // We skip all checkpoints below the minimum retained checkpoint in the
1705                        // Orchard tree, because branches below this height may be pruned.
1706                        {
1707                            let min_checkpoint_height = orchard_tree
1708                                .store()
1709                                .min_checkpoint_id()
1710                                .map_err(ShardTreeError::Storage)?
1711                                .expect(
1712                                    "At least one checkpoint was inserted (by insert_frontier)",
1713                                );
1714
1715                            for (height, checkpoint) in &missing_orchard_checkpoints {
1716                                if *height > min_checkpoint_height {
1717                                    debug!(
1718                                        "Adding missing Orchard checkpoint for height: {:?}: {:?}",
1719                                        height,
1720                                        checkpoint.position()
1721                                    );
1722                                    orchard_tree
1723                                        .store_mut()
1724                                        .add_checkpoint(*height, checkpoint.clone())
1725                                        .map_err(ShardTreeError::Storage)?;
1726                                }
1727                            }
1728                        }
1729                        Ok(())
1730                    })?;
1731                }
1732
1733                wallet::scanning::scan_complete(
1734                    wdb.conn.0,
1735                    &wdb.params,
1736                    Range {
1737                        start: start_positions.height,
1738                        end: last_scanned_height + 1,
1739                    },
1740                    &note_positions,
1741                )?;
1742            }
1743
1744            Ok(())
1745        })
1746    }
1747
1748    fn put_received_transparent_utxo(
1749        &mut self,
1750        _output: &WalletTransparentOutput,
1751    ) -> Result<Self::UtxoRef, Self::Error> {
1752        #[cfg(feature = "transparent-inputs")]
1753        return self.transactionally(|wdb| {
1754            let (account_id, key_scope, utxo_id) =
1755                wallet::transparent::put_received_transparent_utxo(
1756                    wdb.conn.0,
1757                    &wdb.params,
1758                    _output,
1759                )?;
1760
1761            use ReceiverRequirement::*;
1762            wallet::transparent::generate_gap_addresses(
1763                wdb.conn.0,
1764                &wdb.params,
1765                account_id,
1766                key_scope,
1767                &wdb.gap_limits,
1768                UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
1769                true,
1770            )?;
1771
1772            Ok(utxo_id)
1773        });
1774
1775        #[cfg(not(feature = "transparent-inputs"))]
1776        panic!(
1777            "The wallet must be compiled with the transparent-inputs feature to use this method."
1778        );
1779    }
1780
1781    fn store_decrypted_tx(
1782        &mut self,
1783        d_tx: DecryptedTransaction<Self::AccountId>,
1784    ) -> Result<(), Self::Error> {
1785        self.transactionally(|wdb| {
1786            wallet::store_decrypted_tx(
1787                wdb.conn.0,
1788                &wdb.params,
1789                d_tx,
1790                #[cfg(feature = "transparent-inputs")]
1791                &wdb.gap_limits,
1792            )
1793        })
1794    }
1795
1796    fn store_transactions_to_be_sent(
1797        &mut self,
1798        transactions: &[SentTransaction<Self::AccountId>],
1799    ) -> Result<(), Self::Error> {
1800        self.transactionally(|wdb| {
1801            for sent_tx in transactions {
1802                wallet::store_transaction_to_be_sent(wdb.conn.0, &wdb.params, sent_tx)?;
1803            }
1804            Ok(())
1805        })
1806    }
1807
1808    fn truncate_to_height(&mut self, max_height: BlockHeight) -> Result<BlockHeight, Self::Error> {
1809        self.transactionally(|wdb| {
1810            wallet::truncate_to_height(
1811                wdb.conn.0,
1812                &wdb.params,
1813                #[cfg(feature = "transparent-inputs")]
1814                &wdb.gap_limits,
1815                max_height,
1816            )
1817        })
1818    }
1819
1820    #[cfg(feature = "transparent-inputs")]
1821    fn reserve_next_n_ephemeral_addresses(
1822        &mut self,
1823        account_id: Self::AccountId,
1824        n: usize,
1825    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, Self::Error> {
1826        self.transactionally(|wdb| {
1827            let account_id = wallet::get_account_ref(wdb.conn.0, account_id)?;
1828            let reserved = wallet::transparent::reserve_next_n_addresses(
1829                wdb.conn.0,
1830                &wdb.params,
1831                account_id,
1832                KeyScope::Ephemeral,
1833                wdb.gap_limits.ephemeral(),
1834                n,
1835            )?;
1836
1837            Ok(reserved.into_iter().map(|(_, a, m)| (a, m)).collect())
1838        })
1839    }
1840
1841    fn set_transaction_status(
1842        &mut self,
1843        txid: TxId,
1844        status: data_api::TransactionStatus,
1845    ) -> Result<(), Self::Error> {
1846        self.transactionally(|wdb| wallet::set_transaction_status(wdb.conn.0, txid, status))
1847    }
1848}
1849
1850pub(crate) type SaplingShardStore<C> = SqliteShardStore<C, sapling::Node, SAPLING_SHARD_HEIGHT>;
1851pub(crate) type SaplingCommitmentTree<C> =
1852    ShardTree<SaplingShardStore<C>, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>;
1853
1854pub(crate) fn sapling_tree<C>(
1855    conn: C,
1856) -> Result<SaplingCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
1857where
1858    SaplingShardStore<C>: ShardStore<H = sapling::Node, CheckpointId = BlockHeight>,
1859{
1860    Ok(ShardTree::new(
1861        SqliteShardStore::from_connection(conn, SAPLING_TABLES_PREFIX)
1862            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
1863        PRUNING_DEPTH.try_into().unwrap(),
1864    ))
1865}
1866
1867#[cfg(feature = "orchard")]
1868pub(crate) type OrchardShardStore<C> =
1869    SqliteShardStore<C, orchard::tree::MerkleHashOrchard, ORCHARD_SHARD_HEIGHT>;
1870
1871#[cfg(feature = "orchard")]
1872pub(crate) type OrchardCommitmentTree<C> = ShardTree<
1873    OrchardShardStore<C>,
1874    { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
1875    ORCHARD_SHARD_HEIGHT,
1876>;
1877
1878#[cfg(feature = "orchard")]
1879pub(crate) fn orchard_tree<C>(
1880    conn: C,
1881) -> Result<OrchardCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
1882where
1883    OrchardShardStore<C>:
1884        ShardStore<H = orchard::tree::MerkleHashOrchard, CheckpointId = BlockHeight>,
1885{
1886    Ok(ShardTree::new(
1887        SqliteShardStore::from_connection(conn, ORCHARD_TABLES_PREFIX)
1888            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
1889        PRUNING_DEPTH.try_into().unwrap(),
1890    ))
1891}
1892
1893impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletCommitmentTrees
1894    for WalletDb<C, P, CL, R>
1895{
1896    type Error = commitment_tree::Error;
1897    type SaplingShardStore<'a> = SaplingShardStore<&'a rusqlite::Transaction<'a>>;
1898
1899    fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
1900    where
1901        for<'a> F:
1902            FnMut(&'a mut SaplingCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
1903        E: From<ShardTreeError<Self::Error>>,
1904    {
1905        let tx = self
1906            .conn
1907            .borrow_mut()
1908            .transaction()
1909            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1910        let result = {
1911            let mut shardtree = sapling_tree(&tx)?;
1912            callback(&mut shardtree)?
1913        };
1914
1915        tx.commit()
1916            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1917        Ok(result)
1918    }
1919
1920    fn put_sapling_subtree_roots(
1921        &mut self,
1922        start_index: u64,
1923        roots: &[CommitmentTreeRoot<sapling::Node>],
1924    ) -> Result<(), ShardTreeError<Self::Error>> {
1925        let tx = self
1926            .conn
1927            .borrow_mut()
1928            .transaction()
1929            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1930        put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>(
1931            &tx,
1932            SAPLING_TABLES_PREFIX,
1933            start_index,
1934            roots,
1935        )?;
1936        tx.commit()
1937            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1938        Ok(())
1939    }
1940
1941    #[cfg(feature = "orchard")]
1942    type OrchardShardStore<'a> = SqliteShardStore<
1943        &'a rusqlite::Transaction<'a>,
1944        orchard::tree::MerkleHashOrchard,
1945        ORCHARD_SHARD_HEIGHT,
1946    >;
1947
1948    #[cfg(feature = "orchard")]
1949    fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
1950    where
1951        for<'a> F:
1952            FnMut(&'a mut OrchardCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
1953        E: From<ShardTreeError<Self::Error>>,
1954    {
1955        let tx = self
1956            .conn
1957            .borrow_mut()
1958            .transaction()
1959            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1960        let result = {
1961            let mut shardtree = orchard_tree(&tx)?;
1962            callback(&mut shardtree)?
1963        };
1964
1965        tx.commit()
1966            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1967        Ok(result)
1968    }
1969
1970    #[cfg(feature = "orchard")]
1971    fn put_orchard_subtree_roots(
1972        &mut self,
1973        start_index: u64,
1974        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
1975    ) -> Result<(), ShardTreeError<Self::Error>> {
1976        let tx = self
1977            .conn
1978            .borrow_mut()
1979            .transaction()
1980            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1981        put_shard_roots::<_, { ORCHARD_SHARD_HEIGHT * 2 }, ORCHARD_SHARD_HEIGHT>(
1982            &tx,
1983            ORCHARD_TABLES_PREFIX,
1984            start_index,
1985            roots,
1986        )?;
1987        tx.commit()
1988            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1989        Ok(())
1990    }
1991}
1992
1993impl<P: consensus::Parameters, CL, R> WalletCommitmentTrees
1994    for WalletDb<SqlTransaction<'_>, P, CL, R>
1995{
1996    type Error = commitment_tree::Error;
1997    type SaplingShardStore<'a> = crate::SaplingShardStore<&'a rusqlite::Transaction<'a>>;
1998
1999    fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
2000    where
2001        for<'a> F:
2002            FnMut(&'a mut SaplingCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
2003        E: From<ShardTreeError<commitment_tree::Error>>,
2004    {
2005        let mut shardtree = sapling_tree(self.conn.0)?;
2006        let result = callback(&mut shardtree)?;
2007
2008        Ok(result)
2009    }
2010
2011    fn put_sapling_subtree_roots(
2012        &mut self,
2013        start_index: u64,
2014        roots: &[CommitmentTreeRoot<sapling::Node>],
2015    ) -> Result<(), ShardTreeError<Self::Error>> {
2016        put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>(
2017            self.conn.0,
2018            SAPLING_TABLES_PREFIX,
2019            start_index,
2020            roots,
2021        )
2022    }
2023
2024    #[cfg(feature = "orchard")]
2025    type OrchardShardStore<'a> = crate::OrchardShardStore<&'a rusqlite::Transaction<'a>>;
2026
2027    #[cfg(feature = "orchard")]
2028    fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
2029    where
2030        for<'a> F:
2031            FnMut(&'a mut OrchardCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
2032        E: From<ShardTreeError<Self::Error>>,
2033    {
2034        let mut shardtree = orchard_tree(self.conn.0)?;
2035        let result = callback(&mut shardtree)?;
2036
2037        Ok(result)
2038    }
2039
2040    #[cfg(feature = "orchard")]
2041    fn put_orchard_subtree_roots(
2042        &mut self,
2043        start_index: u64,
2044        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
2045    ) -> Result<(), ShardTreeError<Self::Error>> {
2046        put_shard_roots::<_, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, ORCHARD_SHARD_HEIGHT>(
2047            self.conn.0,
2048            ORCHARD_TABLES_PREFIX,
2049            start_index,
2050            roots,
2051        )
2052    }
2053}
2054
2055/// A handle for the SQLite block source.
2056pub struct BlockDb(Connection);
2057
2058impl BlockDb {
2059    /// Opens a connection to the wallet database stored at the specified path.
2060    pub fn for_path<P: AsRef<Path>>(path: P) -> Result<Self, rusqlite::Error> {
2061        Connection::open(path).map(BlockDb)
2062    }
2063}
2064
2065impl BlockSource for BlockDb {
2066    type Error = SqliteClientError;
2067
2068    fn with_blocks<F, DbErrT>(
2069        &self,
2070        from_height: Option<BlockHeight>,
2071        limit: Option<usize>,
2072        with_row: F,
2073    ) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
2074    where
2075        F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
2076    {
2077        chain::blockdb_with_blocks(self, from_height, limit, with_row)
2078    }
2079}
2080
2081/// A block source that reads block data from disk and block metadata from a SQLite database.
2082///
2083/// This block source expects each compact block to be stored on disk in the `blocks` subdirectory
2084/// of the `blockstore_root` path provided at the time of construction. Each block should be
2085/// written, as the serialized bytes of its protobuf representation, where the path for each block
2086/// has the pattern:
2087///
2088/// `<blockstore_root>/blocks/<block_height>-<block_hash>-compactblock`
2089///
2090/// where `<block_height>` is the decimal value of the height at which the block was mined, and
2091/// `<block_hash>` is the hexadecimal representation of the block hash, as produced by the
2092/// [`fmt::Display`] implementation for [`zcash_primitives::block::BlockHash`].
2093///
2094/// This block source is intended to be used with the following data flow:
2095/// * When the cache is being filled:
2096///   * The caller requests the current maximum height at which cached data is available
2097///     using [`FsBlockDb::get_max_cached_height`]. If no cached data is available, the caller
2098///     can use the wallet's synced-to height for the following operations instead.
2099///   * (recommended for privacy) the caller should round the returned height down to some 100- /
2100///     1000-block boundary.
2101///   * The caller uses the lightwalletd's `getblock` gRPC method to obtain a stream of blocks.
2102///     For each block returned, the caller writes the compact block to `blocks_dir` using the
2103///     path format specified above. It is fine to overwrite an existing block, since block hashes
2104///     are immutable and collision-resistant.
2105///   * Once a caller-determined number of blocks have been successfully written to disk, the
2106///     caller should invoke [`FsBlockDb::write_block_metadata`] with the metadata for each block
2107///     written to disk.
2108/// * The cache can then be scanned using the [`BlockSource`] implementation, providing the
2109///   wallet's synced-to-height as a starting point.
2110/// * When part of the cache is no longer needed:
2111///   * The caller determines some height `H` that is the earliest block data it needs to preserve.
2112///     This might be determined based on where the wallet is fully-synced to, or other heuristics.
2113///   * The caller searches the defined filesystem folder for all files beginning in `HEIGHT-*` where
2114///     `HEIGHT < H`, and deletes those files.
2115///
2116/// Note: This API is unstable, and may change in the future. In particular, the [`BlockSource`]
2117/// API and the above description currently assume that scanning is performed in linear block
2118/// order; this assumption is likely to be weakened and/or removed in a future update.
2119#[cfg(feature = "unstable")]
2120pub struct FsBlockDb {
2121    conn: Connection,
2122    blocks_dir: PathBuf,
2123}
2124
2125/// Errors that can be generated by the filesystem/sqlite-backed
2126/// block source.
2127#[derive(Debug)]
2128#[cfg(feature = "unstable")]
2129pub enum FsBlockDbError {
2130    Fs(io::Error),
2131    Db(rusqlite::Error),
2132    Protobuf(prost::DecodeError),
2133    MissingBlockPath(PathBuf),
2134    InvalidBlockstoreRoot(PathBuf),
2135    InvalidBlockPath(PathBuf),
2136    CorruptedData(String),
2137    CacheMiss(BlockHeight),
2138}
2139
2140#[cfg(feature = "unstable")]
2141impl From<io::Error> for FsBlockDbError {
2142    fn from(err: io::Error) -> Self {
2143        FsBlockDbError::Fs(err)
2144    }
2145}
2146
2147#[cfg(feature = "unstable")]
2148impl From<rusqlite::Error> for FsBlockDbError {
2149    fn from(err: rusqlite::Error) -> Self {
2150        FsBlockDbError::Db(err)
2151    }
2152}
2153
2154#[cfg(feature = "unstable")]
2155impl From<prost::DecodeError> for FsBlockDbError {
2156    fn from(e: prost::DecodeError) -> Self {
2157        FsBlockDbError::Protobuf(e)
2158    }
2159}
2160
2161#[cfg(feature = "unstable")]
2162impl FsBlockDb {
2163    /// Creates a filesystem-backed block store at the given path.
2164    ///
2165    /// This will construct or open a SQLite database at the path
2166    /// `<fsblockdb_root>/blockmeta.sqlite` and will ensure that a directory exists at
2167    /// `<fsblockdb_root>/blocks` where this block store will expect to find serialized block
2168    /// files as described for [`FsBlockDb`].
2169    ///
2170    /// An application using this constructor should ensure that they call
2171    /// [`crate::chain::init::init_blockmeta_db`] at application startup to ensure
2172    /// that the resulting metadata database is properly initialized and has had all required
2173    /// migrations applied before use.
2174    pub fn for_path<P: AsRef<Path>>(fsblockdb_root: P) -> Result<Self, FsBlockDbError> {
2175        let meta = fs::metadata(&fsblockdb_root).map_err(FsBlockDbError::Fs)?;
2176        if meta.is_dir() {
2177            let db_path = fsblockdb_root.as_ref().join("blockmeta.sqlite");
2178            let blocks_dir = fsblockdb_root.as_ref().join("blocks");
2179            fs::create_dir_all(&blocks_dir)?;
2180            Ok(FsBlockDb {
2181                conn: Connection::open(db_path).map_err(FsBlockDbError::Db)?,
2182                blocks_dir,
2183            })
2184        } else {
2185            Err(FsBlockDbError::InvalidBlockstoreRoot(
2186                fsblockdb_root.as_ref().to_path_buf(),
2187            ))
2188        }
2189    }
2190
2191    /// Returns the maximum height of blocks known to the block metadata database.
2192    pub fn get_max_cached_height(&self) -> Result<Option<BlockHeight>, FsBlockDbError> {
2193        Ok(chain::blockmetadb_get_max_cached_height(&self.conn)?)
2194    }
2195
2196    /// Adds a set of block metadata entries to the metadata database, overwriting any
2197    /// existing entries at the given block heights.
2198    ///
2199    /// This will return an error if any block file corresponding to one of these metadata records
2200    /// is absent from the blocks directory.
2201    pub fn write_block_metadata(&self, block_meta: &[BlockMeta]) -> Result<(), FsBlockDbError> {
2202        for m in block_meta {
2203            let block_path = m.block_file_path(&self.blocks_dir);
2204            match fs::metadata(&block_path) {
2205                Err(e) => {
2206                    return Err(match e.kind() {
2207                        io::ErrorKind::NotFound => FsBlockDbError::MissingBlockPath(block_path),
2208                        _ => FsBlockDbError::Fs(e),
2209                    });
2210                }
2211                Ok(meta) => {
2212                    if !meta.is_file() {
2213                        return Err(FsBlockDbError::InvalidBlockPath(block_path));
2214                    }
2215                }
2216            }
2217        }
2218
2219        Ok(chain::blockmetadb_insert(&self.conn, block_meta)?)
2220    }
2221
2222    /// Returns the metadata for the block with the given height, if it exists in the
2223    /// database.
2224    pub fn find_block(&self, height: BlockHeight) -> Result<Option<BlockMeta>, FsBlockDbError> {
2225        Ok(chain::blockmetadb_find_block(&self.conn, height)?)
2226    }
2227
2228    /// Rewinds the BlockMeta Db to the `block_height` provided.
2229    ///
2230    /// This doesn't delete any files referenced by the records
2231    /// stored in BlockMeta.
2232    ///
2233    /// If the requested height is greater than or equal to the height
2234    /// of the last scanned block, or if the DB is empty, this function
2235    /// does nothing.
2236    pub fn truncate_to_height(&self, block_height: BlockHeight) -> Result<(), FsBlockDbError> {
2237        Ok(chain::blockmetadb_truncate_to_height(
2238            &self.conn,
2239            block_height,
2240        )?)
2241    }
2242}
2243
2244#[cfg(feature = "unstable")]
2245impl BlockSource for FsBlockDb {
2246    type Error = FsBlockDbError;
2247
2248    fn with_blocks<F, DbErrT>(
2249        &self,
2250        from_height: Option<BlockHeight>,
2251        limit: Option<usize>,
2252        with_row: F,
2253    ) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
2254    where
2255        F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
2256    {
2257        fsblockdb_with_blocks(self, from_height, limit, with_row)
2258    }
2259}
2260
2261#[cfg(feature = "unstable")]
2262impl std::fmt::Display for FsBlockDbError {
2263    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2264        match self {
2265            FsBlockDbError::Fs(io_error) => {
2266                write!(f, "Failed to access the file system: {}", io_error)
2267            }
2268            FsBlockDbError::Db(e) => {
2269                write!(f, "There was a problem with the sqlite db: {}", e)
2270            }
2271            FsBlockDbError::Protobuf(e) => {
2272                write!(f, "Failed to parse protobuf-encoded record: {}", e)
2273            }
2274            FsBlockDbError::MissingBlockPath(block_path) => {
2275                write!(
2276                    f,
2277                    "CompactBlock file expected but not found at {}",
2278                    block_path.display(),
2279                )
2280            }
2281            FsBlockDbError::InvalidBlockstoreRoot(fsblockdb_root) => {
2282                write!(
2283                    f,
2284                    "The block storage root {} is not a directory",
2285                    fsblockdb_root.display(),
2286                )
2287            }
2288            FsBlockDbError::InvalidBlockPath(block_path) => {
2289                write!(
2290                    f,
2291                    "CompactBlock path {} is not a file",
2292                    block_path.display(),
2293                )
2294            }
2295            FsBlockDbError::CorruptedData(e) => {
2296                write!(
2297                    f,
2298                    "The block cache has corrupted data and this caused an error: {}",
2299                    e,
2300                )
2301            }
2302            FsBlockDbError::CacheMiss(height) => {
2303                write!(
2304                    f,
2305                    "Requested height {} does not exist in the block cache",
2306                    height
2307                )
2308            }
2309        }
2310    }
2311}
2312
2313#[cfg(test)]
2314#[macro_use]
2315extern crate assert_matches;
2316
2317#[cfg(test)]
2318mod tests {
2319    use std::time::{Duration, SystemTime};
2320
2321    use secrecy::{ExposeSecret, Secret, SecretVec};
2322    use uuid::Uuid;
2323    use zcash_client_backend::data_api::{
2324        chain::ChainState,
2325        testing::{TestBuilder, TestState},
2326        Account, AccountBirthday, AccountPurpose, AccountSource, WalletRead, WalletTest,
2327        WalletWrite,
2328    };
2329    use zcash_keys::keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey};
2330    use zcash_primitives::block::BlockHash;
2331    use zcash_protocol::consensus;
2332
2333    use crate::{
2334        error::SqliteClientError, testing::db::TestDbFactory, util::Clock as _,
2335        wallet::MIN_SHIELDED_DIVERSIFIER_OFFSET, AccountUuid,
2336    };
2337
2338    #[cfg(feature = "unstable")]
2339    use zcash_keys::keys::sapling;
2340
2341    #[test]
2342    fn validate_seed() {
2343        let st = TestBuilder::new()
2344            .with_data_store_factory(TestDbFactory::default())
2345            .with_account_from_sapling_activation(BlockHash([0; 32]))
2346            .build();
2347        let account = st.test_account().unwrap();
2348
2349        assert!({
2350            st.wallet()
2351                .validate_seed(account.id(), st.test_seed().unwrap())
2352                .unwrap()
2353        });
2354
2355        // check that passing an invalid account results in a failure
2356        assert!({
2357            let wrong_account_uuid = AccountUuid(Uuid::nil());
2358            !st.wallet()
2359                .validate_seed(wrong_account_uuid, st.test_seed().unwrap())
2360                .unwrap()
2361        });
2362
2363        // check that passing an invalid seed results in a failure
2364        assert!({
2365            !st.wallet()
2366                .validate_seed(account.id(), &SecretVec::new(vec![1u8; 32]))
2367                .unwrap()
2368        });
2369    }
2370
2371    #[test]
2372    pub(crate) fn get_next_available_address() {
2373        let mut st = TestBuilder::new()
2374            .with_data_store_factory(TestDbFactory::default())
2375            .with_account_from_sapling_activation(BlockHash([0; 32]))
2376            .build();
2377        let account = st.test_account().cloned().unwrap();
2378
2379        // We have to have the chain tip height in order to allocate new addresses, to record the
2380        // exposed-at height.
2381        st.wallet_mut()
2382            .update_chain_tip(account.birthday().height())
2383            .unwrap();
2384
2385        let current_addr = st
2386            .wallet()
2387            .get_last_generated_address_matching(
2388                account.id(),
2389                UnifiedAddressRequest::AllAvailableKeys,
2390            )
2391            .unwrap();
2392        assert!(current_addr.is_some());
2393
2394        let addr2 = st
2395            .wallet_mut()
2396            .get_next_available_address(account.id(), UnifiedAddressRequest::AllAvailableKeys)
2397            .unwrap()
2398            .map(|(a, _)| a);
2399        assert!(addr2.is_some());
2400        assert_ne!(current_addr, addr2);
2401
2402        let addr2_cur = st
2403            .wallet()
2404            .get_last_generated_address_matching(
2405                account.id(),
2406                UnifiedAddressRequest::AllAvailableKeys,
2407            )
2408            .unwrap();
2409        assert_eq!(addr2, addr2_cur);
2410
2411        // Perform similar tests for shielded-only addresses. These should be timestamp-based; we
2412        // will tick the clock between each generation.
2413        use zcash_keys::keys::ReceiverRequirement::*;
2414        #[cfg(feature = "orchard")]
2415        let shielded_only_request = UnifiedAddressRequest::unsafe_custom(Require, Require, Omit);
2416        #[cfg(not(feature = "orchard"))]
2417        let shielded_only_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, Omit);
2418
2419        let cur_shielded_only = st
2420            .wallet()
2421            .get_last_generated_address_matching(account.id(), shielded_only_request)
2422            .unwrap();
2423        // If transparent support is disabled, then the previous "transparent-including"
2424        // addresses were actually shielded-only, so we do have a current address.
2425        #[cfg(not(feature = "transparent-inputs"))]
2426        assert_eq!(cur_shielded_only, addr2);
2427        // If transparent support is enabled, this works as expected.
2428        #[cfg(feature = "transparent-inputs")]
2429        assert!(cur_shielded_only.is_none());
2430
2431        let di_lower = st
2432            .wallet()
2433            .db()
2434            .clock
2435            .now()
2436            .duration_since(SystemTime::UNIX_EPOCH)
2437            .expect("current time is valid")
2438            .as_secs()
2439            .saturating_add(MIN_SHIELDED_DIVERSIFIER_OFFSET);
2440
2441        let (shielded_only, di) = st
2442            .wallet_mut()
2443            .get_next_available_address(account.id(), shielded_only_request)
2444            .unwrap()
2445            .expect("generated a shielded-only address");
2446
2447        // since not every Sapling diversifier index is valid, the resulting index will be bounded
2448        // by the current time, but may not be equal to it
2449        assert!(u128::from(di) >= u128::from(di_lower));
2450
2451        let cur_shielded_only = st
2452            .wallet()
2453            .get_last_generated_address_matching(account.id(), shielded_only_request)
2454            .unwrap()
2455            .expect("retrieved the last-generated shielded-only address");
2456        assert_eq!(cur_shielded_only, shielded_only);
2457
2458        // This gives around a 2^{-32} probability of `di` and `di_2` colliding, which is
2459        // low enough for unit tests.
2460        let collision_offset = 32;
2461
2462        st.wallet_mut()
2463            .db_mut()
2464            .clock
2465            .tick(Duration::from_secs(collision_offset));
2466
2467        let (shielded_only_2, di_2) = st
2468            .wallet_mut()
2469            .get_next_available_address(account.id(), shielded_only_request)
2470            .unwrap()
2471            .expect("generated a shielded-only address");
2472        assert_ne!(shielded_only_2, shielded_only);
2473        assert!(u128::from(di_2) >= u128::from(di_lower) + u128::from(collision_offset));
2474    }
2475
2476    #[test]
2477    pub(crate) fn import_account_hd_0() {
2478        let st = TestBuilder::new()
2479            .with_data_store_factory(TestDbFactory::default())
2480            .with_account_from_sapling_activation(BlockHash([0; 32]))
2481            .set_account_index(zip32::AccountId::ZERO)
2482            .build();
2483        assert_matches!(
2484            st.test_account().unwrap().account().source(),
2485            AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32::AccountId::ZERO);
2486    }
2487
2488    #[test]
2489    pub(crate) fn import_account_hd_1_then_2() {
2490        let mut st = TestBuilder::new()
2491            .with_data_store_factory(TestDbFactory::default())
2492            .build();
2493
2494        let birthday = AccountBirthday::from_parts(
2495            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
2496            None,
2497        );
2498
2499        let seed = Secret::new(vec![0u8; 32]);
2500        let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap();
2501
2502        let first = st
2503            .wallet_mut()
2504            .import_account_hd("", &seed, zip32_index_1, &birthday, None)
2505            .unwrap();
2506        assert_matches!(
2507            first.0.source(),
2508            AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32_index_1);
2509
2510        let zip32_index_2 = zip32_index_1.next().unwrap();
2511        let second = st
2512            .wallet_mut()
2513            .import_account_hd("", &seed, zip32_index_2, &birthday, None)
2514            .unwrap();
2515        assert_matches!(
2516            second.0.source(),
2517            AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32_index_2);
2518    }
2519
2520    fn check_collisions<C, DbT: WalletTest + WalletWrite, P: consensus::Parameters>(
2521        st: &mut TestState<C, DbT, P>,
2522        ufvk: &UnifiedFullViewingKey,
2523        birthday: &AccountBirthday,
2524        is_account_collision: impl Fn(&<DbT as WalletRead>::Error) -> bool,
2525    ) where
2526        DbT::Account: core::fmt::Debug,
2527    {
2528        assert_matches!(
2529            st.wallet_mut()
2530                .import_account_ufvk("", ufvk, birthday, AccountPurpose::Spending { derivation: None }, None),
2531            Err(e) if is_account_collision(&e)
2532        );
2533
2534        // Remove the transparent component so that we don't have a match on the full UFVK.
2535        // That should still produce an AccountCollision error.
2536        #[cfg(feature = "transparent-inputs")]
2537        {
2538            assert!(ufvk.transparent().is_some());
2539            let subset_ufvk = UnifiedFullViewingKey::new(
2540                None,
2541                ufvk.sapling().cloned(),
2542                #[cfg(feature = "orchard")]
2543                ufvk.orchard().cloned(),
2544            )
2545            .unwrap();
2546            assert_matches!(
2547                st.wallet_mut().import_account_ufvk(
2548                    "",
2549                    &subset_ufvk,
2550                    birthday,
2551                    AccountPurpose::Spending { derivation: None },
2552                    None,
2553                ),
2554                Err(e) if is_account_collision(&e)
2555            );
2556        }
2557
2558        // Remove the Orchard component so that we don't have a match on the full UFVK.
2559        // That should still produce an AccountCollision error.
2560        #[cfg(feature = "orchard")]
2561        {
2562            assert!(ufvk.orchard().is_some());
2563            let subset_ufvk = UnifiedFullViewingKey::new(
2564                #[cfg(feature = "transparent-inputs")]
2565                ufvk.transparent().cloned(),
2566                ufvk.sapling().cloned(),
2567                None,
2568            )
2569            .unwrap();
2570            assert_matches!(
2571                st.wallet_mut().import_account_ufvk(
2572                    "",
2573                    &subset_ufvk,
2574                    birthday,
2575                    AccountPurpose::Spending { derivation: None },
2576                    None,
2577                ),
2578                Err(e) if is_account_collision(&e)
2579            );
2580        }
2581    }
2582
2583    #[test]
2584    pub(crate) fn import_account_hd_1_then_conflicts() {
2585        let mut st = TestBuilder::new()
2586            .with_data_store_factory(TestDbFactory::default())
2587            .build();
2588
2589        let birthday = AccountBirthday::from_parts(
2590            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
2591            None,
2592        );
2593
2594        let seed = Secret::new(vec![0u8; 32]);
2595        let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap();
2596
2597        let (first_account, _) = st
2598            .wallet_mut()
2599            .import_account_hd("", &seed, zip32_index_1, &birthday, None)
2600            .unwrap();
2601        let ufvk = first_account.ufvk().unwrap();
2602
2603        assert_matches!(
2604            st.wallet_mut().import_account_hd("", &seed, zip32_index_1, &birthday, None),
2605            Err(SqliteClientError::AccountCollision(id)) if id == first_account.id());
2606
2607        check_collisions(
2608            &mut st,
2609            ufvk,
2610            &birthday,
2611            |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == first_account.id()),
2612        );
2613    }
2614
2615    #[test]
2616    pub(crate) fn import_account_ufvk_then_conflicts() {
2617        let mut st = TestBuilder::new()
2618            .with_data_store_factory(TestDbFactory::default())
2619            .build();
2620
2621        let birthday = AccountBirthday::from_parts(
2622            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
2623            None,
2624        );
2625
2626        let seed = Secret::new(vec![0u8; 32]);
2627        let zip32_index_0 = zip32::AccountId::ZERO;
2628        let usk = UnifiedSpendingKey::from_seed(st.network(), seed.expose_secret(), zip32_index_0)
2629            .unwrap();
2630        let ufvk = usk.to_unified_full_viewing_key();
2631
2632        let account = st
2633            .wallet_mut()
2634            .import_account_ufvk(
2635                "",
2636                &ufvk,
2637                &birthday,
2638                AccountPurpose::Spending { derivation: None },
2639                None,
2640            )
2641            .unwrap();
2642        assert_eq!(
2643            ufvk.encode(st.network()),
2644            account.ufvk().unwrap().encode(st.network())
2645        );
2646
2647        assert_matches!(
2648            account.source(),
2649            AccountSource::Imported {
2650                purpose: AccountPurpose::Spending { .. },
2651                ..
2652            }
2653        );
2654
2655        assert_matches!(
2656            st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None),
2657            Err(SqliteClientError::AccountCollision(id)) if id == account.id());
2658
2659        check_collisions(
2660            &mut st,
2661            &ufvk,
2662            &birthday,
2663            |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == account.id()),
2664        );
2665    }
2666
2667    #[test]
2668    pub(crate) fn create_account_then_conflicts() {
2669        let mut st = TestBuilder::new()
2670            .with_data_store_factory(TestDbFactory::default())
2671            .build();
2672
2673        let birthday = AccountBirthday::from_parts(
2674            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
2675            None,
2676        );
2677
2678        let seed = Secret::new(vec![0u8; 32]);
2679        let zip32_index_0 = zip32::AccountId::ZERO;
2680        let seed_based = st
2681            .wallet_mut()
2682            .create_account("", &seed, &birthday, None)
2683            .unwrap();
2684        let seed_based_account = st.wallet().get_account(seed_based.0).unwrap().unwrap();
2685        let ufvk = seed_based_account.ufvk().unwrap();
2686
2687        assert_matches!(
2688            st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None),
2689            Err(SqliteClientError::AccountCollision(id)) if id == seed_based.0);
2690
2691        check_collisions(
2692            &mut st,
2693            ufvk,
2694            &birthday,
2695            |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == seed_based.0),
2696        );
2697    }
2698
2699    #[cfg(feature = "transparent-inputs")]
2700    #[test]
2701    fn transparent_receivers() {
2702        use std::collections::BTreeSet;
2703
2704        use crate::{
2705            testing::BlockCache, wallet::transparent::transaction_data_requests, GapLimits,
2706        };
2707        use zcash_client_backend::data_api::TransactionDataRequest;
2708
2709        let mut st = TestBuilder::new()
2710            .with_data_store_factory(TestDbFactory::default())
2711            .with_block_cache(BlockCache::new())
2712            .with_account_from_sapling_activation(BlockHash([0; 32]))
2713            .build();
2714        let account = st.test_account().unwrap();
2715        let ufvk = account.usk().to_unified_full_viewing_key();
2716        let (taddr, _) = account.usk().default_transparent_address();
2717        let birthday = account.birthday().height();
2718        let account_id = account.id();
2719
2720        let receivers = st
2721            .wallet()
2722            .get_transparent_receivers(account.id(), false)
2723            .unwrap();
2724
2725        // The receiver for the default UA should be in the set.
2726        assert!(receivers.contains_key(
2727            ufvk.default_address(UnifiedAddressRequest::AllAvailableKeys)
2728                .expect("A valid default address exists for the UFVK")
2729                .0
2730                .transparent()
2731                .unwrap()
2732        ));
2733
2734        // The default t-addr should be in the set.
2735        assert!(receivers.contains_key(&taddr));
2736
2737        // The chain tip height must be known in order to query for data requests.
2738        st.wallet_mut().update_chain_tip(birthday).unwrap();
2739
2740        // Transaction data requests should include a request for each ephemeral address
2741        let ephemeral_addrs = st
2742            .wallet()
2743            .get_known_ephemeral_addresses(account_id, None)
2744            .unwrap();
2745
2746        assert_eq!(
2747            ephemeral_addrs.len(),
2748            GapLimits::default().ephemeral() as usize
2749        );
2750
2751        st.wallet_mut()
2752            .db_mut()
2753            .schedule_ephemeral_address_checks()
2754            .unwrap();
2755        let data_requests =
2756            transaction_data_requests(st.wallet().conn(), &st.wallet().db().params).unwrap();
2757
2758        let base_time = st.wallet().db().clock.now();
2759        let day = Duration::from_secs(60 * 60 * 24);
2760        let mut check_times = BTreeSet::new();
2761        for (addr, _) in ephemeral_addrs {
2762            let has_valid_request = data_requests.iter().any(|req| match req {
2763                TransactionDataRequest::TransactionsInvolvingAddress {
2764                    address,
2765                    request_at: Some(t),
2766                    ..
2767                } => {
2768                    *address == addr && *t > base_time && {
2769                        let t_delta = t.duration_since(base_time).unwrap();
2770                        // This is an imprecise check; the objective of the randomized time
2771                        // selection is that all ephemeral address checks be performed within a
2772                        // day, and that their check times be distinct.
2773                        let result = t_delta < day && !check_times.contains(t);
2774                        check_times.insert(*t);
2775                        result
2776                    }
2777                }
2778                _ => false,
2779            });
2780
2781            assert!(has_valid_request);
2782        }
2783    }
2784
2785    #[cfg(feature = "unstable")]
2786    #[test]
2787    pub(crate) fn fsblockdb_api() {
2788        use zcash_client_backend::data_api::testing::AddressType;
2789        use zcash_protocol::{consensus::NetworkConstants, value::Zatoshis};
2790
2791        use crate::testing::FsBlockCache;
2792
2793        let mut st = TestBuilder::new()
2794            .with_data_store_factory(TestDbFactory::default())
2795            .with_block_cache(FsBlockCache::new())
2796            .build();
2797
2798        // The BlockMeta DB starts off empty.
2799        assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
2800
2801        // Generate some fake CompactBlocks.
2802        let seed = [0u8; 32];
2803        let hd_account_index = zip32::AccountId::ZERO;
2804        let extsk = sapling::spending_key(&seed, st.network().coin_type(), hd_account_index);
2805        let dfvk = extsk.to_diversifiable_full_viewing_key();
2806        let (h1, meta1, _) = st.generate_next_block(
2807            &dfvk,
2808            AddressType::DefaultExternal,
2809            Zatoshis::const_from_u64(5),
2810        );
2811        let (h2, meta2, _) = st.generate_next_block(
2812            &dfvk,
2813            AddressType::DefaultExternal,
2814            Zatoshis::const_from_u64(10),
2815        );
2816
2817        // The BlockMeta DB is not updated until we do so explicitly.
2818        assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
2819
2820        // Inform the BlockMeta DB about the newly-persisted CompactBlocks.
2821        st.cache().write_block_metadata(&[meta1, meta2]).unwrap();
2822
2823        // The BlockMeta DB now sees blocks up to height 2.
2824        assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h2),);
2825        assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1));
2826        assert_eq!(st.cache().find_block(h2).unwrap(), Some(meta2));
2827        assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None);
2828
2829        // Rewinding to height 1 should cause the metadata for height 2 to be deleted.
2830        st.cache().truncate_to_height(h1).unwrap();
2831        assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h1));
2832        assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1));
2833        assert_eq!(st.cache().find_block(h2).unwrap(), None);
2834        assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None);
2835    }
2836}