Skip to content

Commit

Permalink
zcash_client_sqlite: Verify that the database is for the correct netw…
Browse files Browse the repository at this point in the history
…ork before (most) migrations.
  • Loading branch information
nuttycom committed Oct 29, 2024
1 parent 1d451b2 commit 6afa7b4
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 4 deletions.
27 changes: 25 additions & 2 deletions zcash_client_sqlite/src/wallet/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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
Expand Down Expand Up @@ -330,9 +334,28 @@ fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
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, those accounts 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()))
Expand Down
48 changes: 48 additions & 0 deletions zcash_client_sqlite/src/wallet/init/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ mod wallet_summaries;

use std::rc::Rc;

use rusqlite::{named_params, OptionalExtension};
use schemer_rusqlite::RusqliteMigration;
use secrecy::SecretVec;
use uuid::Uuid;
use zcash_address::unified::{Encoding as _, Ufvk};
use zcash_protocol::consensus;

use super::WalletMigrationError;
Expand Down Expand Up @@ -202,6 +204,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<P: consensus::Parameters>(
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,21 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {

#[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};

#[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(
Expand All @@ -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
Expand Down

0 comments on commit 6afa7b4

Please sign in to comment.