Skip to main content

zcash_client_sqlite/wallet/
init.rs

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