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