diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index 2fa90e826..baed0a129 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -16,6 +16,8 @@ use zcash_client_backend::{ }; use zcash_primitives::{consensus, transaction::components::amount::BalanceError}; +use self::migrations::verify_network_compatibility; + use super::commitment_tree; use crate::{error::SqliteClientError, WalletDb}; @@ -24,6 +26,8 @@ mod migrations; const SQLITE_MAJOR_VERSION: u32 = 3; const MIN_SQLITE_MINOR_VERSION: u32 = 35; +const MIGRATIONS_TABLE: &str = "schemer_migrations"; + #[derive(Debug)] pub enum WalletMigrationError { /// A feature required by the wallet database is not supported by the version of @@ -330,9 +334,28 @@ pub(crate) fn init_wallet_db_internal( wdb.conn .execute_batch("PRAGMA foreign_keys = OFF;") .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?; - let adapter = RusqliteAdapter::new(&mut wdb.conn, Some("schemer_migrations".to_string())); - adapter.init().expect("Migrations table setup succeeds."); + // Temporarily take ownership of the connection in a wrapper to perform the initial migration + // table setup. This extra adapter creation could be omitted if `RusqliteAdapter` provided an + // accessor for the connection that it wraps, or if it provided a mechanism to query to + // determine whether a given migration has been applied. (see + // https://github.com/zcash/schemerz/issues/6) + { + let adapter = RusqliteAdapter::<'_, WalletMigrationError>::new( + &mut wdb.conn, + Some(MIGRATIONS_TABLE.to_string()), + ); + adapter.init().expect("Migrations table setup succeeds."); + } + + // Now that we are certain that the migrations table exists, verify that if the database + // already contains account data, any stored UFVKs correspond to the same network that the + // migrations are being run for. + verify_network_compatibility(&wdb.conn, &wdb.params).map_err(MigratorError::Adapter)?; + + // Now create the adapter that we're actually going to use to perform the migrations, and + // proceed. + let adapter = RusqliteAdapter::new(&mut wdb.conn, Some(MIGRATIONS_TABLE.to_string())); let mut migrator = Migrator::new(adapter); migrator .register_multiple(migrations::all_migrations(&wdb.params, seed.clone()).into_iter()) diff --git a/zcash_client_sqlite/src/wallet/init/migrations.rs b/zcash_client_sqlite/src/wallet/init/migrations.rs index 8ec0e9b49..b66ef4cc3 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations.rs @@ -32,9 +32,11 @@ mod wallet_summaries; use std::rc::Rc; +use rusqlite::{named_params, OptionalExtension}; use schemerz_rusqlite::RusqliteMigration; use secrecy::SecretVec; use uuid::Uuid; +use zcash_address::unified::{Encoding as _, Ufvk}; use zcash_protocol::consensus; use super::WalletMigrationError; @@ -206,6 +208,52 @@ const V_0_11_0: &[Uuid] = &[ /// Leaf migrations in the 0.11.1 release. const V_0_11_1: &[Uuid] = &[tx_retrieval_queue::MIGRATION_ID]; +pub(super) fn verify_network_compatibility( + conn: &rusqlite::Connection, + params: &P, +) -> Result<(), WalletMigrationError> { + // Ensure that the `ufvk_support` migration has been applied; if it hasn't, we won't be able to + // validate that the UFVKs in the wallet correspond to the network type that the wallet is + // being migrated for. + let has_ufvk = conn + .query_row( + &format!( + "SELECT 1 FROM {} WHERE id = :migration_id", + super::MIGRATIONS_TABLE + ), + named_params![":migration_id": &ufvk_support::MIGRATION_ID.as_bytes()[..]], + |row| row.get::<_, bool>(0), + ) + .optional()? + == Some(true); + + if has_ufvk { + let mut fvks_stmt = conn.prepare("SELECT ufvk FROM accounts")?; + let mut rows = fvks_stmt.query([])?; + while let Some(row) = rows.next()? { + let ufvk_str = row.get::<_, String>(0)?; + let (network, _) = Ufvk::decode(&ufvk_str).map_err(|e| { + WalletMigrationError::CorruptedData(format!("Unable to parse UFVK: {e}")) + })?; + + if network != params.network_type() { + let network_name = |n| match n { + consensus::NetworkType::Main => "mainnet", + consensus::NetworkType::Test => "testnet", + consensus::NetworkType::Regtest => "regtest", + }; + return Err(WalletMigrationError::CorruptedData(format!( + "Network type mismatch: account UFVK is for {} but attempting to initialize for {}.", + network_name(network), + network_name(params.network_type()) + ))); + } + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use std::collections::HashSet; diff --git a/zcash_client_sqlite/src/wallet/init/migrations/add_account_birthdays.rs b/zcash_client_sqlite/src/wallet/init/migrations/add_account_birthdays.rs index 0d43a90d3..89b08448a 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/add_account_birthdays.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_account_birthdays.rs @@ -75,9 +75,12 @@ impl RusqliteMigration for Migration

{ #[cfg(test)] mod tests { + use rusqlite::named_params; use secrecy::Secret; use tempfile::NamedTempFile; + use zcash_keys::keys::UnifiedSpendingKey; use zcash_protocol::consensus::Network; + use zip32::AccountId; use super::{DEPENDENCIES, MIGRATION_ID}; use crate::{wallet::init::init_wallet_db_internal, WalletDb}; @@ -85,7 +88,8 @@ mod tests { #[test] fn migrate() { let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); + let network = Network::TestNetwork; + let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap(); let seed_bytes = vec![0xab; 32]; init_wallet_db_internal( @@ -96,9 +100,16 @@ mod tests { ) .unwrap(); + let usk = + UnifiedSpendingKey::from_seed(&network, &seed_bytes[..], AccountId::ZERO).unwrap(); + let ufvk_str = usk.to_unified_full_viewing_key().encode(&network); + db_data .conn - .execute_batch(r#"INSERT INTO accounts (account, ufvk) VALUES (0, 'not_a_real_ufvk');"#) + .execute( + "INSERT INTO accounts (account, ufvk) VALUES (0, :ufvk_str)", + named_params![":ufvk_str": ufvk_str], + ) .unwrap(); db_data .conn diff --git a/zcash_client_sqlite/src/wallet/transparent.rs b/zcash_client_sqlite/src/wallet/transparent.rs index b63848040..11bf60f18 100644 --- a/zcash_client_sqlite/src/wallet/transparent.rs +++ b/zcash_client_sqlite/src/wallet/transparent.rs @@ -142,9 +142,16 @@ pub(crate) fn get_legacy_transparent_address( let (network, uivk) = Uivk::decode(&uivk_str) .map_err(|e| SqliteClientError::CorruptedData(format!("Unable to parse UIVK: {e}")))?; if params.network_type() != network { - return Err(SqliteClientError::CorruptedData( - "Network type mismatch".to_owned(), - )); + let network_name = |n| match n { + consensus::NetworkType::Main => "mainnet", + consensus::NetworkType::Test => "testnet", + consensus::NetworkType::Regtest => "regtest", + }; + return Err(SqliteClientError::CorruptedData(format!( + "Network type mismatch: account UIVK is for {} but a {} address was requested.", + network_name(network), + network_name(params.network_type()) + ))); } // Derive the default transparent address (if it wasn't already part of a derived UA).