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, TargetValue, 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    ShieldedProtocol,
82};
83use zip32::{fingerprint::SeedFingerprint, DiversifierIndex};
84
85use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
86use wallet::{
87    commitment_tree::{self, put_shard_roots},
88    common::spendable_notes_meta,
89    scanning::replace_queue_entries,
90    upsert_address, SubtreeProgressEstimator,
91};
92
93#[cfg(feature = "orchard")]
94use {
95    incrementalmerkletree::frontier::Frontier, shardtree::store::Checkpoint,
96    std::collections::BTreeMap, zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT,
97};
98
99#[cfg(feature = "transparent-inputs")]
100use {
101    crate::wallet::transparent::ephemeral::schedule_ephemeral_address_checks,
102    ::transparent::{address::TransparentAddress, bundle::OutPoint, keys::NonHardenedChildIndex},
103    std::collections::BTreeSet,
104    zcash_client_backend::wallet::TransparentAddressMetadata,
105    zcash_keys::encoding::AddressCodec,
106    zcash_protocol::value::Zatoshis,
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: TargetValue,
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        use zcash_protocol::value::Zatoshis;
1012
1013        let mut stmt_sent = self.conn.borrow().prepare(
1014            "SELECT value, to_address,
1015                    a.cached_transparent_receiver_address, a.transparent_child_index
1016             FROM sent_notes
1017             JOIN transactions t ON t.id_tx = sent_notes.tx
1018             LEFT JOIN transparent_received_outputs tro ON tro.transaction_id = t.id_tx
1019             LEFT JOIN addresses a ON a.id = tro.address_id AND a.key_scope = :key_scope
1020             WHERE t.txid = :txid
1021             ORDER BY value",
1022        )?;
1023
1024        let sends = stmt_sent
1025            .query_map(
1026                named_params![
1027                    ":txid": txid.as_ref(),
1028                    ":key_scope": KeyScope::Ephemeral.encode()
1029                ],
1030                |row| {
1031                    let v = row.get(0)?;
1032                    let to_address = row
1033                        .get::<_, Option<String>>(1)?
1034                        .and_then(|s| Address::decode(&self.params, &s));
1035                    let ephemeral_address = row
1036                        .get::<_, Option<String>>(2)?
1037                        .and_then(|s| Address::decode(&self.params, &s));
1038                    let address_index: Option<u32> = row.get(3)?;
1039                    Ok((v, to_address, ephemeral_address.zip(address_index)))
1040                },
1041            )?
1042            .map(|res| {
1043                let (amount, external_recipient, ephemeral_address) = res?;
1044                Ok::<_, <Self as WalletRead>::Error>(OutputOfSentTx::from_parts(
1045                    Zatoshis::from_u64(amount)?,
1046                    external_recipient,
1047                    ephemeral_address,
1048                ))
1049            })
1050            .collect::<Result<_, _>>()?;
1051
1052        Ok(sends)
1053    }
1054
1055    fn get_checkpoint_history(
1056        &self,
1057        protocol: &ShieldedProtocol,
1058    ) -> Result<
1059        Vec<(BlockHeight, Option<incrementalmerkletree::Position>)>,
1060        <Self as WalletRead>::Error,
1061    > {
1062        wallet::testing::get_checkpoint_history(self.conn.borrow(), protocol)
1063    }
1064
1065    #[cfg(feature = "transparent-inputs")]
1066    fn get_transparent_output(
1067        &self,
1068        outpoint: &OutPoint,
1069        allow_unspendable: bool,
1070    ) -> Result<Option<WalletTransparentOutput>, <Self as InputSource>::Error> {
1071        wallet::transparent::get_wallet_transparent_output(
1072            self.conn.borrow(),
1073            outpoint,
1074            allow_unspendable,
1075        )
1076    }
1077
1078    fn get_notes(
1079        &self,
1080        protocol: ShieldedProtocol,
1081    ) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, <Self as InputSource>::Error> {
1082        let (table_prefix, index_col, _) = wallet::common::per_protocol_names(protocol);
1083        let mut stmt_received_notes = self.conn.borrow().prepare(&format!(
1084            "SELECT txid, {index_col}
1085             FROM {table_prefix}_received_notes rn
1086             INNER JOIN transactions ON transactions.id_tx = rn.tx
1087             WHERE transactions.block IS NOT NULL
1088             AND recipient_key_scope IS NOT NULL
1089             AND nf IS NOT NULL
1090             AND commitment_tree_position IS NOT NULL"
1091        ))?;
1092
1093        let result = stmt_received_notes
1094            .query_map([], |row| {
1095                let txid: [u8; 32] = row.get(0)?;
1096                let output_index: u32 = row.get(1)?;
1097                let note = self
1098                    .get_spendable_note(&TxId::from_bytes(txid), protocol, output_index)
1099                    .unwrap()
1100                    .unwrap();
1101                Ok(note)
1102            })?
1103            .collect::<Result<Vec<_>, _>>()?;
1104
1105        Ok(result)
1106    }
1107}
1108
1109impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL: Clock, R> WalletWrite
1110    for WalletDb<C, P, CL, R>
1111{
1112    type UtxoRef = UtxoId;
1113
1114    fn create_account(
1115        &mut self,
1116        account_name: &str,
1117        seed: &SecretVec<u8>,
1118        birthday: &AccountBirthday,
1119        key_source: Option<&str>,
1120    ) -> Result<(Self::AccountId, UnifiedSpendingKey), Self::Error> {
1121        self.borrow_mut().transactionally(|wdb| {
1122            let seed_fingerprint =
1123                SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
1124                    SqliteClientError::BadAccountData(
1125                        "Seed must be between 32 and 252 bytes in length.".to_owned(),
1126                    )
1127                })?;
1128            let zip32_account_index =
1129                wallet::max_zip32_account_index(wdb.conn.0, &seed_fingerprint)?
1130                    .map(|a| {
1131                        a.next()
1132                            .ok_or(SqliteClientError::Zip32AccountIndexOutOfRange)
1133                    })
1134                    .transpose()?
1135                    .unwrap_or(zip32::AccountId::ZERO);
1136
1137            let usk = UnifiedSpendingKey::from_seed(
1138                &wdb.params,
1139                seed.expose_secret(),
1140                zip32_account_index,
1141            )
1142            .map_err(|_| SqliteClientError::KeyDerivationError(zip32_account_index))?;
1143            let ufvk = usk.to_unified_full_viewing_key();
1144
1145            let account = wallet::add_account(
1146                wdb.conn.0,
1147                &wdb.params,
1148                account_name,
1149                &AccountSource::Derived {
1150                    derivation: Zip32Derivation::new(seed_fingerprint, zip32_account_index),
1151                    key_source: key_source.map(|s| s.to_owned()),
1152                },
1153                wallet::ViewingKey::Full(Box::new(ufvk)),
1154                birthday,
1155                #[cfg(feature = "transparent-inputs")]
1156                &wdb.gap_limits,
1157            )?;
1158
1159            Ok((account.id(), usk))
1160        })
1161    }
1162
1163    fn import_account_hd(
1164        &mut self,
1165        account_name: &str,
1166        seed: &SecretVec<u8>,
1167        account_index: zip32::AccountId,
1168        birthday: &AccountBirthday,
1169        key_source: Option<&str>,
1170    ) -> Result<(Self::Account, UnifiedSpendingKey), Self::Error> {
1171        self.transactionally(|wdb| {
1172            let seed_fingerprint =
1173                SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
1174                    SqliteClientError::BadAccountData(
1175                        "Seed must be between 32 and 252 bytes in length.".to_owned(),
1176                    )
1177                })?;
1178
1179            let usk =
1180                UnifiedSpendingKey::from_seed(&wdb.params, seed.expose_secret(), account_index)
1181                    .map_err(|_| SqliteClientError::KeyDerivationError(account_index))?;
1182            let ufvk = usk.to_unified_full_viewing_key();
1183
1184            let account = wallet::add_account(
1185                wdb.conn.0,
1186                &wdb.params,
1187                account_name,
1188                &AccountSource::Derived {
1189                    derivation: Zip32Derivation::new(seed_fingerprint, account_index),
1190                    key_source: key_source.map(|s| s.to_owned()),
1191                },
1192                wallet::ViewingKey::Full(Box::new(ufvk)),
1193                birthday,
1194                #[cfg(feature = "transparent-inputs")]
1195                &wdb.gap_limits,
1196            )?;
1197
1198            Ok((account, usk))
1199        })
1200    }
1201
1202    fn import_account_ufvk(
1203        &mut self,
1204        account_name: &str,
1205        ufvk: &UnifiedFullViewingKey,
1206        birthday: &AccountBirthday,
1207        purpose: AccountPurpose,
1208        key_source: Option<&str>,
1209    ) -> Result<Self::Account, Self::Error> {
1210        self.transactionally(|wdb| {
1211            wallet::add_account(
1212                wdb.conn.0,
1213                &wdb.params,
1214                account_name,
1215                &AccountSource::Imported {
1216                    purpose,
1217                    key_source: key_source.map(|s| s.to_owned()),
1218                },
1219                wallet::ViewingKey::Full(Box::new(ufvk.to_owned())),
1220                birthday,
1221                #[cfg(feature = "transparent-inputs")]
1222                &wdb.gap_limits,
1223            )
1224        })
1225    }
1226
1227    fn get_next_available_address(
1228        &mut self,
1229        account_uuid: Self::AccountId,
1230        request: UnifiedAddressRequest,
1231    ) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, Self::Error> {
1232        self.transactionally(|wdb| {
1233            wallet::get_next_available_address(
1234                wdb.conn.0,
1235                &wdb.params,
1236                &wdb.clock,
1237                account_uuid,
1238                request,
1239                #[cfg(feature = "transparent-inputs")]
1240                &wdb.gap_limits,
1241            )
1242        })
1243    }
1244
1245    fn get_address_for_index(
1246        &mut self,
1247        account: Self::AccountId,
1248        diversifier_index: DiversifierIndex,
1249        request: UnifiedAddressRequest,
1250    ) -> Result<Option<UnifiedAddress>, Self::Error> {
1251        if let Some(account) = self.get_account(account)? {
1252            use zcash_keys::keys::AddressGenerationError::*;
1253
1254            match account.uivk().address(diversifier_index, request) {
1255                Ok(address) => {
1256                    let chain_tip_height = wallet::chain_tip_height(self.conn.borrow())?;
1257                    upsert_address(
1258                        self.conn.borrow(),
1259                        &self.params,
1260                        account.internal_id(),
1261                        diversifier_index,
1262                        &address,
1263                        Some(chain_tip_height.unwrap_or(account.birthday())),
1264                        true,
1265                    )?;
1266
1267                    Ok(Some(address))
1268                }
1269                #[cfg(feature = "transparent-inputs")]
1270                Err(InvalidTransparentChildIndex(_)) => Ok(None),
1271                Err(InvalidSaplingDiversifierIndex(_)) => Ok(None),
1272                Err(e) => Err(SqliteClientError::AddressGeneration(e)),
1273            }
1274        } else {
1275            Err(SqliteClientError::AccountUnknown)
1276        }
1277    }
1278
1279    fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error> {
1280        let tx = self.conn.borrow_mut().transaction()?;
1281        wallet::scanning::update_chain_tip(&tx, &self.params, tip_height)?;
1282        tx.commit()?;
1283        Ok(())
1284    }
1285
1286    #[tracing::instrument(skip_all, fields(height = blocks.first().map(|b| u32::from(b.height())), count = blocks.len()))]
1287    #[allow(clippy::type_complexity)]
1288    fn put_blocks(
1289        &mut self,
1290        from_state: &ChainState,
1291        blocks: Vec<ScannedBlock<Self::AccountId>>,
1292    ) -> Result<(), Self::Error> {
1293        struct BlockPositions {
1294            height: BlockHeight,
1295            sapling_start_position: Position,
1296            #[cfg(feature = "orchard")]
1297            orchard_start_position: Position,
1298        }
1299
1300        if blocks.is_empty() {
1301            return Ok(());
1302        }
1303
1304        self.transactionally(|wdb| {
1305            let initial_block = blocks.first().expect("blocks is known to be nonempty");
1306            assert!(from_state.block_height() + 1 == initial_block.height());
1307
1308            let start_positions = BlockPositions {
1309                height: initial_block.height(),
1310                sapling_start_position: Position::from(
1311                    u64::from(initial_block.sapling().final_tree_size())
1312                        - u64::try_from(initial_block.sapling().commitments().len()).unwrap(),
1313                ),
1314                #[cfg(feature = "orchard")]
1315                orchard_start_position: Position::from(
1316                    u64::from(initial_block.orchard().final_tree_size())
1317                        - u64::try_from(initial_block.orchard().commitments().len()).unwrap(),
1318                ),
1319            };
1320
1321            let mut sapling_commitments = vec![];
1322            #[cfg(feature = "orchard")]
1323            let mut orchard_commitments = vec![];
1324            let mut last_scanned_height = None;
1325            let mut note_positions = vec![];
1326
1327            #[cfg(feature = "transparent-inputs")]
1328            let mut tx_refs = BTreeSet::new();
1329
1330            for block in blocks.into_iter() {
1331                if last_scanned_height
1332                    .iter()
1333                    .any(|prev| block.height() != *prev + 1)
1334                {
1335                    return Err(SqliteClientError::NonSequentialBlocks);
1336                }
1337
1338                // Insert the block into the database.
1339                wallet::put_block(
1340                    wdb.conn.0,
1341                    block.height(),
1342                    block.block_hash(),
1343                    block.block_time(),
1344                    block.sapling().final_tree_size(),
1345                    block.sapling().commitments().len().try_into().unwrap(),
1346                    #[cfg(feature = "orchard")]
1347                    block.orchard().final_tree_size(),
1348                    #[cfg(feature = "orchard")]
1349                    block.orchard().commitments().len().try_into().unwrap(),
1350                )?;
1351
1352                for tx in block.transactions() {
1353                    let tx_ref = wallet::put_tx_meta(wdb.conn.0, tx, block.height())?;
1354
1355                    #[cfg(feature = "transparent-inputs")]
1356                    tx_refs.insert(tx_ref);
1357
1358                    wallet::queue_tx_retrieval(wdb.conn.0, std::iter::once(tx.txid()), None)?;
1359
1360                    // Mark notes as spent and remove them from the scanning cache
1361                    for spend in tx.sapling_spends() {
1362                        wallet::sapling::mark_sapling_note_spent(wdb.conn.0, tx_ref, spend.nf())?;
1363                    }
1364                    #[cfg(feature = "orchard")]
1365                    for spend in tx.orchard_spends() {
1366                        wallet::orchard::mark_orchard_note_spent(wdb.conn.0, tx_ref, spend.nf())?;
1367                    }
1368
1369                    for output in tx.sapling_outputs() {
1370                        // Check whether this note was spent in a later block range that
1371                        // we previously scanned.
1372                        let spent_in = output
1373                            .nf()
1374                            .map(|nf| {
1375                                wallet::query_nullifier_map(
1376                                    wdb.conn.0,
1377                                    ShieldedProtocol::Sapling,
1378                                    nf,
1379                                )
1380                            })
1381                            .transpose()?
1382                            .flatten();
1383
1384                        wallet::sapling::put_received_note(
1385                            wdb.conn.0,
1386                            &wdb.params,
1387                            output,
1388                            tx_ref,
1389                            Some(block.height()),
1390                            spent_in,
1391                        )?;
1392                    }
1393                    #[cfg(feature = "orchard")]
1394                    for output in tx.orchard_outputs() {
1395                        // Check whether this note was spent in a later block range that
1396                        // we previously scanned.
1397                        let spent_in = output
1398                            .nf()
1399                            .map(|nf| {
1400                                wallet::query_nullifier_map(
1401                                    wdb.conn.0,
1402                                    ShieldedProtocol::Orchard,
1403                                    &nf.to_bytes(),
1404                                )
1405                            })
1406                            .transpose()?
1407                            .flatten();
1408
1409                        wallet::orchard::put_received_note(
1410                            wdb.conn.0,
1411                            &wdb.params,
1412                            output,
1413                            tx_ref,
1414                            Some(block.height()),
1415                            spent_in,
1416                        )?;
1417                    }
1418                }
1419
1420                // Insert the new nullifiers from this block into the nullifier map.
1421                wallet::insert_nullifier_map(
1422                    wdb.conn.0,
1423                    block.height(),
1424                    ShieldedProtocol::Sapling,
1425                    block.sapling().nullifier_map(),
1426                )?;
1427                #[cfg(feature = "orchard")]
1428                wallet::insert_nullifier_map(
1429                    wdb.conn.0,
1430                    block.height(),
1431                    ShieldedProtocol::Orchard,
1432                    &block
1433                        .orchard()
1434                        .nullifier_map()
1435                        .iter()
1436                        .map(|(txid, idx, nfs)| {
1437                            (*txid, *idx, nfs.iter().map(|nf| nf.to_bytes()).collect())
1438                        })
1439                        .collect::<Vec<_>>(),
1440                )?;
1441
1442                note_positions.extend(block.transactions().iter().flat_map(|wtx| {
1443                    let iter = wtx.sapling_outputs().iter().map(|out| {
1444                        (
1445                            ShieldedProtocol::Sapling,
1446                            out.note_commitment_tree_position(),
1447                        )
1448                    });
1449                    #[cfg(feature = "orchard")]
1450                    let iter = iter.chain(wtx.orchard_outputs().iter().map(|out| {
1451                        (
1452                            ShieldedProtocol::Orchard,
1453                            out.note_commitment_tree_position(),
1454                        )
1455                    }));
1456
1457                    iter
1458                }));
1459
1460                last_scanned_height = Some(block.height());
1461                let block_commitments = block.into_commitments();
1462                trace!(
1463                    "Sapling commitments for {:?}: {:?}",
1464                    last_scanned_height,
1465                    block_commitments
1466                        .sapling
1467                        .iter()
1468                        .map(|(_, r)| *r)
1469                        .collect::<Vec<_>>()
1470                );
1471                #[cfg(feature = "orchard")]
1472                trace!(
1473                    "Orchard commitments for {:?}: {:?}",
1474                    last_scanned_height,
1475                    block_commitments
1476                        .orchard
1477                        .iter()
1478                        .map(|(_, r)| *r)
1479                        .collect::<Vec<_>>()
1480                );
1481
1482                sapling_commitments.extend(block_commitments.sapling.into_iter().map(Some));
1483                #[cfg(feature = "orchard")]
1484                orchard_commitments.extend(block_commitments.orchard.into_iter().map(Some));
1485            }
1486
1487            #[cfg(feature = "transparent-inputs")]
1488            for (account_id, key_scope) in wallet::involved_accounts(wdb.conn.0, tx_refs)? {
1489                use ReceiverRequirement::*;
1490                wallet::transparent::generate_gap_addresses(
1491                    wdb.conn.0,
1492                    &wdb.params,
1493                    account_id,
1494                    key_scope,
1495                    &wdb.gap_limits,
1496                    UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
1497                    false,
1498                )?;
1499            }
1500
1501            // Prune the nullifier map of entries we no longer need.
1502            if let Some(meta) = wdb.block_fully_scanned()? {
1503                wallet::prune_nullifier_map(
1504                    wdb.conn.0,
1505                    meta.block_height().saturating_sub(PRUNING_DEPTH),
1506                )?;
1507            }
1508
1509            // We will have a start position and a last scanned height in all cases where
1510            // `blocks` is non-empty.
1511            if let Some(last_scanned_height) = last_scanned_height {
1512                // Create subtrees from the note commitments in parallel.
1513                const CHUNK_SIZE: usize = 1024;
1514                let sapling_subtrees = sapling_commitments
1515                    .par_chunks_mut(CHUNK_SIZE)
1516                    .enumerate()
1517                    .filter_map(|(i, chunk)| {
1518                        let start =
1519                            start_positions.sapling_start_position + (i * CHUNK_SIZE) as u64;
1520                        let end = start + chunk.len() as u64;
1521
1522                        shardtree::LocatedTree::from_iter(
1523                            start..end,
1524                            SAPLING_SHARD_HEIGHT.into(),
1525                            chunk.iter_mut().map(|n| n.take().expect("always Some")),
1526                        )
1527                    })
1528                    .map(|res| (res.subtree, res.checkpoints))
1529                    .collect::<Vec<_>>();
1530
1531                #[cfg(feature = "orchard")]
1532                let orchard_subtrees = orchard_commitments
1533                    .par_chunks_mut(CHUNK_SIZE)
1534                    .enumerate()
1535                    .filter_map(|(i, chunk)| {
1536                        let start =
1537                            start_positions.orchard_start_position + (i * CHUNK_SIZE) as u64;
1538                        let end = start + chunk.len() as u64;
1539
1540                        shardtree::LocatedTree::from_iter(
1541                            start..end,
1542                            ORCHARD_SHARD_HEIGHT.into(),
1543                            chunk.iter_mut().map(|n| n.take().expect("always Some")),
1544                        )
1545                    })
1546                    .map(|res| (res.subtree, res.checkpoints))
1547                    .collect::<Vec<_>>();
1548
1549                // Collect the complete set of Sapling checkpoints
1550                #[cfg(feature = "orchard")]
1551                let sapling_checkpoint_positions: BTreeMap<BlockHeight, Position> =
1552                    sapling_subtrees
1553                        .iter()
1554                        .flat_map(|(_, checkpoints)| checkpoints.iter())
1555                        .map(|(k, v)| (*k, *v))
1556                        .collect();
1557
1558                #[cfg(feature = "orchard")]
1559                let orchard_checkpoint_positions: BTreeMap<BlockHeight, Position> =
1560                    orchard_subtrees
1561                        .iter()
1562                        .flat_map(|(_, checkpoints)| checkpoints.iter())
1563                        .map(|(k, v)| (*k, *v))
1564                        .collect();
1565
1566                #[cfg(feature = "orchard")]
1567                fn ensure_checkpoints<
1568                    'a,
1569                    H,
1570                    I: Iterator<Item = &'a BlockHeight>,
1571                    const DEPTH: u8,
1572                >(
1573                    // An iterator of checkpoints heights for which we wish to ensure that
1574                    // checkpoints exists.
1575                    ensure_heights: I,
1576                    // The map of checkpoint positions from which we will draw note commitment tree
1577                    // position information for the newly created checkpoints.
1578                    existing_checkpoint_positions: &BTreeMap<BlockHeight, Position>,
1579                    // The frontier whose position will be used for an inserted checkpoint when
1580                    // there is no preceding checkpoint in existing_checkpoint_positions.
1581                    state_final_tree: &Frontier<H, DEPTH>,
1582                ) -> Vec<(BlockHeight, Checkpoint)> {
1583                    ensure_heights
1584                        .flat_map(|ensure_height| {
1585                            existing_checkpoint_positions
1586                                .range::<BlockHeight, _>(..=*ensure_height)
1587                                .last()
1588                                .map_or_else(
1589                                    || {
1590                                        Some((
1591                                            *ensure_height,
1592                                            state_final_tree
1593                                                .value()
1594                                                .map_or_else(Checkpoint::tree_empty, |t| {
1595                                                    Checkpoint::at_position(t.position())
1596                                                }),
1597                                        ))
1598                                    },
1599                                    |(existing_checkpoint_height, position)| {
1600                                        if *existing_checkpoint_height < *ensure_height {
1601                                            Some((
1602                                                *ensure_height,
1603                                                Checkpoint::at_position(*position),
1604                                            ))
1605                                        } else {
1606                                            // The checkpoint already exists, so we don't need to
1607                                            // do anything.
1608                                            None
1609                                        }
1610                                    },
1611                                )
1612                                .into_iter()
1613                        })
1614                        .collect::<Vec<_>>()
1615                }
1616
1617                #[cfg(feature = "orchard")]
1618                let (missing_sapling_checkpoints, missing_orchard_checkpoints) = (
1619                    ensure_checkpoints(
1620                        orchard_checkpoint_positions.keys(),
1621                        &sapling_checkpoint_positions,
1622                        from_state.final_sapling_tree(),
1623                    ),
1624                    ensure_checkpoints(
1625                        sapling_checkpoint_positions.keys(),
1626                        &orchard_checkpoint_positions,
1627                        from_state.final_orchard_tree(),
1628                    ),
1629                );
1630
1631                // Update the Sapling note commitment tree with all newly read note commitments
1632                {
1633                    let mut sapling_subtrees_iter = sapling_subtrees.into_iter();
1634                    wdb.with_sapling_tree_mut::<_, _, Self::Error>(|sapling_tree| {
1635                        debug!(
1636                            "Sapling initial tree size at {:?}: {:?}",
1637                            from_state.block_height(),
1638                            from_state.final_sapling_tree().tree_size()
1639                        );
1640                        // We insert the frontier with `Checkpoint` retention because we need to be
1641                        // able to truncate the tree back to this point.
1642                        sapling_tree.insert_frontier(
1643                            from_state.final_sapling_tree().clone(),
1644                            Retention::Checkpoint {
1645                                id: from_state.block_height(),
1646                                marking: Marking::Reference,
1647                            },
1648                        )?;
1649
1650                        for (tree, checkpoints) in &mut sapling_subtrees_iter {
1651                            sapling_tree.insert_tree(tree, checkpoints)?;
1652                        }
1653
1654                        // Ensure we have a Sapling checkpoint for each checkpointed Orchard block height.
1655                        // We skip all checkpoints below the minimum retained checkpoint in the
1656                        // Sapling tree, because branches below this height may be pruned.
1657                        #[cfg(feature = "orchard")]
1658                        {
1659                            let min_checkpoint_height = sapling_tree
1660                                .store()
1661                                .min_checkpoint_id()
1662                                .map_err(ShardTreeError::Storage)?
1663                                .expect(
1664                                    "At least one checkpoint was inserted (by insert_frontier)",
1665                                );
1666
1667                            for (height, checkpoint) in &missing_sapling_checkpoints {
1668                                if *height > min_checkpoint_height {
1669                                    sapling_tree
1670                                        .store_mut()
1671                                        .add_checkpoint(*height, checkpoint.clone())
1672                                        .map_err(ShardTreeError::Storage)?;
1673                                }
1674                            }
1675                        }
1676
1677                        Ok(())
1678                    })?;
1679                }
1680
1681                // Update the Orchard note commitment tree with all newly read note commitments
1682                #[cfg(feature = "orchard")]
1683                {
1684                    let mut orchard_subtrees = orchard_subtrees.into_iter();
1685                    wdb.with_orchard_tree_mut::<_, _, Self::Error>(|orchard_tree| {
1686                        debug!(
1687                            "Orchard initial tree size at {:?}: {:?}",
1688                            from_state.block_height(),
1689                            from_state.final_orchard_tree().tree_size()
1690                        );
1691                        // We insert the frontier with `Checkpoint` retention because we need to be
1692                        // able to truncate the tree back to this point.
1693                        orchard_tree.insert_frontier(
1694                            from_state.final_orchard_tree().clone(),
1695                            Retention::Checkpoint {
1696                                id: from_state.block_height(),
1697                                marking: Marking::Reference,
1698                            },
1699                        )?;
1700
1701                        for (tree, checkpoints) in &mut orchard_subtrees {
1702                            orchard_tree.insert_tree(tree, checkpoints)?;
1703                        }
1704
1705                        // Ensure we have an Orchard checkpoint for each checkpointed Sapling block height.
1706                        // We skip all checkpoints below the minimum retained checkpoint in the
1707                        // Orchard tree, because branches below this height may be pruned.
1708                        {
1709                            let min_checkpoint_height = orchard_tree
1710                                .store()
1711                                .min_checkpoint_id()
1712                                .map_err(ShardTreeError::Storage)?
1713                                .expect(
1714                                    "At least one checkpoint was inserted (by insert_frontier)",
1715                                );
1716
1717                            for (height, checkpoint) in &missing_orchard_checkpoints {
1718                                if *height > min_checkpoint_height {
1719                                    debug!(
1720                                        "Adding missing Orchard checkpoint for height: {:?}: {:?}",
1721                                        height,
1722                                        checkpoint.position()
1723                                    );
1724                                    orchard_tree
1725                                        .store_mut()
1726                                        .add_checkpoint(*height, checkpoint.clone())
1727                                        .map_err(ShardTreeError::Storage)?;
1728                                }
1729                            }
1730                        }
1731                        Ok(())
1732                    })?;
1733                }
1734
1735                wallet::scanning::scan_complete(
1736                    wdb.conn.0,
1737                    &wdb.params,
1738                    Range {
1739                        start: start_positions.height,
1740                        end: last_scanned_height + 1,
1741                    },
1742                    &note_positions,
1743                )?;
1744            }
1745
1746            Ok(())
1747        })
1748    }
1749
1750    fn put_received_transparent_utxo(
1751        &mut self,
1752        _output: &WalletTransparentOutput,
1753    ) -> Result<Self::UtxoRef, Self::Error> {
1754        #[cfg(feature = "transparent-inputs")]
1755        return self.transactionally(|wdb| {
1756            let (account_id, key_scope, utxo_id) =
1757                wallet::transparent::put_received_transparent_utxo(
1758                    wdb.conn.0,
1759                    &wdb.params,
1760                    _output,
1761                )?;
1762
1763            use ReceiverRequirement::*;
1764            wallet::transparent::generate_gap_addresses(
1765                wdb.conn.0,
1766                &wdb.params,
1767                account_id,
1768                key_scope,
1769                &wdb.gap_limits,
1770                UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
1771                true,
1772            )?;
1773
1774            Ok(utxo_id)
1775        });
1776
1777        #[cfg(not(feature = "transparent-inputs"))]
1778        panic!(
1779            "The wallet must be compiled with the transparent-inputs feature to use this method."
1780        );
1781    }
1782
1783    fn store_decrypted_tx(
1784        &mut self,
1785        d_tx: DecryptedTransaction<Self::AccountId>,
1786    ) -> Result<(), Self::Error> {
1787        self.transactionally(|wdb| {
1788            wallet::store_decrypted_tx(
1789                wdb.conn.0,
1790                &wdb.params,
1791                d_tx,
1792                #[cfg(feature = "transparent-inputs")]
1793                &wdb.gap_limits,
1794            )
1795        })
1796    }
1797
1798    fn store_transactions_to_be_sent(
1799        &mut self,
1800        transactions: &[SentTransaction<Self::AccountId>],
1801    ) -> Result<(), Self::Error> {
1802        self.transactionally(|wdb| {
1803            for sent_tx in transactions {
1804                wallet::store_transaction_to_be_sent(wdb.conn.0, &wdb.params, sent_tx)?;
1805            }
1806            Ok(())
1807        })
1808    }
1809
1810    fn truncate_to_height(&mut self, max_height: BlockHeight) -> Result<BlockHeight, Self::Error> {
1811        self.transactionally(|wdb| {
1812            wallet::truncate_to_height(
1813                wdb.conn.0,
1814                &wdb.params,
1815                #[cfg(feature = "transparent-inputs")]
1816                &wdb.gap_limits,
1817                max_height,
1818            )
1819        })
1820    }
1821
1822    #[cfg(feature = "transparent-inputs")]
1823    fn reserve_next_n_ephemeral_addresses(
1824        &mut self,
1825        account_id: Self::AccountId,
1826        n: usize,
1827    ) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, Self::Error> {
1828        self.transactionally(|wdb| {
1829            let account_id = wallet::get_account_ref(wdb.conn.0, account_id)?;
1830            let reserved = wallet::transparent::reserve_next_n_addresses(
1831                wdb.conn.0,
1832                &wdb.params,
1833                account_id,
1834                KeyScope::Ephemeral,
1835                wdb.gap_limits.ephemeral(),
1836                n,
1837            )?;
1838
1839            Ok(reserved.into_iter().map(|(_, a, m)| (a, m)).collect())
1840        })
1841    }
1842
1843    fn set_transaction_status(
1844        &mut self,
1845        txid: TxId,
1846        status: data_api::TransactionStatus,
1847    ) -> Result<(), Self::Error> {
1848        self.transactionally(|wdb| wallet::set_transaction_status(wdb.conn.0, txid, status))
1849    }
1850}
1851
1852pub(crate) type SaplingShardStore<C> = SqliteShardStore<C, sapling::Node, SAPLING_SHARD_HEIGHT>;
1853pub(crate) type SaplingCommitmentTree<C> =
1854    ShardTree<SaplingShardStore<C>, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>;
1855
1856pub(crate) fn sapling_tree<C>(
1857    conn: C,
1858) -> Result<SaplingCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
1859where
1860    SaplingShardStore<C>: ShardStore<H = sapling::Node, CheckpointId = BlockHeight>,
1861{
1862    Ok(ShardTree::new(
1863        SqliteShardStore::from_connection(conn, SAPLING_TABLES_PREFIX)
1864            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
1865        PRUNING_DEPTH.try_into().unwrap(),
1866    ))
1867}
1868
1869#[cfg(feature = "orchard")]
1870pub(crate) type OrchardShardStore<C> =
1871    SqliteShardStore<C, orchard::tree::MerkleHashOrchard, ORCHARD_SHARD_HEIGHT>;
1872
1873#[cfg(feature = "orchard")]
1874pub(crate) type OrchardCommitmentTree<C> = ShardTree<
1875    OrchardShardStore<C>,
1876    { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
1877    ORCHARD_SHARD_HEIGHT,
1878>;
1879
1880#[cfg(feature = "orchard")]
1881pub(crate) fn orchard_tree<C>(
1882    conn: C,
1883) -> Result<OrchardCommitmentTree<C>, ShardTreeError<commitment_tree::Error>>
1884where
1885    OrchardShardStore<C>:
1886        ShardStore<H = orchard::tree::MerkleHashOrchard, CheckpointId = BlockHeight>,
1887{
1888    Ok(ShardTree::new(
1889        SqliteShardStore::from_connection(conn, ORCHARD_TABLES_PREFIX)
1890            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
1891        PRUNING_DEPTH.try_into().unwrap(),
1892    ))
1893}
1894
1895impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletCommitmentTrees
1896    for WalletDb<C, P, CL, R>
1897{
1898    type Error = commitment_tree::Error;
1899    type SaplingShardStore<'a> = SaplingShardStore<&'a rusqlite::Transaction<'a>>;
1900
1901    fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
1902    where
1903        for<'a> F:
1904            FnMut(&'a mut SaplingCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
1905        E: From<ShardTreeError<Self::Error>>,
1906    {
1907        let tx = self
1908            .conn
1909            .borrow_mut()
1910            .transaction()
1911            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1912        let result = {
1913            let mut shardtree = sapling_tree(&tx)?;
1914            callback(&mut shardtree)?
1915        };
1916
1917        tx.commit()
1918            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1919        Ok(result)
1920    }
1921
1922    fn put_sapling_subtree_roots(
1923        &mut self,
1924        start_index: u64,
1925        roots: &[CommitmentTreeRoot<sapling::Node>],
1926    ) -> Result<(), ShardTreeError<Self::Error>> {
1927        let tx = self
1928            .conn
1929            .borrow_mut()
1930            .transaction()
1931            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1932        put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>(
1933            &tx,
1934            SAPLING_TABLES_PREFIX,
1935            start_index,
1936            roots,
1937        )?;
1938        tx.commit()
1939            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1940        Ok(())
1941    }
1942
1943    #[cfg(feature = "orchard")]
1944    type OrchardShardStore<'a> = SqliteShardStore<
1945        &'a rusqlite::Transaction<'a>,
1946        orchard::tree::MerkleHashOrchard,
1947        ORCHARD_SHARD_HEIGHT,
1948    >;
1949
1950    #[cfg(feature = "orchard")]
1951    fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
1952    where
1953        for<'a> F:
1954            FnMut(&'a mut OrchardCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
1955        E: From<ShardTreeError<Self::Error>>,
1956    {
1957        let tx = self
1958            .conn
1959            .borrow_mut()
1960            .transaction()
1961            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1962        let result = {
1963            let mut shardtree = orchard_tree(&tx)?;
1964            callback(&mut shardtree)?
1965        };
1966
1967        tx.commit()
1968            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1969        Ok(result)
1970    }
1971
1972    #[cfg(feature = "orchard")]
1973    fn put_orchard_subtree_roots(
1974        &mut self,
1975        start_index: u64,
1976        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
1977    ) -> Result<(), ShardTreeError<Self::Error>> {
1978        let tx = self
1979            .conn
1980            .borrow_mut()
1981            .transaction()
1982            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1983        put_shard_roots::<_, { ORCHARD_SHARD_HEIGHT * 2 }, ORCHARD_SHARD_HEIGHT>(
1984            &tx,
1985            ORCHARD_TABLES_PREFIX,
1986            start_index,
1987            roots,
1988        )?;
1989        tx.commit()
1990            .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
1991        Ok(())
1992    }
1993}
1994
1995impl<P: consensus::Parameters, CL, R> WalletCommitmentTrees
1996    for WalletDb<SqlTransaction<'_>, P, CL, R>
1997{
1998    type Error = commitment_tree::Error;
1999    type SaplingShardStore<'a> = crate::SaplingShardStore<&'a rusqlite::Transaction<'a>>;
2000
2001    fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
2002    where
2003        for<'a> F:
2004            FnMut(&'a mut SaplingCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
2005        E: From<ShardTreeError<commitment_tree::Error>>,
2006    {
2007        let mut shardtree = sapling_tree(self.conn.0)?;
2008        let result = callback(&mut shardtree)?;
2009
2010        Ok(result)
2011    }
2012
2013    fn put_sapling_subtree_roots(
2014        &mut self,
2015        start_index: u64,
2016        roots: &[CommitmentTreeRoot<sapling::Node>],
2017    ) -> Result<(), ShardTreeError<Self::Error>> {
2018        put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>(
2019            self.conn.0,
2020            SAPLING_TABLES_PREFIX,
2021            start_index,
2022            roots,
2023        )
2024    }
2025
2026    #[cfg(feature = "orchard")]
2027    type OrchardShardStore<'a> = crate::OrchardShardStore<&'a rusqlite::Transaction<'a>>;
2028
2029    #[cfg(feature = "orchard")]
2030    fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
2031    where
2032        for<'a> F:
2033            FnMut(&'a mut OrchardCommitmentTree<&'a rusqlite::Transaction<'a>>) -> Result<A, E>,
2034        E: From<ShardTreeError<Self::Error>>,
2035    {
2036        let mut shardtree = orchard_tree(self.conn.0)?;
2037        let result = callback(&mut shardtree)?;
2038
2039        Ok(result)
2040    }
2041
2042    #[cfg(feature = "orchard")]
2043    fn put_orchard_subtree_roots(
2044        &mut self,
2045        start_index: u64,
2046        roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
2047    ) -> Result<(), ShardTreeError<Self::Error>> {
2048        put_shard_roots::<_, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }, ORCHARD_SHARD_HEIGHT>(
2049            self.conn.0,
2050            ORCHARD_TABLES_PREFIX,
2051            start_index,
2052            roots,
2053        )
2054    }
2055}
2056
2057/// A handle for the SQLite block source.
2058pub struct BlockDb(Connection);
2059
2060impl BlockDb {
2061    /// Opens a connection to the wallet database stored at the specified path.
2062    pub fn for_path<P: AsRef<Path>>(path: P) -> Result<Self, rusqlite::Error> {
2063        Connection::open(path).map(BlockDb)
2064    }
2065}
2066
2067impl BlockSource for BlockDb {
2068    type Error = SqliteClientError;
2069
2070    fn with_blocks<F, DbErrT>(
2071        &self,
2072        from_height: Option<BlockHeight>,
2073        limit: Option<usize>,
2074        with_row: F,
2075    ) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
2076    where
2077        F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
2078    {
2079        chain::blockdb_with_blocks(self, from_height, limit, with_row)
2080    }
2081}
2082
2083/// A block source that reads block data from disk and block metadata from a SQLite database.
2084///
2085/// This block source expects each compact block to be stored on disk in the `blocks` subdirectory
2086/// of the `blockstore_root` path provided at the time of construction. Each block should be
2087/// written, as the serialized bytes of its protobuf representation, where the path for each block
2088/// has the pattern:
2089///
2090/// `<blockstore_root>/blocks/<block_height>-<block_hash>-compactblock`
2091///
2092/// where `<block_height>` is the decimal value of the height at which the block was mined, and
2093/// `<block_hash>` is the hexadecimal representation of the block hash, as produced by the
2094/// [`fmt::Display`] implementation for [`zcash_primitives::block::BlockHash`].
2095///
2096/// This block source is intended to be used with the following data flow:
2097/// * When the cache is being filled:
2098///   * The caller requests the current maximum height at which cached data is available
2099///     using [`FsBlockDb::get_max_cached_height`]. If no cached data is available, the caller
2100///     can use the wallet's synced-to height for the following operations instead.
2101///   * (recommended for privacy) the caller should round the returned height down to some 100- /
2102///     1000-block boundary.
2103///   * The caller uses the lightwalletd's `getblock` gRPC method to obtain a stream of blocks.
2104///     For each block returned, the caller writes the compact block to `blocks_dir` using the
2105///     path format specified above. It is fine to overwrite an existing block, since block hashes
2106///     are immutable and collision-resistant.
2107///   * Once a caller-determined number of blocks have been successfully written to disk, the
2108///     caller should invoke [`FsBlockDb::write_block_metadata`] with the metadata for each block
2109///     written to disk.
2110/// * The cache can then be scanned using the [`BlockSource`] implementation, providing the
2111///   wallet's synced-to-height as a starting point.
2112/// * When part of the cache is no longer needed:
2113///   * The caller determines some height `H` that is the earliest block data it needs to preserve.
2114///     This might be determined based on where the wallet is fully-synced to, or other heuristics.
2115///   * The caller searches the defined filesystem folder for all files beginning in `HEIGHT-*` where
2116///     `HEIGHT < H`, and deletes those files.
2117///
2118/// Note: This API is unstable, and may change in the future. In particular, the [`BlockSource`]
2119/// API and the above description currently assume that scanning is performed in linear block
2120/// order; this assumption is likely to be weakened and/or removed in a future update.
2121#[cfg(feature = "unstable")]
2122pub struct FsBlockDb {
2123    conn: Connection,
2124    blocks_dir: PathBuf,
2125}
2126
2127/// Errors that can be generated by the filesystem/sqlite-backed
2128/// block source.
2129#[derive(Debug)]
2130#[cfg(feature = "unstable")]
2131pub enum FsBlockDbError {
2132    Fs(io::Error),
2133    Db(rusqlite::Error),
2134    Protobuf(prost::DecodeError),
2135    MissingBlockPath(PathBuf),
2136    InvalidBlockstoreRoot(PathBuf),
2137    InvalidBlockPath(PathBuf),
2138    CorruptedData(String),
2139    CacheMiss(BlockHeight),
2140}
2141
2142#[cfg(feature = "unstable")]
2143impl From<io::Error> for FsBlockDbError {
2144    fn from(err: io::Error) -> Self {
2145        FsBlockDbError::Fs(err)
2146    }
2147}
2148
2149#[cfg(feature = "unstable")]
2150impl From<rusqlite::Error> for FsBlockDbError {
2151    fn from(err: rusqlite::Error) -> Self {
2152        FsBlockDbError::Db(err)
2153    }
2154}
2155
2156#[cfg(feature = "unstable")]
2157impl From<prost::DecodeError> for FsBlockDbError {
2158    fn from(e: prost::DecodeError) -> Self {
2159        FsBlockDbError::Protobuf(e)
2160    }
2161}
2162
2163#[cfg(feature = "unstable")]
2164impl FsBlockDb {
2165    /// Creates a filesystem-backed block store at the given path.
2166    ///
2167    /// This will construct or open a SQLite database at the path
2168    /// `<fsblockdb_root>/blockmeta.sqlite` and will ensure that a directory exists at
2169    /// `<fsblockdb_root>/blocks` where this block store will expect to find serialized block
2170    /// files as described for [`FsBlockDb`].
2171    ///
2172    /// An application using this constructor should ensure that they call
2173    /// [`crate::chain::init::init_blockmeta_db`] at application startup to ensure
2174    /// that the resulting metadata database is properly initialized and has had all required
2175    /// migrations applied before use.
2176    pub fn for_path<P: AsRef<Path>>(fsblockdb_root: P) -> Result<Self, FsBlockDbError> {
2177        let meta = fs::metadata(&fsblockdb_root).map_err(FsBlockDbError::Fs)?;
2178        if meta.is_dir() {
2179            let db_path = fsblockdb_root.as_ref().join("blockmeta.sqlite");
2180            let blocks_dir = fsblockdb_root.as_ref().join("blocks");
2181            fs::create_dir_all(&blocks_dir)?;
2182            Ok(FsBlockDb {
2183                conn: Connection::open(db_path).map_err(FsBlockDbError::Db)?,
2184                blocks_dir,
2185            })
2186        } else {
2187            Err(FsBlockDbError::InvalidBlockstoreRoot(
2188                fsblockdb_root.as_ref().to_path_buf(),
2189            ))
2190        }
2191    }
2192
2193    /// Returns the maximum height of blocks known to the block metadata database.
2194    pub fn get_max_cached_height(&self) -> Result<Option<BlockHeight>, FsBlockDbError> {
2195        Ok(chain::blockmetadb_get_max_cached_height(&self.conn)?)
2196    }
2197
2198    /// Adds a set of block metadata entries to the metadata database, overwriting any
2199    /// existing entries at the given block heights.
2200    ///
2201    /// This will return an error if any block file corresponding to one of these metadata records
2202    /// is absent from the blocks directory.
2203    pub fn write_block_metadata(&self, block_meta: &[BlockMeta]) -> Result<(), FsBlockDbError> {
2204        for m in block_meta {
2205            let block_path = m.block_file_path(&self.blocks_dir);
2206            match fs::metadata(&block_path) {
2207                Err(e) => {
2208                    return Err(match e.kind() {
2209                        io::ErrorKind::NotFound => FsBlockDbError::MissingBlockPath(block_path),
2210                        _ => FsBlockDbError::Fs(e),
2211                    });
2212                }
2213                Ok(meta) => {
2214                    if !meta.is_file() {
2215                        return Err(FsBlockDbError::InvalidBlockPath(block_path));
2216                    }
2217                }
2218            }
2219        }
2220
2221        Ok(chain::blockmetadb_insert(&self.conn, block_meta)?)
2222    }
2223
2224    /// Returns the metadata for the block with the given height, if it exists in the
2225    /// database.
2226    pub fn find_block(&self, height: BlockHeight) -> Result<Option<BlockMeta>, FsBlockDbError> {
2227        Ok(chain::blockmetadb_find_block(&self.conn, height)?)
2228    }
2229
2230    /// Rewinds the BlockMeta Db to the `block_height` provided.
2231    ///
2232    /// This doesn't delete any files referenced by the records
2233    /// stored in BlockMeta.
2234    ///
2235    /// If the requested height is greater than or equal to the height
2236    /// of the last scanned block, or if the DB is empty, this function
2237    /// does nothing.
2238    pub fn truncate_to_height(&self, block_height: BlockHeight) -> Result<(), FsBlockDbError> {
2239        Ok(chain::blockmetadb_truncate_to_height(
2240            &self.conn,
2241            block_height,
2242        )?)
2243    }
2244}
2245
2246#[cfg(feature = "unstable")]
2247impl BlockSource for FsBlockDb {
2248    type Error = FsBlockDbError;
2249
2250    fn with_blocks<F, DbErrT>(
2251        &self,
2252        from_height: Option<BlockHeight>,
2253        limit: Option<usize>,
2254        with_row: F,
2255    ) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
2256    where
2257        F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
2258    {
2259        fsblockdb_with_blocks(self, from_height, limit, with_row)
2260    }
2261}
2262
2263#[cfg(feature = "unstable")]
2264impl std::fmt::Display for FsBlockDbError {
2265    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2266        match self {
2267            FsBlockDbError::Fs(io_error) => {
2268                write!(f, "Failed to access the file system: {}", io_error)
2269            }
2270            FsBlockDbError::Db(e) => {
2271                write!(f, "There was a problem with the sqlite db: {}", e)
2272            }
2273            FsBlockDbError::Protobuf(e) => {
2274                write!(f, "Failed to parse protobuf-encoded record: {}", e)
2275            }
2276            FsBlockDbError::MissingBlockPath(block_path) => {
2277                write!(
2278                    f,
2279                    "CompactBlock file expected but not found at {}",
2280                    block_path.display(),
2281                )
2282            }
2283            FsBlockDbError::InvalidBlockstoreRoot(fsblockdb_root) => {
2284                write!(
2285                    f,
2286                    "The block storage root {} is not a directory",
2287                    fsblockdb_root.display(),
2288                )
2289            }
2290            FsBlockDbError::InvalidBlockPath(block_path) => {
2291                write!(
2292                    f,
2293                    "CompactBlock path {} is not a file",
2294                    block_path.display(),
2295                )
2296            }
2297            FsBlockDbError::CorruptedData(e) => {
2298                write!(
2299                    f,
2300                    "The block cache has corrupted data and this caused an error: {}",
2301                    e,
2302                )
2303            }
2304            FsBlockDbError::CacheMiss(height) => {
2305                write!(
2306                    f,
2307                    "Requested height {} does not exist in the block cache",
2308                    height
2309                )
2310            }
2311        }
2312    }
2313}
2314
2315#[cfg(test)]
2316#[macro_use]
2317extern crate assert_matches;
2318
2319#[cfg(test)]
2320mod tests {
2321    use std::time::{Duration, SystemTime};
2322
2323    use secrecy::{ExposeSecret, Secret, SecretVec};
2324    use uuid::Uuid;
2325    use zcash_client_backend::data_api::{
2326        chain::ChainState,
2327        testing::{TestBuilder, TestState},
2328        Account, AccountBirthday, AccountPurpose, AccountSource, WalletRead, WalletTest,
2329        WalletWrite,
2330    };
2331    use zcash_keys::keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey};
2332    use zcash_primitives::block::BlockHash;
2333    use zcash_protocol::consensus;
2334
2335    use crate::{
2336        error::SqliteClientError, testing::db::TestDbFactory, util::Clock as _,
2337        wallet::MIN_SHIELDED_DIVERSIFIER_OFFSET, AccountUuid,
2338    };
2339
2340    #[cfg(feature = "unstable")]
2341    use zcash_keys::keys::sapling;
2342
2343    #[test]
2344    fn validate_seed() {
2345        let st = TestBuilder::new()
2346            .with_data_store_factory(TestDbFactory::default())
2347            .with_account_from_sapling_activation(BlockHash([0; 32]))
2348            .build();
2349        let account = st.test_account().unwrap();
2350
2351        assert!({
2352            st.wallet()
2353                .validate_seed(account.id(), st.test_seed().unwrap())
2354                .unwrap()
2355        });
2356
2357        // check that passing an invalid account results in a failure
2358        assert!({
2359            let wrong_account_uuid = AccountUuid(Uuid::nil());
2360            !st.wallet()
2361                .validate_seed(wrong_account_uuid, st.test_seed().unwrap())
2362                .unwrap()
2363        });
2364
2365        // check that passing an invalid seed results in a failure
2366        assert!({
2367            !st.wallet()
2368                .validate_seed(account.id(), &SecretVec::new(vec![1u8; 32]))
2369                .unwrap()
2370        });
2371    }
2372
2373    #[test]
2374    pub(crate) fn get_next_available_address() {
2375        let mut st = TestBuilder::new()
2376            .with_data_store_factory(TestDbFactory::default())
2377            .with_account_from_sapling_activation(BlockHash([0; 32]))
2378            .build();
2379        let account = st.test_account().cloned().unwrap();
2380
2381        // We have to have the chain tip height in order to allocate new addresses, to record the
2382        // exposed-at height.
2383        st.wallet_mut()
2384            .update_chain_tip(account.birthday().height())
2385            .unwrap();
2386
2387        let current_addr = st
2388            .wallet()
2389            .get_last_generated_address_matching(
2390                account.id(),
2391                UnifiedAddressRequest::AllAvailableKeys,
2392            )
2393            .unwrap();
2394        assert!(current_addr.is_some());
2395
2396        let addr2 = st
2397            .wallet_mut()
2398            .get_next_available_address(account.id(), UnifiedAddressRequest::AllAvailableKeys)
2399            .unwrap()
2400            .map(|(a, _)| a);
2401        assert!(addr2.is_some());
2402        assert_ne!(current_addr, addr2);
2403
2404        let addr2_cur = st
2405            .wallet()
2406            .get_last_generated_address_matching(
2407                account.id(),
2408                UnifiedAddressRequest::AllAvailableKeys,
2409            )
2410            .unwrap();
2411        assert_eq!(addr2, addr2_cur);
2412
2413        // Perform similar tests for shielded-only addresses. These should be timestamp-based; we
2414        // will tick the clock between each generation.
2415        use zcash_keys::keys::ReceiverRequirement::*;
2416        #[cfg(feature = "orchard")]
2417        let shielded_only_request = UnifiedAddressRequest::unsafe_custom(Require, Require, Omit);
2418        #[cfg(not(feature = "orchard"))]
2419        let shielded_only_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, Omit);
2420
2421        let cur_shielded_only = st
2422            .wallet()
2423            .get_last_generated_address_matching(account.id(), shielded_only_request)
2424            .unwrap();
2425        // If transparent support is disabled, then the previous "transparent-including"
2426        // addresses were actually shielded-only, so we do have a current address.
2427        #[cfg(not(feature = "transparent-inputs"))]
2428        assert_eq!(cur_shielded_only, addr2);
2429        // If transparent support is enabled, this works as expected.
2430        #[cfg(feature = "transparent-inputs")]
2431        assert!(cur_shielded_only.is_none());
2432
2433        let di_lower = st
2434            .wallet()
2435            .db()
2436            .clock
2437            .now()
2438            .duration_since(SystemTime::UNIX_EPOCH)
2439            .expect("current time is valid")
2440            .as_secs()
2441            .saturating_add(MIN_SHIELDED_DIVERSIFIER_OFFSET);
2442
2443        let (shielded_only, di) = st
2444            .wallet_mut()
2445            .get_next_available_address(account.id(), shielded_only_request)
2446            .unwrap()
2447            .expect("generated a shielded-only address");
2448
2449        // since not every Sapling diversifier index is valid, the resulting index will be bounded
2450        // by the current time, but may not be equal to it
2451        assert!(u128::from(di) >= u128::from(di_lower));
2452
2453        let cur_shielded_only = st
2454            .wallet()
2455            .get_last_generated_address_matching(account.id(), shielded_only_request)
2456            .unwrap()
2457            .expect("retrieved the last-generated shielded-only address");
2458        assert_eq!(cur_shielded_only, shielded_only);
2459
2460        // This gives around a 2^{-32} probability of `di` and `di_2` colliding, which is
2461        // low enough for unit tests.
2462        let collision_offset = 32;
2463
2464        st.wallet_mut()
2465            .db_mut()
2466            .clock
2467            .tick(Duration::from_secs(collision_offset));
2468
2469        let (shielded_only_2, di_2) = st
2470            .wallet_mut()
2471            .get_next_available_address(account.id(), shielded_only_request)
2472            .unwrap()
2473            .expect("generated a shielded-only address");
2474        assert_ne!(shielded_only_2, shielded_only);
2475        assert!(u128::from(di_2) >= u128::from(di_lower) + u128::from(collision_offset));
2476    }
2477
2478    #[test]
2479    pub(crate) fn import_account_hd_0() {
2480        let st = TestBuilder::new()
2481            .with_data_store_factory(TestDbFactory::default())
2482            .with_account_from_sapling_activation(BlockHash([0; 32]))
2483            .set_account_index(zip32::AccountId::ZERO)
2484            .build();
2485        assert_matches!(
2486            st.test_account().unwrap().account().source(),
2487            AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32::AccountId::ZERO);
2488    }
2489
2490    #[test]
2491    pub(crate) fn import_account_hd_1_then_2() {
2492        let mut st = TestBuilder::new()
2493            .with_data_store_factory(TestDbFactory::default())
2494            .build();
2495
2496        let birthday = AccountBirthday::from_parts(
2497            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
2498            None,
2499        );
2500
2501        let seed = Secret::new(vec![0u8; 32]);
2502        let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap();
2503
2504        let first = st
2505            .wallet_mut()
2506            .import_account_hd("", &seed, zip32_index_1, &birthday, None)
2507            .unwrap();
2508        assert_matches!(
2509            first.0.source(),
2510            AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32_index_1);
2511
2512        let zip32_index_2 = zip32_index_1.next().unwrap();
2513        let second = st
2514            .wallet_mut()
2515            .import_account_hd("", &seed, zip32_index_2, &birthday, None)
2516            .unwrap();
2517        assert_matches!(
2518            second.0.source(),
2519            AccountSource::Derived { derivation, .. } if derivation.account_index() == zip32_index_2);
2520    }
2521
2522    fn check_collisions<C, DbT: WalletTest + WalletWrite, P: consensus::Parameters>(
2523        st: &mut TestState<C, DbT, P>,
2524        ufvk: &UnifiedFullViewingKey,
2525        birthday: &AccountBirthday,
2526        is_account_collision: impl Fn(&<DbT as WalletRead>::Error) -> bool,
2527    ) where
2528        DbT::Account: core::fmt::Debug,
2529    {
2530        assert_matches!(
2531            st.wallet_mut()
2532                .import_account_ufvk("", ufvk, birthday, AccountPurpose::Spending { derivation: None }, None),
2533            Err(e) if is_account_collision(&e)
2534        );
2535
2536        // Remove the transparent component so that we don't have a match on the full UFVK.
2537        // That should still produce an AccountCollision error.
2538        #[cfg(feature = "transparent-inputs")]
2539        {
2540            assert!(ufvk.transparent().is_some());
2541            let subset_ufvk = UnifiedFullViewingKey::new(
2542                None,
2543                ufvk.sapling().cloned(),
2544                #[cfg(feature = "orchard")]
2545                ufvk.orchard().cloned(),
2546            )
2547            .unwrap();
2548            assert_matches!(
2549                st.wallet_mut().import_account_ufvk(
2550                    "",
2551                    &subset_ufvk,
2552                    birthday,
2553                    AccountPurpose::Spending { derivation: None },
2554                    None,
2555                ),
2556                Err(e) if is_account_collision(&e)
2557            );
2558        }
2559
2560        // Remove the Orchard component so that we don't have a match on the full UFVK.
2561        // That should still produce an AccountCollision error.
2562        #[cfg(feature = "orchard")]
2563        {
2564            assert!(ufvk.orchard().is_some());
2565            let subset_ufvk = UnifiedFullViewingKey::new(
2566                #[cfg(feature = "transparent-inputs")]
2567                ufvk.transparent().cloned(),
2568                ufvk.sapling().cloned(),
2569                None,
2570            )
2571            .unwrap();
2572            assert_matches!(
2573                st.wallet_mut().import_account_ufvk(
2574                    "",
2575                    &subset_ufvk,
2576                    birthday,
2577                    AccountPurpose::Spending { derivation: None },
2578                    None,
2579                ),
2580                Err(e) if is_account_collision(&e)
2581            );
2582        }
2583    }
2584
2585    #[test]
2586    pub(crate) fn import_account_hd_1_then_conflicts() {
2587        let mut st = TestBuilder::new()
2588            .with_data_store_factory(TestDbFactory::default())
2589            .build();
2590
2591        let birthday = AccountBirthday::from_parts(
2592            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
2593            None,
2594        );
2595
2596        let seed = Secret::new(vec![0u8; 32]);
2597        let zip32_index_1 = zip32::AccountId::ZERO.next().unwrap();
2598
2599        let (first_account, _) = st
2600            .wallet_mut()
2601            .import_account_hd("", &seed, zip32_index_1, &birthday, None)
2602            .unwrap();
2603        let ufvk = first_account.ufvk().unwrap();
2604
2605        assert_matches!(
2606            st.wallet_mut().import_account_hd("", &seed, zip32_index_1, &birthday, None),
2607            Err(SqliteClientError::AccountCollision(id)) if id == first_account.id());
2608
2609        check_collisions(
2610            &mut st,
2611            ufvk,
2612            &birthday,
2613            |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == first_account.id()),
2614        );
2615    }
2616
2617    #[test]
2618    pub(crate) fn import_account_ufvk_then_conflicts() {
2619        let mut st = TestBuilder::new()
2620            .with_data_store_factory(TestDbFactory::default())
2621            .build();
2622
2623        let birthday = AccountBirthday::from_parts(
2624            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
2625            None,
2626        );
2627
2628        let seed = Secret::new(vec![0u8; 32]);
2629        let zip32_index_0 = zip32::AccountId::ZERO;
2630        let usk = UnifiedSpendingKey::from_seed(st.network(), seed.expose_secret(), zip32_index_0)
2631            .unwrap();
2632        let ufvk = usk.to_unified_full_viewing_key();
2633
2634        let account = st
2635            .wallet_mut()
2636            .import_account_ufvk(
2637                "",
2638                &ufvk,
2639                &birthday,
2640                AccountPurpose::Spending { derivation: None },
2641                None,
2642            )
2643            .unwrap();
2644        assert_eq!(
2645            ufvk.encode(st.network()),
2646            account.ufvk().unwrap().encode(st.network())
2647        );
2648
2649        assert_matches!(
2650            account.source(),
2651            AccountSource::Imported {
2652                purpose: AccountPurpose::Spending { .. },
2653                ..
2654            }
2655        );
2656
2657        assert_matches!(
2658            st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None),
2659            Err(SqliteClientError::AccountCollision(id)) if id == account.id());
2660
2661        check_collisions(
2662            &mut st,
2663            &ufvk,
2664            &birthday,
2665            |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == account.id()),
2666        );
2667    }
2668
2669    #[test]
2670    pub(crate) fn create_account_then_conflicts() {
2671        let mut st = TestBuilder::new()
2672            .with_data_store_factory(TestDbFactory::default())
2673            .build();
2674
2675        let birthday = AccountBirthday::from_parts(
2676            ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
2677            None,
2678        );
2679
2680        let seed = Secret::new(vec![0u8; 32]);
2681        let zip32_index_0 = zip32::AccountId::ZERO;
2682        let seed_based = st
2683            .wallet_mut()
2684            .create_account("", &seed, &birthday, None)
2685            .unwrap();
2686        let seed_based_account = st.wallet().get_account(seed_based.0).unwrap().unwrap();
2687        let ufvk = seed_based_account.ufvk().unwrap();
2688
2689        assert_matches!(
2690            st.wallet_mut().import_account_hd("", &seed, zip32_index_0, &birthday, None),
2691            Err(SqliteClientError::AccountCollision(id)) if id == seed_based.0);
2692
2693        check_collisions(
2694            &mut st,
2695            ufvk,
2696            &birthday,
2697            |e| matches!(e, SqliteClientError::AccountCollision(id) if *id == seed_based.0),
2698        );
2699    }
2700
2701    #[cfg(feature = "transparent-inputs")]
2702    #[test]
2703    fn transparent_receivers() {
2704        use std::collections::BTreeSet;
2705
2706        use crate::{
2707            testing::BlockCache, wallet::transparent::transaction_data_requests, GapLimits,
2708        };
2709        use zcash_client_backend::data_api::TransactionDataRequest;
2710
2711        let mut st = TestBuilder::new()
2712            .with_data_store_factory(TestDbFactory::default())
2713            .with_block_cache(BlockCache::new())
2714            .with_account_from_sapling_activation(BlockHash([0; 32]))
2715            .build();
2716        let account = st.test_account().unwrap();
2717        let ufvk = account.usk().to_unified_full_viewing_key();
2718        let (taddr, _) = account.usk().default_transparent_address();
2719        let birthday = account.birthday().height();
2720        let account_id = account.id();
2721
2722        let receivers = st
2723            .wallet()
2724            .get_transparent_receivers(account.id(), false)
2725            .unwrap();
2726
2727        // The receiver for the default UA should be in the set.
2728        assert!(receivers.contains_key(
2729            ufvk.default_address(UnifiedAddressRequest::AllAvailableKeys)
2730                .expect("A valid default address exists for the UFVK")
2731                .0
2732                .transparent()
2733                .unwrap()
2734        ));
2735
2736        // The default t-addr should be in the set.
2737        assert!(receivers.contains_key(&taddr));
2738
2739        // The chain tip height must be known in order to query for data requests.
2740        st.wallet_mut().update_chain_tip(birthday).unwrap();
2741
2742        // Transaction data requests should include a request for each ephemeral address
2743        let ephemeral_addrs = st
2744            .wallet()
2745            .get_known_ephemeral_addresses(account_id, None)
2746            .unwrap();
2747
2748        assert_eq!(
2749            ephemeral_addrs.len(),
2750            GapLimits::default().ephemeral() as usize
2751        );
2752
2753        st.wallet_mut()
2754            .db_mut()
2755            .schedule_ephemeral_address_checks()
2756            .unwrap();
2757        let data_requests =
2758            transaction_data_requests(st.wallet().conn(), &st.wallet().db().params).unwrap();
2759
2760        let base_time = st.wallet().db().clock.now();
2761        let day = Duration::from_secs(60 * 60 * 24);
2762        let mut check_times = BTreeSet::new();
2763        for (addr, _) in ephemeral_addrs {
2764            let has_valid_request = data_requests.iter().any(|req| match req {
2765                TransactionDataRequest::TransactionsInvolvingAddress {
2766                    address,
2767                    request_at: Some(t),
2768                    ..
2769                } => {
2770                    *address == addr && *t > base_time && {
2771                        let t_delta = t.duration_since(base_time).unwrap();
2772                        // This is an imprecise check; the objective of the randomized time
2773                        // selection is that all ephemeral address checks be performed within a
2774                        // day, and that their check times be distinct.
2775                        let result = t_delta < day && !check_times.contains(t);
2776                        check_times.insert(*t);
2777                        result
2778                    }
2779                }
2780                _ => false,
2781            });
2782
2783            assert!(has_valid_request);
2784        }
2785    }
2786
2787    #[cfg(feature = "unstable")]
2788    #[test]
2789    pub(crate) fn fsblockdb_api() {
2790        use zcash_client_backend::data_api::testing::AddressType;
2791        use zcash_protocol::{consensus::NetworkConstants, value::Zatoshis};
2792
2793        use crate::testing::FsBlockCache;
2794
2795        let mut st = TestBuilder::new()
2796            .with_data_store_factory(TestDbFactory::default())
2797            .with_block_cache(FsBlockCache::new())
2798            .build();
2799
2800        // The BlockMeta DB starts off empty.
2801        assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
2802
2803        // Generate some fake CompactBlocks.
2804        let seed = [0u8; 32];
2805        let hd_account_index = zip32::AccountId::ZERO;
2806        let extsk = sapling::spending_key(&seed, st.network().coin_type(), hd_account_index);
2807        let dfvk = extsk.to_diversifiable_full_viewing_key();
2808        let (h1, meta1, _) = st.generate_next_block(
2809            &dfvk,
2810            AddressType::DefaultExternal,
2811            Zatoshis::const_from_u64(5),
2812        );
2813        let (h2, meta2, _) = st.generate_next_block(
2814            &dfvk,
2815            AddressType::DefaultExternal,
2816            Zatoshis::const_from_u64(10),
2817        );
2818
2819        // The BlockMeta DB is not updated until we do so explicitly.
2820        assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
2821
2822        // Inform the BlockMeta DB about the newly-persisted CompactBlocks.
2823        st.cache().write_block_metadata(&[meta1, meta2]).unwrap();
2824
2825        // The BlockMeta DB now sees blocks up to height 2.
2826        assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h2),);
2827        assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1));
2828        assert_eq!(st.cache().find_block(h2).unwrap(), Some(meta2));
2829        assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None);
2830
2831        // Rewinding to height 1 should cause the metadata for height 2 to be deleted.
2832        st.cache().truncate_to_height(h1).unwrap();
2833        assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h1));
2834        assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1));
2835        assert_eq!(st.cache().find_block(h2).unwrap(), None);
2836        assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None);
2837    }
2838}