zcash_client_sqlite/wallet/
init.rs

1//! Functions for initializing the various databases.
2
3use std::borrow::BorrowMut;
4use std::fmt;
5use std::rc::Rc;
6
7use rand_core::RngCore;
8use regex::Regex;
9use schemerz::{Migrator, MigratorError};
10use schemerz_rusqlite::{RusqliteAdapter, RusqliteMigration};
11use secrecy::SecretVec;
12use shardtree::error::ShardTreeError;
13use uuid::Uuid;
14
15use zcash_client_backend::data_api::{SeedRelevance, WalletRead};
16use zcash_keys::keys::AddressGenerationError;
17use zcash_protocol::{consensus, value::BalanceError};
18
19use self::migrations::verify_network_compatibility;
20
21use super::commitment_tree;
22use crate::{error::SqliteClientError, util::Clock, WalletDb};
23
24pub mod migrations;
25
26const SQLITE_MAJOR_VERSION: u32 = 3;
27const MIN_SQLITE_MINOR_VERSION: u32 = 35;
28
29const MIGRATIONS_TABLE: &str = "schemer_migrations";
30
31#[derive(Debug)]
32pub enum WalletMigrationError {
33    /// A feature required by the wallet database is not supported by the version of
34    /// SQLite that the migration is running against.
35    DatabaseNotSupported(String),
36
37    /// The seed is required for the migration.
38    SeedRequired,
39
40    /// A seed was provided that is not relevant to any of the accounts within the wallet.
41    ///
42    /// Specifically, it is not relevant to any account for which [`Account::source`] is
43    /// [`AccountSource::Derived`]. We do not check whether the seed is relevant to any
44    /// imported account, because that would require brute-forcing the ZIP 32 account
45    /// index space.
46    ///
47    /// [`Account::source`]: zcash_client_backend::data_api::Account::source
48    /// [`AccountSource::Derived`]: zcash_client_backend::data_api::AccountSource::Derived
49    SeedNotRelevant,
50
51    /// Decoding of an existing value from its serialized form has failed.
52    CorruptedData(String),
53
54    /// An error occurred in migrating a Zcash address or key.
55    AddressGeneration(AddressGenerationError),
56
57    /// Wrapper for rusqlite errors.
58    DbError(rusqlite::Error),
59
60    /// Wrapper for amount balance violations
61    BalanceError(BalanceError),
62
63    /// Wrapper for commitment tree invariant violations
64    CommitmentTree(ShardTreeError<commitment_tree::Error>),
65
66    /// Reverting the specified migration is not supported.
67    CannotRevert(Uuid),
68
69    /// Some other unexpected violation of database business rules occurred
70    Other(SqliteClientError),
71}
72
73impl From<rusqlite::Error> for WalletMigrationError {
74    fn from(e: rusqlite::Error) -> Self {
75        WalletMigrationError::DbError(e)
76    }
77}
78
79impl From<BalanceError> for WalletMigrationError {
80    fn from(e: BalanceError) -> Self {
81        WalletMigrationError::BalanceError(e)
82    }
83}
84
85impl From<ShardTreeError<commitment_tree::Error>> for WalletMigrationError {
86    fn from(e: ShardTreeError<commitment_tree::Error>) -> Self {
87        WalletMigrationError::CommitmentTree(e)
88    }
89}
90
91impl From<AddressGenerationError> for WalletMigrationError {
92    fn from(e: AddressGenerationError) -> Self {
93        WalletMigrationError::AddressGeneration(e)
94    }
95}
96
97impl From<SqliteClientError> for WalletMigrationError {
98    fn from(value: SqliteClientError) -> Self {
99        match value {
100            SqliteClientError::CorruptedData(err) => WalletMigrationError::CorruptedData(err),
101            SqliteClientError::DbError(err) => WalletMigrationError::DbError(err),
102            SqliteClientError::CommitmentTree(err) => WalletMigrationError::CommitmentTree(err),
103            SqliteClientError::BalanceError(err) => WalletMigrationError::BalanceError(err),
104            SqliteClientError::AddressGeneration(err) => {
105                WalletMigrationError::AddressGeneration(err)
106            }
107            other => WalletMigrationError::Other(other),
108        }
109    }
110}
111
112impl fmt::Display for WalletMigrationError {
113    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114        match &self {
115            WalletMigrationError::DatabaseNotSupported(version) => {
116                write!(
117                    f,
118                    "The installed SQLite version {} does not support operations required by the wallet.",
119                    version
120                )
121            }
122            WalletMigrationError::SeedRequired => {
123                write!(
124                    f,
125                    "The wallet seed is required in order to update the database."
126                )
127            }
128            WalletMigrationError::SeedNotRelevant => {
129                write!(
130                    f,
131                    "The provided seed is not relevant to any derived accounts in the database."
132                )
133            }
134            WalletMigrationError::CorruptedData(reason) => {
135                write!(f, "Wallet database is corrupted: {}", reason)
136            }
137            WalletMigrationError::DbError(e) => write!(f, "{}", e),
138            WalletMigrationError::BalanceError(e) => write!(f, "Balance error: {:?}", e),
139            WalletMigrationError::CommitmentTree(e) => write!(f, "Commitment tree error: {:?}", e),
140            WalletMigrationError::AddressGeneration(e) => {
141                write!(f, "Address generation error: {:?}", e)
142            }
143            WalletMigrationError::CannotRevert(uuid) => {
144                write!(f, "Reverting migration {} is not supported", uuid)
145            }
146            WalletMigrationError::Other(err) => {
147                write!(
148                    f,
149                    "Unexpected violation of database business rules: {}",
150                    err
151                )
152            }
153        }
154    }
155}
156
157impl std::error::Error for WalletMigrationError {
158    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
159        match &self {
160            WalletMigrationError::DbError(e) => Some(e),
161            WalletMigrationError::BalanceError(e) => Some(e),
162            WalletMigrationError::CommitmentTree(e) => Some(e),
163            WalletMigrationError::AddressGeneration(e) => Some(e),
164            WalletMigrationError::Other(e) => Some(e),
165            _ => None,
166        }
167    }
168}
169
170/// Helper to enable calling regular `WalletDb` methods inside the migration code.
171///
172/// In this context we can know the full set of errors that are generated by any call we
173/// make, so we mark errors as unreachable instead of adding new `WalletMigrationError`
174/// variants.
175fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> WalletMigrationError {
176    match e {
177        SqliteClientError::CorruptedData(e) => WalletMigrationError::CorruptedData(e),
178        SqliteClientError::Protobuf(e) => WalletMigrationError::CorruptedData(e.to_string()),
179        SqliteClientError::InvalidNote => {
180            WalletMigrationError::CorruptedData("invalid note".into())
181        }
182        SqliteClientError::DecodingError(e) => WalletMigrationError::CorruptedData(e.to_string()),
183        #[cfg(feature = "transparent-inputs")]
184        SqliteClientError::TransparentDerivation(e) => {
185            WalletMigrationError::CorruptedData(e.to_string())
186        }
187        #[cfg(feature = "transparent-inputs")]
188        SqliteClientError::TransparentAddress(e) => {
189            WalletMigrationError::CorruptedData(e.to_string())
190        }
191        SqliteClientError::DbError(e) => WalletMigrationError::DbError(e),
192        SqliteClientError::Io(e) => WalletMigrationError::CorruptedData(e.to_string()),
193        SqliteClientError::InvalidMemo(e) => WalletMigrationError::CorruptedData(e.to_string()),
194        SqliteClientError::AddressGeneration(e) => WalletMigrationError::AddressGeneration(e),
195        SqliteClientError::BadAccountData(e) => WalletMigrationError::CorruptedData(e),
196        SqliteClientError::CommitmentTree(e) => WalletMigrationError::CommitmentTree(e),
197        SqliteClientError::UnsupportedPoolType(pool) => WalletMigrationError::CorruptedData(
198            format!("Wallet DB contains unsupported pool type {}", pool),
199        ),
200        SqliteClientError::BalanceError(e) => WalletMigrationError::BalanceError(e),
201        SqliteClientError::TableNotEmpty => unreachable!("wallet already initialized"),
202        SqliteClientError::BlockConflict(_)
203        | SqliteClientError::NonSequentialBlocks
204        | SqliteClientError::RequestedRewindInvalid { .. }
205        | SqliteClientError::KeyDerivationError(_)
206        | SqliteClientError::Zip32AccountIndexOutOfRange
207        | SqliteClientError::AccountCollision(_)
208        | SqliteClientError::CacheMiss(_) => {
209            unreachable!("we only call WalletRead methods; mutations can't occur")
210        }
211        #[cfg(feature = "transparent-inputs")]
212        SqliteClientError::AddressNotRecognized(_) => {
213            unreachable!("we only call WalletRead methods; mutations can't occur")
214        }
215        SqliteClientError::AccountUnknown => {
216            unreachable!("all accounts are known in migration context")
217        }
218        SqliteClientError::UnknownZip32Derivation => {
219            unreachable!("we don't call methods that require operating on imported accounts")
220        }
221        SqliteClientError::ChainHeightUnknown => {
222            unreachable!("we don't call methods that require a known chain height")
223        }
224        #[cfg(feature = "transparent-inputs")]
225        SqliteClientError::ReachedGapLimit(..) => {
226            unreachable!("we don't do ephemeral address tracking")
227        }
228        SqliteClientError::DiversifierIndexReuse(i, _) => {
229            WalletMigrationError::CorruptedData(format!(
230                "invalid attempt to overwrite address at diversifier index {}",
231                u128::from(i)
232            ))
233        }
234        SqliteClientError::AddressReuse(_, _) => {
235            unreachable!("we don't create transactions in migrations")
236        }
237        SqliteClientError::NoteFilterInvalid(_) => {
238            unreachable!("we don't do note selection in migrations")
239        }
240        #[cfg(feature = "transparent-inputs")]
241        SqliteClientError::Scheduling(e) => {
242            WalletMigrationError::Other(SqliteClientError::Scheduling(e))
243        }
244    }
245}
246
247/// Sets up the internal structure of the data database.
248///
249/// This procedure will automatically perform migration operations to update the wallet database to
250/// the database structure required by the current version of this library, and should be invoked
251/// at least once any time a client program upgrades to a new version of this library.  The
252/// operation of this procedure is idempotent, so it is safe (though not required) to invoke this
253/// operation every time the wallet is opened.
254///
255/// In order to correctly apply migrations to accounts derived from a seed, sometimes the
256/// optional `seed` argument is required. This function should first be invoked with
257/// `seed` set to `None`; if a pending migration requires the seed, the function returns
258/// `Err(schemerz::MigratorError::Migration { error: WalletMigrationError::SeedRequired, .. })`.
259/// The caller can then re-call this function with the necessary seed.
260///
261/// > Note that currently only one seed can be provided; as such, wallets containing
262/// > accounts derived from several different seeds are unsupported, and will result in an
263/// > error. Support for multi-seed wallets is being tracked in [zcash/librustzcash#1284].
264///
265/// When the `seed` argument is provided, the seed is checked against the database for
266/// _relevance_: if any account in the wallet for which [`Account::source`] is
267/// [`AccountSource::Derived`] can be derived from the given seed, the seed is relevant to
268/// the wallet. If the given seed is not relevant, the function returns
269/// `Err(schemerz::MigratorError::Migration { error: WalletMigrationError::SeedNotRelevant, .. })`
270/// or `Err(schemerz::MigratorError::Adapter(WalletMigrationError::SeedNotRelevant))`.
271///
272/// We do not check whether the seed is relevant to any imported account, because that
273/// would require brute-forcing the ZIP 32 account index space. Consequentially, seed-requiring
274/// migrations cannot be applied to imported accounts.
275///
276/// It is safe to use a wallet database previously created without the ability to create
277/// transparent spends with a build that enables transparent spends (via use of the
278/// `transparent-inputs` feature flag.) The reverse is unsafe, as wallet balance calculations would
279/// ignore the transparent UTXOs already controlled by the wallet.
280///
281/// [zcash/librustzcash#1284]: https://github.com/zcash/librustzcash/issues/1284
282/// [`Account::source`]: zcash_client_backend::data_api::Account::source
283/// [`AccountSource::Derived`]: zcash_client_backend::data_api::AccountSource::Derived
284///
285/// # Examples
286///
287/// ```
288/// # use std::error::Error;
289/// # use secrecy::SecretVec;
290/// # use tempfile::NamedTempFile;
291/// use rand_core::OsRng;
292/// use zcash_protocol::consensus::Network;
293/// use zcash_client_sqlite::{
294///     WalletDb,
295///     util::SystemClock,
296///     wallet::init::{WalletMigrationError, init_wallet_db},
297/// };
298///
299/// # fn main() -> Result<(), Box<dyn Error>> {
300/// # let data_file = NamedTempFile::new().unwrap();
301/// # let get_data_db_path = || data_file.path();
302/// # let load_seed = || -> Result<_, String> { Ok(SecretVec::new(vec![])) };
303/// let mut db = WalletDb::for_path(get_data_db_path(), Network::TestNetwork, SystemClock, OsRng)?;
304/// match init_wallet_db(&mut db, None) {
305///     Err(e)
306///         if matches!(
307///             e.source().and_then(|e| e.downcast_ref()),
308///             Some(&WalletMigrationError::SeedRequired)
309///         ) =>
310///     {
311///         let seed = load_seed()?;
312///         init_wallet_db(&mut db, Some(seed))
313///     }
314///     res => res,
315/// }?;
316/// # Ok(())
317/// # }
318/// ```
319// TODO: It would be possible to make the transition from providing transparent support to no
320// longer providing transparent support safe, by including a migration that verifies that no
321// unspent transparent outputs exist in the wallet at the time of upgrading to a version of
322// the library that does not support transparent use. It might be a good idea to add an explicit
323// check for unspent transparent outputs whenever running initialization with a version of the
324// library *not* compiled with the `transparent-inputs` feature flag, and fail if any are present.
325pub fn init_wallet_db<
326    C: BorrowMut<rusqlite::Connection>,
327    P: consensus::Parameters + 'static,
328    CL: Clock + Clone + 'static,
329    R: RngCore + Clone + 'static,
330>(
331    wdb: &mut WalletDb<C, P, CL, R>,
332    seed: Option<SecretVec<u8>>,
333) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
334    if let Some(seed) = seed {
335        WalletMigrator::new().with_seed(seed)
336    } else {
337        WalletMigrator::new()
338    }
339    .init_or_migrate(wdb)
340}
341
342/// A migrator that sets up the internal structure of the wallet database.
343///
344/// This procedure will automatically perform migration operations to update the wallet
345/// database to the database structure required by the current version of this library,
346/// and should be invoked at least once any time a client program upgrades to a new
347/// version of this library. The operation of this procedure is idempotent, so it is safe
348/// (though not required) to invoke this operation every time the wallet is opened.
349///
350/// In order to correctly apply migrations to accounts derived from a seed, sometimes the
351/// seed is required. The migrator should first be used without calling [`Self::with_seed`];
352/// if a pending migration requires the seed, [`Self::init_or_migrate`] returns
353/// `Err(schemerz::MigratorError::Migration { error: WalletMigrationError::SeedRequired, .. })`.
354/// The caller can then call [`Self::with_seed`] and then re-call [`Self::init_or_migrate`]
355/// with the necessary seed.
356///
357/// > Note that currently only one seed can be provided; as such, wallets containing
358/// > accounts derived from several different seeds are unsupported, and will result in an
359/// > error. Support for multi-seed wallets is being tracked in [zcash/librustzcash#1284].
360///
361/// When a seed is provided, it is checked against the database for _relevance_: if any
362/// account in the wallet for which [`Account::source`] is [`AccountSource::Derived`] can
363/// be derived from the given seed, the seed is relevant to the wallet. If the given seed
364/// is not relevant, [`Self::init_or_migrate`] returns
365/// `Err(schemerz::MigratorError::Migration { error: WalletMigrationError::SeedNotRelevant, .. })`
366/// or `Err(schemerz::MigratorError::Adapter(WalletMigrationError::SeedNotRelevant))`.
367///
368/// We do not check whether the seed is relevant to any imported account, because that
369/// would require brute-forcing the ZIP 32 account index space. Consequentially, seed-requiring
370/// migrations cannot be applied to imported accounts.
371///
372/// It is safe to use a wallet database previously created without the ability to create
373/// transparent spends with a build that enables transparent spends (via use of the
374/// `transparent-inputs` feature flag.) The reverse is unsafe, as wallet balance
375/// calculations would ignore the transparent UTXOs already controlled by the wallet.
376///
377/// [zcash/librustzcash#1284]: https://github.com/zcash/librustzcash/issues/1284
378/// [`Account::source`]: zcash_client_backend::data_api::Account::source
379/// [`AccountSource::Derived`]: zcash_client_backend::data_api::AccountSource::Derived
380///
381/// # Examples
382///
383/// ```
384/// # use std::error::Error;
385/// # use secrecy::SecretVec;
386/// # use tempfile::NamedTempFile;
387/// use rand_core::OsRng;
388/// use zcash_protocol::consensus::Network;
389/// use zcash_client_sqlite::{
390///     WalletDb,
391///     util::SystemClock,
392///     wallet::init::{WalletMigrationError, WalletMigrator},
393/// };
394///
395/// # fn main() -> Result<(), Box<dyn Error>> {
396/// # let data_file = NamedTempFile::new().unwrap();
397/// # let get_data_db_path = || data_file.path();
398/// # let load_seed = || -> Result<_, String> { Ok(SecretVec::new(vec![])) };
399/// let mut db = WalletDb::for_path(get_data_db_path(), Network::TestNetwork, SystemClock, OsRng)?;
400/// match WalletMigrator::new().init_or_migrate(&mut db) {
401///     Err(e)
402///         if matches!(
403///             e.source().and_then(|e| e.downcast_ref()),
404///             Some(&WalletMigrationError::SeedRequired)
405///         ) =>
406///     {
407///         let seed = load_seed()?;
408///         WalletMigrator::new()
409///             .with_seed(seed)
410///             .init_or_migrate(&mut db)
411///     }
412///     res => res,
413/// }?;
414/// # Ok(())
415/// # }
416/// ```
417pub struct WalletMigrator {
418    seed: Option<SecretVec<u8>>,
419    verify_seed_relevance: bool,
420    external_migrations: Option<Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>>>,
421}
422
423impl Default for WalletMigrator {
424    fn default() -> Self {
425        Self::new()
426    }
427}
428
429impl WalletMigrator {
430    /// Constructs a new wallet migrator.
431    pub fn new() -> Self {
432        Self {
433            seed: None,
434            verify_seed_relevance: true,
435            external_migrations: None,
436        }
437    }
438
439    /// Sets the seed for the migrator to use.
440    pub fn with_seed(mut self, seed: SecretVec<u8>) -> Self {
441        self.seed = Some(seed);
442        self
443    }
444
445    /// API for internal test usage only.
446    #[cfg(test)]
447    pub(crate) fn ignore_seed_relevance(mut self) -> Self {
448        self.verify_seed_relevance = false;
449        self
450    }
451
452    /// Sets the external migration graph to apply alongside the internal migrations.
453    ///
454    /// From a data management perspective, it can be useful to store additional data
455    /// alongside the `zcash_client_sqlite` wallet database. This method enables you to
456    /// provide an external [`schemerz`] migration graph that the migrator will apply to
457    /// the wallet database.
458    ///
459    /// # WARNING
460    ///
461    /// **DO NOT** depend on or modify internal details of the `zcash_client_sqlite`
462    /// schema!
463    ///
464    /// The internal migrations are written to take into account internal relationships
465    /// between the `zcash_client_sqlite` tables, but they will never take into account
466    /// external tables. In particular, this means that you **MUST NOT**:
467    /// - Modify the structure or contents of any internal table.
468    /// - Assume that internal IDs will exist indefinitely (instead have a backup plan for
469    ///   recovering your data relationships if a new internal migration affects your
470    ///   foreign keys).
471    ///
472    /// The `zcash_client_sqlite` schema does not have any common prefix it uses for
473    /// tables, indexes, or views. However, we promise to not use the prefix `ext_` for
474    /// any internal names. Schema created by external migrations **MUST** use name
475    /// prefixing with a prefix that is unlikely to collide with either the internal names
476    /// or other potential external schemas (e.g. `ext_myappname_*`).
477    ///
478    /// # Integration
479    ///
480    /// In order to enable anchoring your external migrations correctly with respect to
481    /// this library's internal migrations, we provide constants in the [`migrations`]
482    /// module (for each release that adds a migration) which you can include within your
483    /// [`schemerz::Migration::dependencies`] set.
484    ///
485    /// Each migration runs inside a database transaction, which has the following
486    /// implications:
487    /// - `PRAGMA foreign_keys` has no effect inside a transaction, so the migrator
488    ///   handles foreign key enforcement itself:
489    ///   - `PRAGMA foreign_keys = OFF` is set before running any migrations.
490    ///   - `PRAGMA foreign_keys = ON` is set after all migrations are successful.
491    /// - `PRAGMA legacy_alter_table` should only be used in cases where its effect is
492    ///   explicitly intended, so the migrator does not use it globally. If you want to
493    ///   rename tables without breaking foreign key relationships, you need to do so
494    ///   yourself inside individual migrations:
495    ///   ```sql
496    ///   PRAGMA legacy_alter_table = ON;
497    ///   DROP TABLE table_name;
498    ///   ALTER TABLE table_name_new RENAME TO table_name;
499    ///   PRAGMA legacy_alter_table = OFF;
500    ///   ```
501    pub fn with_external_migrations(
502        mut self,
503        migrations: Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>>,
504    ) -> Self {
505        self.external_migrations = Some(migrations);
506        self
507    }
508
509    /// Sets up the internal structure of the given wallet database to be compatible with
510    /// this library version.
511    pub fn init_or_migrate<
512        C: BorrowMut<rusqlite::Connection>,
513        P: consensus::Parameters + 'static,
514        CL: Clock + Clone + 'static,
515        R: RngCore + Clone + 'static,
516    >(
517        self,
518        wdb: &mut WalletDb<C, P, CL, R>,
519    ) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
520        self.init_or_migrate_to(wdb, &[])
521    }
522
523    /// Sets up the internal structure of the given wallet database to be compatible with
524    /// this library version.
525    pub(crate) fn init_or_migrate_to<
526        C: BorrowMut<rusqlite::Connection>,
527        P: consensus::Parameters + 'static,
528        CL: Clock + Clone + 'static,
529        R: RngCore + Clone + 'static,
530    >(
531        self,
532        wdb: &mut WalletDb<C, P, CL, R>,
533        target_migrations: &[Uuid],
534    ) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
535        init_wallet_db_internal(
536            wdb,
537            self.seed,
538            self.external_migrations,
539            target_migrations,
540            self.verify_seed_relevance,
541        )
542    }
543}
544
545fn init_wallet_db_internal<
546    C: BorrowMut<rusqlite::Connection>,
547    P: consensus::Parameters + 'static,
548    CL: Clock + Clone + 'static,
549    R: RngCore + Clone + 'static,
550>(
551    wdb: &mut WalletDb<C, P, CL, R>,
552    seed: Option<SecretVec<u8>>,
553    external_migrations: Option<Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>>>,
554    target_migrations: &[Uuid],
555    verify_seed_relevance: bool,
556) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
557    let seed = seed.map(Rc::new);
558
559    verify_sqlite_version_compatibility(wdb.conn.borrow()).map_err(MigratorError::Adapter)?;
560
561    // Turn off foreign key enforcement, to ensure that table replacement does not break foreign
562    // key references in table definitions.
563    //
564    // It is necessary to perform this operation globally using the outer connection because this
565    // pragma has no effect when set or unset within a transaction.
566    wdb.conn
567        .borrow()
568        .execute_batch("PRAGMA foreign_keys = OFF;")
569        .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
570
571    // Temporarily take ownership of the connection in a wrapper to perform the initial migration
572    // table setup. This extra adapter creation could be omitted if `RusqliteAdapter` provided an
573    // accessor for the connection that it wraps, or if it provided a mechanism to query to
574    // determine whether a given migration has been applied. (see
575    // https://github.com/zcash/schemerz/issues/6)
576    {
577        let adapter = RusqliteAdapter::<'_, WalletMigrationError>::new(
578            wdb.conn.borrow_mut(),
579            Some(MIGRATIONS_TABLE.to_string()),
580        );
581        adapter.init().expect("Migrations table setup succeeds.");
582    }
583
584    // Now that we are certain that the migrations table exists, verify that if the database
585    // already contains account data, any stored UFVKs correspond to the same network that the
586    // migrations are being run for.
587    verify_network_compatibility(wdb.conn.borrow(), &wdb.params).map_err(MigratorError::Adapter)?;
588
589    // Now create the adapter that we're actually going to use to perform the migrations, and
590    // proceed.
591    let adapter = RusqliteAdapter::new(wdb.conn.borrow_mut(), Some(MIGRATIONS_TABLE.to_string()));
592    let mut migrator = Migrator::new(adapter);
593    migrator
594        .register_multiple(
595            migrations::all_migrations(
596                &wdb.params,
597                wdb.clock.clone(),
598                wdb.rng.clone(),
599                seed.clone(),
600            )
601            .into_iter(),
602        )
603        .expect("Wallet migration registration should have been successful.");
604    if let Some(migrations) = external_migrations {
605        migrator.register_multiple(migrations.into_iter())?;
606    }
607    if target_migrations.is_empty() {
608        migrator.up(None)?;
609    } else {
610        for target_migration in target_migrations {
611            migrator.up(Some(*target_migration))?;
612        }
613    }
614    wdb.conn
615        .borrow()
616        .execute("PRAGMA foreign_keys = ON", [])
617        .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
618
619    // Now that the migration succeeded, check whether the seed is relevant to the wallet.
620    // We can only check this if we have migrated as far as `full_account_ids::MIGRATION_ID`,
621    // but unfortunately `schemer` does not currently expose its DAG of migrations. As a
622    // consequence, the caller has to choose whether or not this check should be performed
623    // based upon which migrations they're asking to apply.
624    if verify_seed_relevance {
625        if let Some(seed) = seed {
626            match wdb
627                .seed_relevance_to_derived_accounts(&seed)
628                .map_err(sqlite_client_error_to_wallet_migration_error)?
629            {
630                SeedRelevance::Relevant { .. } => (),
631                // Every seed is relevant to a wallet with no accounts; this is most likely a
632                // new wallet database being initialized for the first time.
633                SeedRelevance::NoAccounts => (),
634                // No seed is relevant to a wallet that only has imported accounts.
635                SeedRelevance::NotRelevant | SeedRelevance::NoDerivedAccounts => {
636                    return Err(WalletMigrationError::SeedNotRelevant.into())
637                }
638            }
639        }
640    }
641
642    Ok(())
643}
644
645/// Verify that the sqlite version in use supports the features required by this library.
646/// Note that the version of sqlite available to the database backend may be different
647/// from what is used to query the views that are part of the public API.
648fn verify_sqlite_version_compatibility(
649    conn: &rusqlite::Connection,
650) -> Result<(), WalletMigrationError> {
651    let sqlite_version =
652        conn.query_row("SELECT sqlite_version()", [], |row| row.get::<_, String>(0))?;
653
654    let version_re = Regex::new(r"^(?<major>[0-9]+)\.(?<minor>[0-9]+).*$").unwrap();
655    let captures =
656        version_re
657            .captures(&sqlite_version)
658            .ok_or(WalletMigrationError::DatabaseNotSupported(
659                "Unknown".to_owned(),
660            ))?;
661    let parse_version_part = |part: &str| {
662        captures[part].parse::<u32>().map_err(|_| {
663            WalletMigrationError::CorruptedData(format!(
664                "Cannot decode SQLite {} version component {}",
665                part, &captures[part]
666            ))
667        })
668    };
669    let major = parse_version_part("major")?;
670    let minor = parse_version_part("minor")?;
671
672    if major != SQLITE_MAJOR_VERSION || minor < MIN_SQLITE_MINOR_VERSION {
673        Err(WalletMigrationError::DatabaseNotSupported(sqlite_version))
674    } else {
675        Ok(())
676    }
677}
678
679#[cfg(test)]
680pub(crate) mod testing {
681    use rand::RngCore;
682    use schemerz::MigratorError;
683    use secrecy::SecretVec;
684    use uuid::Uuid;
685    use zcash_protocol::consensus;
686
687    use crate::{util::Clock, WalletDb};
688
689    use super::WalletMigrationError;
690
691    pub(crate) fn init_wallet_db<
692        P: consensus::Parameters + 'static,
693        CL: Clock + Clone + 'static,
694        R: RngCore + Clone + 'static,
695    >(
696        wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
697        seed: Option<SecretVec<u8>>,
698    ) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
699        super::init_wallet_db_internal(wdb, seed, None, &[], true)
700    }
701}
702
703#[cfg(test)]
704mod tests {
705    use rand::RngCore;
706    use rusqlite::{self, named_params, Connection, ToSql};
707    use secrecy::Secret;
708
709    use tempfile::NamedTempFile;
710
711    use ::sapling::zip32::ExtendedFullViewingKey;
712    use zcash_client_backend::data_api::testing::TestBuilder;
713    use zcash_keys::{
714        address::Address,
715        encoding::{encode_extended_full_viewing_key, encode_payment_address},
716        keys::{
717            sapling, ReceiverRequirement::*, UnifiedAddressRequest, UnifiedFullViewingKey,
718            UnifiedSpendingKey,
719        },
720    };
721    use zcash_primitives::transaction::{TransactionData, TxVersion};
722    use zcash_protocol::consensus::{self, BlockHeight, BranchId, Network, NetworkConstants};
723    use zip32::AccountId;
724
725    use super::testing::init_wallet_db;
726    use crate::{
727        testing::db::{test_clock, test_rng, TestDbFactory},
728        util::Clock,
729        wallet::db,
730        WalletDb, UA_TRANSPARENT,
731    };
732
733    #[cfg(feature = "transparent-inputs")]
734    use {
735        super::WalletMigrationError,
736        crate::wallet::{self, pool_code, PoolType},
737        zcash_address::test_vectors,
738        zcash_client_backend::data_api::WalletWrite,
739        zip32::DiversifierIndex,
740    };
741
742    pub(crate) fn describe_tables(conn: &Connection) -> Result<Vec<String>, rusqlite::Error> {
743        let result = conn
744            .prepare("SELECT sql FROM sqlite_schema WHERE type = 'table' ORDER BY tbl_name")?
745            .query_and_then([], |row| row.get::<_, String>(0))?
746            .collect::<Result<Vec<_>, _>>()?;
747
748        Ok(result)
749    }
750
751    #[test]
752    fn verify_schema() {
753        let st = TestBuilder::new()
754            .with_data_store_factory(TestDbFactory::default())
755            .build();
756
757        use regex::Regex;
758        let re = Regex::new(r"\s+").unwrap();
759
760        let expected_tables = vec![
761            db::TABLE_ACCOUNTS,
762            db::TABLE_ADDRESSES,
763            db::TABLE_BLOCKS,
764            db::TABLE_NULLIFIER_MAP,
765            db::TABLE_ORCHARD_RECEIVED_NOTE_SPENDS,
766            db::TABLE_ORCHARD_RECEIVED_NOTES,
767            db::TABLE_ORCHARD_TREE_CAP,
768            db::TABLE_ORCHARD_TREE_CHECKPOINT_MARKS_REMOVED,
769            db::TABLE_ORCHARD_TREE_CHECKPOINTS,
770            db::TABLE_ORCHARD_TREE_SHARDS,
771            db::TABLE_SAPLING_RECEIVED_NOTE_SPENDS,
772            db::TABLE_SAPLING_RECEIVED_NOTES,
773            db::TABLE_SAPLING_TREE_CAP,
774            db::TABLE_SAPLING_TREE_CHECKPOINT_MARKS_REMOVED,
775            db::TABLE_SAPLING_TREE_CHECKPOINTS,
776            db::TABLE_SAPLING_TREE_SHARDS,
777            db::TABLE_SCAN_QUEUE,
778            db::TABLE_SCHEMERZ_MIGRATIONS,
779            db::TABLE_SENT_NOTES,
780            db::TABLE_SQLITE_SEQUENCE,
781            db::TABLE_TRANSACTIONS,
782            db::TABLE_TRANSPARENT_RECEIVED_OUTPUT_SPENDS,
783            db::TABLE_TRANSPARENT_RECEIVED_OUTPUTS,
784            db::TABLE_TRANSPARENT_SPEND_MAP,
785            db::TABLE_TRANSPARENT_SPEND_SEARCH_QUEUE,
786            db::TABLE_TX_LOCATOR_MAP,
787            db::TABLE_TX_RETRIEVAL_QUEUE,
788        ];
789
790        let rows = describe_tables(&st.wallet().db().conn).unwrap();
791        assert_eq!(rows.len(), expected_tables.len());
792        for (actual, expected) in rows.iter().zip(expected_tables.iter()) {
793            assert_eq!(
794                re.replace_all(actual, " "),
795                re.replace_all(expected, " ").trim(),
796            );
797        }
798
799        let expected_indices = vec![
800            db::INDEX_ACCOUNTS_UFVK,
801            db::INDEX_ACCOUNTS_UIVK,
802            db::INDEX_ACCOUNTS_UUID,
803            db::INDEX_HD_ACCOUNT,
804            db::INDEX_ADDRESSES_ACCOUNTS,
805            db::INDEX_ADDRESSES_INDICES,
806            db::INDEX_ADDRESSES_T_INDICES,
807            db::INDEX_NF_MAP_LOCATOR_IDX,
808            db::INDEX_ORCHARD_RECEIVED_NOTES_ACCOUNT,
809            db::INDEX_ORCHARD_RECEIVED_NOTES_TX,
810            db::INDEX_SAPLING_RECEIVED_NOTES_ACCOUNT,
811            db::INDEX_SAPLING_RECEIVED_NOTES_TX,
812            db::INDEX_SENT_NOTES_FROM_ACCOUNT,
813            db::INDEX_SENT_NOTES_TO_ACCOUNT,
814            db::INDEX_SENT_NOTES_TX,
815            db::INDEX_TRANSPARENT_RECEIVED_OUTPUTS_ACCOUNT_ID,
816        ];
817        let mut indices_query = st
818            .wallet()
819            .db()
820            .conn
821            .prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND sql != '' ORDER BY tbl_name, name")
822            .unwrap();
823        let mut rows = indices_query.query([]).unwrap();
824        let mut expected_idx = 0;
825        while let Some(row) = rows.next().unwrap() {
826            let sql: String = row.get(0).unwrap();
827            assert_eq!(
828                re.replace_all(&sql, " "),
829                re.replace_all(expected_indices[expected_idx], " ").trim(),
830            );
831            expected_idx += 1;
832        }
833
834        let expected_views = vec![
835            db::VIEW_ADDRESS_FIRST_USE.to_owned(),
836            db::VIEW_ADDRESS_USES.to_owned(),
837            db::view_orchard_shard_scan_ranges(st.network()),
838            db::view_orchard_shard_unscanned_ranges(),
839            db::VIEW_ORCHARD_SHARDS_SCAN_STATE.to_owned(),
840            db::VIEW_RECEIVED_OUTPUT_SPENDS.to_owned(),
841            db::VIEW_RECEIVED_OUTPUTS.to_owned(),
842            db::view_sapling_shard_scan_ranges(st.network()),
843            db::view_sapling_shard_unscanned_ranges(),
844            db::VIEW_SAPLING_SHARDS_SCAN_STATE.to_owned(),
845            db::VIEW_TRANSACTIONS.to_owned(),
846            db::VIEW_TX_OUTPUTS.to_owned(),
847        ];
848
849        let mut views_query = st
850            .wallet()
851            .db()
852            .conn
853            .prepare("SELECT sql FROM sqlite_schema WHERE type = 'view' ORDER BY tbl_name")
854            .unwrap();
855        let mut rows = views_query.query([]).unwrap();
856        let mut expected_idx = 0;
857        while let Some(row) = rows.next().unwrap() {
858            let sql: String = row.get(0).unwrap();
859            assert_eq!(
860                re.replace_all(&sql, " "),
861                re.replace_all(&expected_views[expected_idx], " ").trim(),
862            );
863            expected_idx += 1;
864        }
865    }
866
867    #[test]
868    fn external_schema_prefix_unused() {
869        let st = TestBuilder::new()
870            .with_data_store_factory(TestDbFactory::default())
871            .build();
872
873        let mut names_query = st
874            .wallet()
875            .db()
876            .conn
877            .prepare("SELECT tbl_name FROM sqlite_schema")
878            .unwrap();
879        let mut rows = names_query.query([]).unwrap();
880        while let Some(row) = rows.next().unwrap() {
881            let name: String = row.get(0).unwrap();
882            assert!(!name.starts_with("ext_"));
883        }
884    }
885
886    #[test]
887    fn init_migrate_from_0_3_0() {
888        fn init_0_3_0<P: consensus::Parameters, CL: Clock + Clone, R: RngCore + Clone>(
889            wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
890            extfvk: &ExtendedFullViewingKey,
891            account: AccountId,
892        ) -> Result<(), rusqlite::Error> {
893            wdb.conn.execute(
894                "CREATE TABLE accounts (
895                    account INTEGER PRIMARY KEY,
896                    extfvk TEXT NOT NULL,
897                    address TEXT NOT NULL
898                )",
899                [],
900            )?;
901            wdb.conn.execute(
902                "CREATE TABLE blocks (
903                    height INTEGER PRIMARY KEY,
904                    hash BLOB NOT NULL,
905                    time INTEGER NOT NULL,
906                    sapling_tree BLOB NOT NULL
907                )",
908                [],
909            )?;
910            wdb.conn.execute(
911                "CREATE TABLE transactions (
912                    id_tx INTEGER PRIMARY KEY,
913                    txid BLOB NOT NULL UNIQUE,
914                    created TEXT,
915                    block INTEGER,
916                    tx_index INTEGER,
917                    expiry_height INTEGER,
918                    raw BLOB,
919                    FOREIGN KEY (block) REFERENCES blocks(height)
920                )",
921                [],
922            )?;
923            wdb.conn.execute(
924                "CREATE TABLE received_notes (
925                    id_note INTEGER PRIMARY KEY,
926                    tx INTEGER NOT NULL,
927                    output_index INTEGER NOT NULL,
928                    account INTEGER NOT NULL,
929                    diversifier BLOB NOT NULL,
930                    value INTEGER NOT NULL,
931                    rcm BLOB NOT NULL,
932                    nf BLOB NOT NULL UNIQUE,
933                    is_change INTEGER NOT NULL,
934                    memo BLOB,
935                    spent INTEGER,
936                    FOREIGN KEY (tx) REFERENCES transactions(id_tx),
937                    FOREIGN KEY (account) REFERENCES accounts(account),
938                    FOREIGN KEY (spent) REFERENCES transactions(id_tx),
939                    CONSTRAINT tx_output UNIQUE (tx, output_index)
940                )",
941                [],
942            )?;
943            wdb.conn.execute(
944                "CREATE TABLE sapling_witnesses (
945                    id_witness INTEGER PRIMARY KEY,
946                    note INTEGER NOT NULL,
947                    block INTEGER NOT NULL,
948                    witness BLOB NOT NULL,
949                    FOREIGN KEY (note) REFERENCES received_notes(id_note),
950                    FOREIGN KEY (block) REFERENCES blocks(height),
951                    CONSTRAINT witness_height UNIQUE (note, block)
952                )",
953                [],
954            )?;
955            wdb.conn.execute(
956                "CREATE TABLE sent_notes (
957                    id_note INTEGER PRIMARY KEY,
958                    tx INTEGER NOT NULL,
959                    output_index INTEGER NOT NULL,
960                    from_account INTEGER NOT NULL,
961                    address TEXT NOT NULL,
962                    value INTEGER NOT NULL,
963                    memo BLOB,
964                    FOREIGN KEY (tx) REFERENCES transactions(id_tx),
965                    FOREIGN KEY (from_account) REFERENCES accounts(account),
966                    CONSTRAINT tx_output UNIQUE (tx, output_index)
967                )",
968                [],
969            )?;
970
971            let address = encode_payment_address(
972                wdb.params.hrp_sapling_payment_address(),
973                &extfvk.default_address().1,
974            );
975            let extfvk = encode_extended_full_viewing_key(
976                wdb.params.hrp_sapling_extended_full_viewing_key(),
977                extfvk,
978            );
979            wdb.conn.execute(
980                "INSERT INTO accounts (account, extfvk, address)
981                VALUES (?, ?, ?)",
982                [
983                    u32::from(account).to_sql()?,
984                    extfvk.to_sql()?,
985                    address.to_sql()?,
986                ],
987            )?;
988
989            Ok(())
990        }
991
992        let data_file = NamedTempFile::new().unwrap();
993        let mut db_data = WalletDb::for_path(
994            data_file.path(),
995            Network::TestNetwork,
996            test_clock(),
997            test_rng(),
998        )
999        .unwrap();
1000
1001        let seed = [0xab; 32];
1002        let account = AccountId::ZERO;
1003        let secret_key = sapling::spending_key(&seed, db_data.params.coin_type(), account);
1004        #[allow(deprecated)]
1005        let extfvk = secret_key.to_extended_full_viewing_key();
1006
1007        init_0_3_0(&mut db_data, &extfvk, account).unwrap();
1008        assert_matches!(
1009            init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1010            Ok(_)
1011        );
1012    }
1013
1014    #[test]
1015    fn init_migrate_from_autoshielding_poc() {
1016        fn init_autoshielding<P: consensus::Parameters, CL, R>(
1017            wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
1018            extfvk: &ExtendedFullViewingKey,
1019            account: AccountId,
1020        ) -> Result<(), rusqlite::Error> {
1021            wdb.conn.execute(
1022                "CREATE TABLE accounts (
1023                    account INTEGER PRIMARY KEY,
1024                    extfvk TEXT NOT NULL,
1025                    address TEXT NOT NULL,
1026                    transparent_address TEXT NOT NULL
1027                )",
1028                [],
1029            )?;
1030            wdb.conn.execute(
1031                "CREATE TABLE blocks (
1032                    height INTEGER PRIMARY KEY,
1033                    hash BLOB NOT NULL,
1034                    time INTEGER NOT NULL,
1035                    sapling_tree BLOB NOT NULL
1036                )",
1037                [],
1038            )?;
1039            wdb.conn.execute(
1040                "CREATE TABLE transactions (
1041                    id_tx INTEGER PRIMARY KEY,
1042                    txid BLOB NOT NULL UNIQUE,
1043                    created TEXT,
1044                    block INTEGER,
1045                    tx_index INTEGER,
1046                    expiry_height INTEGER,
1047                    raw BLOB,
1048                    FOREIGN KEY (block) REFERENCES blocks(height)
1049                )",
1050                [],
1051            )?;
1052            wdb.conn.execute(
1053                "CREATE TABLE received_notes (
1054                    id_note INTEGER PRIMARY KEY,
1055                    tx INTEGER NOT NULL,
1056                    output_index INTEGER NOT NULL,
1057                    account INTEGER NOT NULL,
1058                    diversifier BLOB NOT NULL,
1059                    value INTEGER NOT NULL,
1060                    rcm BLOB NOT NULL,
1061                    nf BLOB NOT NULL UNIQUE,
1062                    is_change INTEGER NOT NULL,
1063                    memo BLOB,
1064                    spent INTEGER,
1065                    FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1066                    FOREIGN KEY (account) REFERENCES accounts(account),
1067                    FOREIGN KEY (spent) REFERENCES transactions(id_tx),
1068                    CONSTRAINT tx_output UNIQUE (tx, output_index)
1069                )",
1070                [],
1071            )?;
1072            wdb.conn.execute(
1073                "CREATE TABLE sapling_witnesses (
1074                    id_witness INTEGER PRIMARY KEY,
1075                    note INTEGER NOT NULL,
1076                    block INTEGER NOT NULL,
1077                    witness BLOB NOT NULL,
1078                    FOREIGN KEY (note) REFERENCES received_notes(id_note),
1079                    FOREIGN KEY (block) REFERENCES blocks(height),
1080                    CONSTRAINT witness_height UNIQUE (note, block)
1081                )",
1082                [],
1083            )?;
1084            wdb.conn.execute(
1085                "CREATE TABLE sent_notes (
1086                    id_note INTEGER PRIMARY KEY,
1087                    tx INTEGER NOT NULL,
1088                    output_index INTEGER NOT NULL,
1089                    from_account INTEGER NOT NULL,
1090                    address TEXT NOT NULL,
1091                    value INTEGER NOT NULL,
1092                    memo BLOB,
1093                    FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1094                    FOREIGN KEY (from_account) REFERENCES accounts(account),
1095                    CONSTRAINT tx_output UNIQUE (tx, output_index)
1096                )",
1097                [],
1098            )?;
1099            wdb.conn.execute(
1100                "CREATE TABLE utxos (
1101                    id_utxo INTEGER PRIMARY KEY,
1102                    address TEXT NOT NULL,
1103                    prevout_txid BLOB NOT NULL,
1104                    prevout_idx INTEGER NOT NULL,
1105                    script BLOB NOT NULL,
1106                    value_zat INTEGER NOT NULL,
1107                    height INTEGER NOT NULL,
1108                    spent_in_tx INTEGER,
1109                    FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
1110                    CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
1111                )",
1112                [],
1113            )?;
1114
1115            let address = encode_payment_address(
1116                wdb.params.hrp_sapling_payment_address(),
1117                &extfvk.default_address().1,
1118            );
1119            let extfvk = encode_extended_full_viewing_key(
1120                wdb.params.hrp_sapling_extended_full_viewing_key(),
1121                extfvk,
1122            );
1123            wdb.conn.execute(
1124                "INSERT INTO accounts (account, extfvk, address, transparent_address)
1125                VALUES (?, ?, ?, '')",
1126                [
1127                    u32::from(account).to_sql()?,
1128                    extfvk.to_sql()?,
1129                    address.to_sql()?,
1130                ],
1131            )?;
1132
1133            // add a sapling sent note
1134            wdb.conn.execute(
1135                "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'000000')",
1136                [],
1137            )?;
1138
1139            let tx = TransactionData::from_parts(
1140                TxVersion::V4,
1141                BranchId::Canopy,
1142                0,
1143                BlockHeight::from(0),
1144                None,
1145                None,
1146                None,
1147                None,
1148            )
1149            .freeze()
1150            .unwrap();
1151
1152            let mut tx_bytes = vec![];
1153            tx.write(&mut tx_bytes).unwrap();
1154            wdb.conn.execute(
1155                "INSERT INTO transactions (block, id_tx, txid, raw) VALUES (0, 0, :txid, :tx_bytes)",
1156                named_params![
1157                    ":txid": tx.txid().as_ref(),
1158                    ":tx_bytes": &tx_bytes[..]
1159                ],
1160            )?;
1161            wdb.conn.execute(
1162                "INSERT INTO sent_notes (tx, output_index, from_account, address, value)
1163                VALUES (0, 0, ?, ?, 0)",
1164                [u32::from(account).to_sql()?, address.to_sql()?],
1165            )?;
1166
1167            Ok(())
1168        }
1169
1170        let data_file = NamedTempFile::new().unwrap();
1171        let mut db_data = WalletDb::for_path(
1172            data_file.path(),
1173            Network::TestNetwork,
1174            test_clock(),
1175            test_rng(),
1176        )
1177        .unwrap();
1178
1179        let seed = [0xab; 32];
1180        let account = AccountId::ZERO;
1181        let secret_key = sapling::spending_key(&seed, db_data.params.coin_type(), account);
1182        #[allow(deprecated)]
1183        let extfvk = secret_key.to_extended_full_viewing_key();
1184
1185        init_autoshielding(&mut db_data, &extfvk, account).unwrap();
1186        assert_matches!(
1187            init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1188            Ok(_)
1189        );
1190    }
1191
1192    #[test]
1193    fn init_migrate_from_main_pre_migrations() {
1194        fn init_main<P: consensus::Parameters, CL, R>(
1195            wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
1196            ufvk: &UnifiedFullViewingKey,
1197            account: AccountId,
1198        ) -> Result<(), rusqlite::Error> {
1199            wdb.conn.execute(
1200                "CREATE TABLE accounts (
1201                    account INTEGER PRIMARY KEY,
1202                    ufvk TEXT,
1203                    address TEXT,
1204                    transparent_address TEXT
1205                )",
1206                [],
1207            )?;
1208            wdb.conn.execute(
1209                "CREATE TABLE blocks (
1210                    height INTEGER PRIMARY KEY,
1211                    hash BLOB NOT NULL,
1212                    time INTEGER NOT NULL,
1213                    sapling_tree BLOB NOT NULL
1214                )",
1215                [],
1216            )?;
1217            wdb.conn.execute(
1218                "CREATE TABLE transactions (
1219                    id_tx INTEGER PRIMARY KEY,
1220                    txid BLOB NOT NULL UNIQUE,
1221                    created TEXT,
1222                    block INTEGER,
1223                    tx_index INTEGER,
1224                    expiry_height INTEGER,
1225                    raw BLOB,
1226                    FOREIGN KEY (block) REFERENCES blocks(height)
1227                )",
1228                [],
1229            )?;
1230            wdb.conn.execute(
1231                "CREATE TABLE received_notes (
1232                    id_note INTEGER PRIMARY KEY,
1233                    tx INTEGER NOT NULL,
1234                    output_index INTEGER NOT NULL,
1235                    account INTEGER NOT NULL,
1236                    diversifier BLOB NOT NULL,
1237                    value INTEGER NOT NULL,
1238                    rcm BLOB NOT NULL,
1239                    nf BLOB NOT NULL UNIQUE,
1240                    is_change INTEGER NOT NULL,
1241                    memo BLOB,
1242                    spent INTEGER,
1243                    FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1244                    FOREIGN KEY (account) REFERENCES accounts(account),
1245                    FOREIGN KEY (spent) REFERENCES transactions(id_tx),
1246                    CONSTRAINT tx_output UNIQUE (tx, output_index)
1247                )",
1248                [],
1249            )?;
1250            wdb.conn.execute(
1251                "CREATE TABLE sapling_witnesses (
1252                    id_witness INTEGER PRIMARY KEY,
1253                    note INTEGER NOT NULL,
1254                    block INTEGER NOT NULL,
1255                    witness BLOB NOT NULL,
1256                    FOREIGN KEY (note) REFERENCES received_notes(id_note),
1257                    FOREIGN KEY (block) REFERENCES blocks(height),
1258                    CONSTRAINT witness_height UNIQUE (note, block)
1259                )",
1260                [],
1261            )?;
1262            wdb.conn.execute(
1263                "CREATE TABLE sent_notes (
1264                    id_note INTEGER PRIMARY KEY,
1265                    tx INTEGER NOT NULL,
1266                    output_pool INTEGER NOT NULL,
1267                    output_index INTEGER NOT NULL,
1268                    from_account INTEGER NOT NULL,
1269                    address TEXT NOT NULL,
1270                    value INTEGER NOT NULL,
1271                    memo BLOB,
1272                    FOREIGN KEY (tx) REFERENCES transactions(id_tx),
1273                    FOREIGN KEY (from_account) REFERENCES accounts(account),
1274                    CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index)
1275                )",
1276                [],
1277            )?;
1278            wdb.conn.execute(
1279                "CREATE TABLE utxos (
1280                    id_utxo INTEGER PRIMARY KEY,
1281                    address TEXT NOT NULL,
1282                    prevout_txid BLOB NOT NULL,
1283                    prevout_idx INTEGER NOT NULL,
1284                    script BLOB NOT NULL,
1285                    value_zat INTEGER NOT NULL,
1286                    height INTEGER NOT NULL,
1287                    spent_in_tx INTEGER,
1288                    FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
1289                    CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
1290                )",
1291                [],
1292            )?;
1293
1294            let ufvk_str = ufvk.encode(&wdb.params);
1295
1296            // Unified addresses at the time of the addition of migrations did not contain an
1297            // Orchard component.
1298            let ua_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, UA_TRANSPARENT);
1299            let address_str = Address::Unified(
1300                ufvk.default_address(ua_request)
1301                    .expect("A valid default address exists for the UFVK")
1302                    .0,
1303            )
1304            .encode(&wdb.params);
1305            wdb.conn.execute(
1306                "INSERT INTO accounts (account, ufvk, address, transparent_address)
1307                VALUES (?, ?, ?, '')",
1308                [
1309                    u32::from(account).to_sql()?,
1310                    ufvk_str.to_sql()?,
1311                    address_str.to_sql()?,
1312                ],
1313            )?;
1314
1315            // add a transparent "sent note"
1316            #[cfg(feature = "transparent-inputs")]
1317            {
1318                let taddr = Address::Transparent(
1319                    *ufvk
1320                        .default_address(ua_request)
1321                        .expect("A valid default address exists for the UFVK")
1322                        .0
1323                        .transparent()
1324                        .unwrap(),
1325                )
1326                .encode(&wdb.params);
1327                wdb.conn.execute(
1328                    "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, x'000000')",
1329                    [],
1330                )?;
1331                wdb.conn.execute(
1332                    "INSERT INTO transactions (block, id_tx, txid) VALUES (0, 0, '')",
1333                    [],
1334                )?;
1335                wdb.conn.execute(
1336                    "INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value)
1337                    VALUES (0, ?, 0, ?, ?, 0)",
1338                    [pool_code(PoolType::TRANSPARENT).to_sql()?, u32::from(account).to_sql()?, taddr.to_sql()?])?;
1339            }
1340
1341            Ok(())
1342        }
1343
1344        let data_file = NamedTempFile::new().unwrap();
1345        let mut db_data = WalletDb::for_path(
1346            data_file.path(),
1347            Network::TestNetwork,
1348            test_clock(),
1349            test_rng(),
1350        )
1351        .unwrap();
1352
1353        let seed = [0xab; 32];
1354        let account = AccountId::ZERO;
1355        let secret_key = UnifiedSpendingKey::from_seed(&db_data.params, &seed, account).unwrap();
1356
1357        init_main(
1358            &mut db_data,
1359            &secret_key.to_unified_full_viewing_key(),
1360            account,
1361        )
1362        .unwrap();
1363        assert_matches!(
1364            init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1365            Ok(_)
1366        );
1367    }
1368
1369    #[test]
1370    #[cfg(feature = "transparent-inputs")]
1371    fn account_produces_expected_ua_sequence() {
1372        use zcash_client_backend::data_api::{AccountBirthday, AccountSource, WalletRead};
1373        use zcash_primitives::block::BlockHash;
1374
1375        let network = Network::MainNetwork;
1376        let data_file = NamedTempFile::new().unwrap();
1377        let mut db_data =
1378            WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap();
1379        assert_matches!(init_wallet_db(&mut db_data, None), Ok(_));
1380
1381        // Prior to adding any accounts, every seed phrase is relevant to the wallet.
1382        let seed = test_vectors::UNIFIED[0].root_seed;
1383        let other_seed = [7; 32];
1384        assert_matches!(
1385            init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1386            Ok(())
1387        );
1388        assert_matches!(
1389            init_wallet_db(&mut db_data, Some(Secret::new(other_seed.to_vec()))),
1390            Ok(())
1391        );
1392
1393        let birthday = AccountBirthday::from_sapling_activation(&network, BlockHash([0; 32]));
1394        let (account_id, _usk) = db_data
1395            .create_account("", &Secret::new(seed.to_vec()), &birthday, None)
1396            .unwrap();
1397
1398        // We have to have the chain tip height in order to allocate new addresses, to record the
1399        // exposed-at height.
1400        db_data.update_chain_tip(birthday.height()).unwrap();
1401
1402        assert_matches!(
1403            db_data.get_account(account_id),
1404            Ok(Some(account)) if matches!(
1405                &account.kind,
1406                AccountSource::Derived{derivation, ..} if derivation.account_index() == zip32::AccountId::ZERO,
1407            )
1408        );
1409
1410        // After adding an account, only the real seed phrase is relevant to the wallet.
1411        assert_matches!(
1412            init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
1413            Ok(())
1414        );
1415        assert_matches!(
1416            init_wallet_db(&mut db_data, Some(Secret::new(other_seed.to_vec()))),
1417            Err(schemerz::MigratorError::Adapter(
1418                WalletMigrationError::SeedNotRelevant
1419            ))
1420        );
1421
1422        for tv in &test_vectors::UNIFIED[..3] {
1423            if let Some(Address::Unified(tvua)) =
1424                Address::decode(&Network::MainNetwork, tv.unified_addr)
1425            {
1426                // hardcoded with knowledge of test vectors
1427                let ua_request = UnifiedAddressRequest::unsafe_custom(Omit, Require, Require);
1428
1429                let (ua, di) = wallet::get_last_generated_address_matching(
1430                    &db_data.conn,
1431                    &db_data.params,
1432                    account_id,
1433                    if tv.diversifier_index == 0 {
1434                        UnifiedAddressRequest::AllAvailableKeys
1435                    } else {
1436                        ua_request
1437                    },
1438                )
1439                .unwrap()
1440                .expect("create_account generated the first address");
1441                assert_eq!(DiversifierIndex::from(tv.diversifier_index), di);
1442                assert_eq!(tvua.transparent(), ua.transparent());
1443                assert_eq!(tvua.sapling(), ua.sapling());
1444                #[cfg(not(feature = "orchard"))]
1445                assert_eq!(tv.unified_addr, ua.encode(&Network::MainNetwork));
1446
1447                db_data
1448                    .get_next_available_address(account_id, ua_request)
1449                    .unwrap()
1450                    .expect("get_next_available_address generated an address");
1451            } else {
1452                panic!(
1453                    "{} did not decode to a valid unified address",
1454                    tv.unified_addr
1455                );
1456            }
1457        }
1458    }
1459}