diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index 9a8f2720f..b854b369b 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -1,10 +1,11 @@ //! Functions for initializing the various databases. -use rusqlite::{self, params, types::ToSql, NO_PARAMS}; -use schemer::{migration, Migration, Migrator, MigratorError}; -use schemer_rusqlite::{RusqliteAdapter, RusqliteMigration}; -use secrecy::{ExposeSecret, SecretVec}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::fmt; + +use rusqlite::{self, types::ToSql, NO_PARAMS}; +use schemer::{Migrator, MigratorError}; +use schemer_rusqlite::RusqliteAdapter; +use secrecy::SecretVec; use uuid::Uuid; use zcash_primitives::{ @@ -14,22 +15,9 @@ use zcash_primitives::{ zip32::AccountId, }; -use zcash_client_backend::{ - address::RecipientAddress, - keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, -}; +use zcash_client_backend::keys::UnifiedFullViewingKey; -use crate::{ - error::SqliteClientError, - wallet::{self, PoolType}, - WalletDb, -}; - -#[cfg(feature = "transparent-inputs")] -use { - zcash_client_backend::encoding::AddressCodec, - zcash_primitives::legacy::keys::IncomingViewingKey, -}; +use crate::{error::SqliteClientError, wallet, WalletDb}; mod migrations; @@ -87,375 +75,6 @@ impl std::error::Error for WalletMigrationError { } } -struct WalletMigration0; - -migration!( - WalletMigration0, - "bc4f5e57-d600-4b6c-990f-b3538f0bfce1", - [], - "Initialize the wallet database." -); - -impl RusqliteMigration for WalletMigration0 { - type Error = WalletMigrationError; - - fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { - transaction.execute_batch( - // We set the user_version field of the database to a constant value of 8 to allow - // correct integration with the Android SDK with versions of the database that were - // created prior to the introduction of migrations in this crate. This constant should - // remain fixed going forward, and should not be altered by migrations; migration - // status is maintained exclusively by the schemer_migrations table. - "PRAGMA user_version = 8; - CREATE TABLE IF NOT EXISTS accounts ( - account INTEGER PRIMARY KEY, - extfvk TEXT NOT NULL, - address TEXT NOT NULL - ); - CREATE TABLE IF NOT EXISTS blocks ( - height INTEGER PRIMARY KEY, - hash BLOB NOT NULL, - time INTEGER NOT NULL, - sapling_tree BLOB NOT NULL - ); - CREATE TABLE IF NOT EXISTS transactions ( - id_tx INTEGER PRIMARY KEY, - txid BLOB NOT NULL UNIQUE, - created TEXT, - block INTEGER, - tx_index INTEGER, - expiry_height INTEGER, - raw BLOB, - FOREIGN KEY (block) REFERENCES blocks(height) - ); - CREATE TABLE IF NOT EXISTS received_notes ( - id_note INTEGER PRIMARY KEY, - tx INTEGER NOT NULL, - output_index INTEGER NOT NULL, - account INTEGER NOT NULL, - diversifier BLOB NOT NULL, - value INTEGER NOT NULL, - rcm BLOB NOT NULL, - nf BLOB NOT NULL UNIQUE, - is_change INTEGER NOT NULL, - memo BLOB, - spent INTEGER, - FOREIGN KEY (tx) REFERENCES transactions(id_tx), - FOREIGN KEY (account) REFERENCES accounts(account), - FOREIGN KEY (spent) REFERENCES transactions(id_tx), - CONSTRAINT tx_output UNIQUE (tx, output_index) - ); - CREATE TABLE IF NOT EXISTS sapling_witnesses ( - id_witness INTEGER PRIMARY KEY, - note INTEGER NOT NULL, - block INTEGER NOT NULL, - witness BLOB NOT NULL, - FOREIGN KEY (note) REFERENCES received_notes(id_note), - FOREIGN KEY (block) REFERENCES blocks(height), - CONSTRAINT witness_height UNIQUE (note, block) - ); - CREATE TABLE IF NOT EXISTS sent_notes ( - id_note INTEGER PRIMARY KEY, - tx INTEGER NOT NULL, - output_index INTEGER NOT NULL, - from_account INTEGER NOT NULL, - address TEXT NOT NULL, - value INTEGER NOT NULL, - memo BLOB, - FOREIGN KEY (tx) REFERENCES transactions(id_tx), - FOREIGN KEY (from_account) REFERENCES accounts(account), - CONSTRAINT tx_output UNIQUE (tx, output_index) - );", - )?; - Ok(()) - } - - fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { - // We should never down-migrate the first migration, as that can irreversibly - // destroy data. - panic!("Cannot revert the initial migration."); - } -} - -struct WalletMigration1; - -migration!( - WalletMigration1, - "a2e0ed2e-8852-475e-b0a4-f154b15b9dbe", - ["bc4f5e57-d600-4b6c-990f-b3538f0bfce1"], - "Add support for receiving transparent UTXOs." -); - -impl RusqliteMigration for WalletMigration1 { - type Error = WalletMigrationError; - - fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { - transaction.execute_batch( - "CREATE TABLE IF NOT EXISTS utxos ( - id_utxo INTEGER PRIMARY KEY, - address TEXT NOT NULL, - prevout_txid BLOB NOT NULL, - prevout_idx INTEGER NOT NULL, - script BLOB NOT NULL, - value_zat INTEGER NOT NULL, - height INTEGER NOT NULL, - spent_in_tx INTEGER, - FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx), - CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx) - );", - )?; - Ok(()) - } - - fn down(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { - transaction.execute_batch("DROP TABLE utxos;")?; - Ok(()) - } -} - -struct WalletMigration2
{
- params: P,
- seed: Option WalletMigration2 {
- fn id() -> Uuid {
- Uuid::parse_str("be57ef3b-388e-42ea-97e2-678dafcf9754").unwrap()
- }
-}
-
-impl Migration for WalletMigration2 {
- fn id(&self) -> Uuid {
- WalletMigration2:: ::id()
- }
-
- fn dependencies(&self) -> HashSet {
- type Error = WalletMigrationError;
-
- fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
- //
- // Update the accounts table to store ufvks rather than extfvks
- //
-
- transaction.execute_batch(
- "CREATE TABLE accounts_new (
- account INTEGER PRIMARY KEY,
- ufvk TEXT NOT NULL,
- address TEXT,
- transparent_address TEXT
- );",
- )?;
-
- let mut stmt_fetch_accounts =
- transaction.prepare("SELECT account, address FROM accounts")?;
-
- let mut rows = stmt_fetch_accounts.query(NO_PARAMS)?;
- while let Some(row) = rows.next()? {
- // We only need to check for the presence of the seed if we have keys that
- // need to be migrated; otherwise, it's fine to not supply the seed if this
- // migration is being used to initialize an empty database.
- if let Some(seed) = &self.seed {
- let account: u32 = row.get(0)?;
- let account = AccountId::from(account);
- let usk =
- UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), account)
- .unwrap();
- let ufvk = usk.to_unified_full_viewing_key();
-
- let address: String = row.get(1)?;
- let decoded =
- RecipientAddress::decode(&self.params, &address).ok_or_else(|| {
- WalletMigrationError::CorruptedData(format!(
- "Could not decode {} as a valid Zcash address.",
- address
- ))
- })?;
- match decoded {
- RecipientAddress::Shielded(decoded_address) => {
- let dfvk = ufvk.sapling().expect(
- "Derivation should have produced a UFVK containing a Sapling component.",
- );
- let (idx, expected_address) = dfvk.default_address();
- if decoded_address != expected_address {
- return Err(WalletMigrationError::CorruptedData(
- format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.",
- address,
- RecipientAddress::Shielded(expected_address).encode(&self.params),
- idx)));
- }
- }
- RecipientAddress::Transparent(_) => {
- return Err(WalletMigrationError::CorruptedData(
- "Address field value decoded to a transparent address; should have been Sapling or unified.".to_string()));
- }
- RecipientAddress::Unified(decoded_address) => {
- let (expected_address, idx) = ufvk.default_address();
- if decoded_address != expected_address {
- return Err(WalletMigrationError::CorruptedData(
- format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.",
- address,
- RecipientAddress::Unified(expected_address).encode(&self.params),
- idx)));
- }
- }
- }
-
- let ufvk_str: String = ufvk.encode(&self.params);
- let address_str: String = ufvk.default_address().0.encode(&self.params);
-
- // This migration, and the wallet behaviour before it, stored the default
- // transparent address in the `accounts` table. This does not necessarily
- // match the transparent receiver in the default Unified Address. Starting
- // from `AddressesTableMigration` below, we no longer store transparent
- // addresses directly, but instead extract them from the Unified Address
- // (or from the UFVK if the UA was derived without a transparent receiver,
- // which is not the case for UAs generated by this crate).
- #[cfg(feature = "transparent-inputs")]
- let taddress_str: Option {
pub(super) params: P,
@@ -24,13 +27,13 @@ pub(crate) struct Migration {
impl schemer::Migration for Migration {
fn id(&self) -> Uuid {
- migration_id()
+ MIGRATION_ID
}
fn dependencies(&self) -> HashSet ::id());
- deps
+ [ufvk_support::MIGRATION_ID, utxos_table::MIGRATION_ID]
+ .into_iter()
+ .collect()
}
fn description(&self) -> &'static str {
@@ -216,11 +219,11 @@ mod tests {
#[cfg(feature = "transparent-inputs")]
use {
- crate::wallet::init::WalletMigration2,
+ crate::wallet::init::migrations::ufvk_support,
rusqlite::params,
zcash_client_backend::{encoding::AddressCodec, keys::UnifiedSpendingKey},
zcash_primitives::{
- consensus::{BlockHeight, BranchId, Network},
+ consensus::{BlockHeight, BranchId},
legacy::{keys::IncomingViewingKey, Script},
transaction::{
components::{
@@ -235,10 +238,7 @@ mod tests {
use crate::{
tests,
- wallet::init::{
- init_wallet_db, init_wallet_db_internal,
- migrations::addresses_table::ADDRESSES_TABLE_MIGRATION,
- },
+ wallet::init::{init_wallet_db, init_wallet_db_internal, migrations::addresses_table},
WalletDb,
};
@@ -246,7 +246,7 @@ mod tests {
fn transaction_views() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
- init_wallet_db_internal(&mut db_data, None, Some(ADDRESSES_TABLE_MIGRATION)).unwrap();
+ init_wallet_db_internal(&mut db_data, None, Some(addresses_table::MIGRATION_ID)).unwrap();
db_data.conn.execute_batch(
"INSERT INTO accounts (account, ufvk) VALUES (0, '');
@@ -327,8 +327,7 @@ mod tests {
fn migrate_from_wm2() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
- init_wallet_db_internal(&mut db_data, None, Some(WalletMigration2:: {
+impl {
fn id(&self) -> Uuid {
- ADDRESSES_TABLE_MIGRATION
+ MIGRATION_ID
}
fn dependencies(&self) -> HashSet {
}
}
-impl {
+impl {
type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {
diff --git a/zcash_client_sqlite/src/wallet/init/migrations/initial_setup.rs b/zcash_client_sqlite/src/wallet/init/migrations/initial_setup.rs
new file mode 100644
index 000000000..850136856
--- /dev/null
+++ b/zcash_client_sqlite/src/wallet/init/migrations/initial_setup.rs
@@ -0,0 +1,116 @@
+//! The migration that performs the initial setup of the wallet database.
+use std::collections::HashSet;
+
+use rusqlite;
+use schemer;
+use schemer_rusqlite::RusqliteMigration;
+use uuid::Uuid;
+
+use crate::wallet::init::WalletMigrationError;
+
+/// Identifier for the migration that performs the initial setup of the wallet database.
+///
+/// bc4f5e57-d600-4b6c-990f-b3538f0bfce1,
+pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields(
+ 0xbc4f5e57,
+ 0xd600,
+ 0x4b6c,
+ b"\x99\x0f\xb3\x53\x8f\x0b\xfc\xe1",
+);
+
+pub(super) struct Migration;
+
+impl schemer::Migration for Migration {
+ fn id(&self) -> Uuid {
+ MIGRATION_ID
+ }
+
+ fn dependencies(&self) -> HashSet {
+ pub(super) params: P,
+ pub(super) seed: Option schemer::Migration for Migration {
+ fn id(&self) -> Uuid {
+ MIGRATION_ID
+ }
+
+ fn dependencies(&self) -> HashSet {
+ type Error = WalletMigrationError;
+
+ fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
+ //
+ // Update the accounts table to store ufvks rather than extfvks
+ //
+
+ transaction.execute_batch(
+ "CREATE TABLE accounts_new (
+ account INTEGER PRIMARY KEY,
+ ufvk TEXT NOT NULL,
+ address TEXT,
+ transparent_address TEXT
+ );",
+ )?;
+
+ let mut stmt_fetch_accounts =
+ transaction.prepare("SELECT account, address FROM accounts")?;
+
+ let mut rows = stmt_fetch_accounts.query(NO_PARAMS)?;
+ while let Some(row) = rows.next()? {
+ // We only need to check for the presence of the seed if we have keys that
+ // need to be migrated; otherwise, it's fine to not supply the seed if this
+ // migration is being used to initialize an empty database.
+ if let Some(seed) = &self.seed {
+ let account: u32 = row.get(0)?;
+ let account = AccountId::from(account);
+ let usk =
+ UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), account)
+ .unwrap();
+ let ufvk = usk.to_unified_full_viewing_key();
+
+ let address: String = row.get(1)?;
+ let decoded =
+ RecipientAddress::decode(&self.params, &address).ok_or_else(|| {
+ WalletMigrationError::CorruptedData(format!(
+ "Could not decode {} as a valid Zcash address.",
+ address
+ ))
+ })?;
+ match decoded {
+ RecipientAddress::Shielded(decoded_address) => {
+ let dfvk = ufvk.sapling().expect(
+ "Derivation should have produced a UFVK containing a Sapling component.",
+ );
+ let (idx, expected_address) = dfvk.default_address();
+ if decoded_address != expected_address {
+ return Err(WalletMigrationError::CorruptedData(
+ format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.",
+ address,
+ RecipientAddress::Shielded(expected_address).encode(&self.params),
+ idx)));
+ }
+ }
+ RecipientAddress::Transparent(_) => {
+ return Err(WalletMigrationError::CorruptedData(
+ "Address field value decoded to a transparent address; should have been Sapling or unified.".to_string()));
+ }
+ RecipientAddress::Unified(decoded_address) => {
+ let (expected_address, idx) = ufvk.default_address();
+ if decoded_address != expected_address {
+ return Err(WalletMigrationError::CorruptedData(
+ format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.",
+ address,
+ RecipientAddress::Unified(expected_address).encode(&self.params),
+ idx)));
+ }
+ }
+ }
+
+ let ufvk_str: String = ufvk.encode(&self.params);
+ let address_str: String = ufvk.default_address().0.encode(&self.params);
+
+ // This migration, and the wallet behaviour before it, stored the default
+ // transparent address in the `accounts` table. This does not necessarily
+ // match the transparent receiver in the default Unified Address. Starting
+ // from `AddressesTableMigration` below, we no longer store transparent
+ // addresses directly, but instead extract them from the Unified Address
+ // (or from the UFVK if the UA was derived without a transparent receiver,
+ // which is not the case for UAs generated by this crate).
+ #[cfg(feature = "transparent-inputs")]
+ let taddress_str: Option