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