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>, -} - -impl

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 { - ["a2e0ed2e-8852-475e-b0a4-f154b15b9dbe"] - .iter() - .map(|uuidstr| ::uuid::Uuid::parse_str(uuidstr).unwrap()) - .collect() - } - - fn description(&self) -> &'static str { - "Add support for unified full viewing keys" - } -} - -impl RusqliteMigration for WalletMigration2

{ - 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 = ufvk.transparent().and_then(|k| { - k.derive_external_ivk() - .ok() - .map(|k| k.default_address().0.encode(&self.params)) - }); - #[cfg(not(feature = "transparent-inputs"))] - let taddress_str: Option = None; - - transaction.execute_named( - "INSERT INTO accounts_new (account, ufvk, address, transparent_address) - VALUES (:account, :ufvk, :address, :transparent_address)", - &[ - (":account", &::from(account)), - (":ufvk", &ufvk_str), - (":address", &address_str), - (":transparent_address", &taddress_str), - ], - )?; - } else { - return Err(WalletMigrationError::SeedRequired); - } - } - - transaction.execute_batch( - "DROP TABLE accounts; - ALTER TABLE accounts_new RENAME TO accounts;", - )?; - - // - // Update the sent_notes table to inclue an output_pool column that - // is respected by the uniqueness constraint - // - - transaction.execute_batch( - "CREATE TABLE sent_notes_new ( - id_note INTEGER PRIMARY KEY, - tx INTEGER NOT NULL, - output_pool 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_pool, output_index) - );", - )?; - - // we query in a nested scope so that the col_names iterator is correctly - // dropped and doesn't maintain a lock on the table. - let has_output_pool = { - let mut stmt_fetch_columns = transaction.prepare("PRAGMA TABLE_INFO('sent_notes')")?; - let mut col_names = stmt_fetch_columns.query_map(NO_PARAMS, |row| { - let col_name: String = row.get(1)?; - Ok(col_name) - })?; - - col_names.any(|cname| cname == Ok("output_pool".to_string())) - }; - - if has_output_pool { - transaction.execute_batch( - "INSERT INTO sent_notes_new - (id_note, tx, output_pool, output_index, from_account, address, value, memo) - SELECT id_note, tx, output_pool, output_index, from_account, address, value, memo - FROM sent_notes;" - )?; - } else { - let mut stmt_fetch_sent_notes = transaction.prepare( - "SELECT id_note, tx, output_index, from_account, address, value, memo - FROM sent_notes", - )?; - - let mut stmt_insert_sent_note = transaction.prepare( - "INSERT INTO sent_notes_new - (id_note, tx, output_pool, output_index, from_account, address, value, memo) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - )?; - - let mut rows = stmt_fetch_sent_notes.query(NO_PARAMS)?; - while let Some(row) = rows.next()? { - let id_note: i64 = row.get(0)?; - let tx_ref: i64 = row.get(1)?; - let output_index: i64 = row.get(2)?; - let account_id: u32 = row.get(3)?; - let address: String = row.get(4)?; - let value: i64 = row.get(5)?; - let memo: Option> = row.get(6)?; - - let decoded_address = - RecipientAddress::decode(&self.params, &address).ok_or_else(|| { - WalletMigrationError::CorruptedData(format!( - "Could not decode {} as a valid Zcash address.", - address - )) - })?; - let output_pool = match decoded_address { - RecipientAddress::Shielded(_) => Ok(PoolType::Sapling.typecode()), - RecipientAddress::Transparent(_) => Ok(PoolType::Transparent.typecode()), - RecipientAddress::Unified(_) => Err(WalletMigrationError::CorruptedData( - "Unified addresses should not yet appear in the sent_notes table." - .to_string(), - )), - }?; - - stmt_insert_sent_note.execute(params![ - id_note, - tx_ref, - output_pool, - output_index, - account_id, - address, - value, - memo - ])?; - } - } - - transaction.execute_batch( - "DROP TABLE sent_notes; - ALTER TABLE sent_notes_new RENAME TO sent_notes;", - )?; - - Ok(()) - } - - fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { - // TODO: something better than just panic? - panic!("Cannot revert this migration."); - } -} - /// Sets up the internal structure of the data database. /// /// This procedure will automatically perform migration operations to update the wallet database to diff --git a/zcash_client_sqlite/src/wallet/init/migrations.rs b/zcash_client_sqlite/src/wallet/init/migrations.rs index ab56dc203..5ff0f5e16 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations.rs @@ -1,26 +1,27 @@ -mod addresses_table; -pub(super) use addresses_table::AddressesTableMigration; - mod add_transaction_views; +mod addresses_table; +mod initial_setup; +mod ufvk_support; +mod utxos_table; use schemer_rusqlite::RusqliteMigration; use secrecy::SecretVec; use zcash_primitives::consensus; -use super::{WalletMigration0, WalletMigration1, WalletMigration2, WalletMigrationError}; +use super::WalletMigrationError; pub(super) fn all_migrations( params: &P, seed: Option>, ) -> Vec>> { vec![ - Box::new(WalletMigration0 {}), - Box::new(WalletMigration1 {}), - Box::new(WalletMigration2 { + Box::new(initial_setup::Migration {}), + Box::new(utxos_table::Migration {}), + Box::new(ufvk_support::Migration { params: params.clone(), seed, }), - Box::new(AddressesTableMigration { + Box::new(addresses_table::Migration { params: params.clone(), }), Box::new(add_transaction_views::Migration { diff --git a/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs index 3cefb8645..e443bab60 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/add_transaction_views.rs @@ -1,10 +1,9 @@ -//! Functions for initializing the various databases. +//! Migration that adds transaction summary views & add fee information to transactions. +use std::collections::HashSet; + use rusqlite::{self, types::ToSql, OptionalExtension, NO_PARAMS}; use schemer::{self}; use schemer_rusqlite::RusqliteMigration; - -use std::collections::HashSet; - use uuid::Uuid; use zcash_primitives::{ @@ -12,11 +11,15 @@ use zcash_primitives::{ transaction::{components::amount::Amount, Transaction}, }; -use super::super::{WalletMigration2, WalletMigrationError}; +use super::{ufvk_support, utxos_table}; +use crate::wallet::init::WalletMigrationError; -pub(crate) fn migration_id() -> Uuid { - Uuid::parse_str("282fad2e-8372-4ca0-8bed-71821320909f").unwrap() -} +pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( + 0x282fad2e, + 0x8372, + 0x4ca0, + b"\x8b\xed\x71\x82\x13\x20\x90\x9f", +); pub(crate) struct Migration

{ 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 { - let mut deps = HashSet::new(); - deps.insert(WalletMigration2::

::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::::id())) - .unwrap(); + init_wallet_db_internal(&mut db_data, None, Some(ufvk_support::MIGRATION_ID)).unwrap(); // create a UTXO to spend let tx = TransactionData::from_parts( diff --git a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs index 84417af2c..bae550091 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs @@ -1,43 +1,41 @@ use std::collections::HashSet; use rusqlite::{Transaction, NO_PARAMS}; -use schemer::Migration; +use schemer; use schemer_rusqlite::RusqliteMigration; use uuid::Uuid; use zcash_client_backend::{address::RecipientAddress, keys::UnifiedFullViewingKey}; use zcash_primitives::{consensus, zip32::AccountId}; -use super::super::WalletMigrationError; -use crate::wallet::add_account_internal; +use crate::wallet::{add_account_internal, init::WalletMigrationError}; #[cfg(feature = "transparent-inputs")] use zcash_primitives::legacy::keys::IncomingViewingKey; +use super::ufvk_support; + /// The migration that removed the address columns from the `accounts` table, and created /// the `accounts` table. /// /// d956978c-9c87-4d6e-815d-fb8f088d094c -pub(super) const ADDRESSES_TABLE_MIGRATION: Uuid = Uuid::from_fields( +pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( 0xd956978c, 0x9c87, 0x4d6e, b"\x81\x5d\xfb\x8f\x08\x8d\x09\x4c", ); -pub(crate) struct AddressesTableMigration { +pub(crate) struct Migration { pub(crate) params: P, } -impl Migration for AddressesTableMigration

{ +impl schemer::Migration for Migration

{ fn id(&self) -> Uuid { - ADDRESSES_TABLE_MIGRATION + MIGRATION_ID } fn dependencies(&self) -> HashSet { - ["be57ef3b-388e-42ea-97e2-678dafcf9754"] - .iter() - .map(|uuidstr| ::uuid::Uuid::parse_str(uuidstr).unwrap()) - .collect() + [ufvk_support::MIGRATION_ID].into_iter().collect() } fn description(&self) -> &'static str { @@ -45,7 +43,7 @@ impl Migration for AddressesTableMigration

{ } } -impl RusqliteMigration for AddressesTableMigration

{ +impl RusqliteMigration for Migration

{ 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 { + HashSet::new() + } + + fn description(&self) -> &'static str { + "Initialize the wallet database." + } +} + +impl RusqliteMigration for Migration { + 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."); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs new file mode 100644 index 000000000..e5ee31c4e --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs @@ -0,0 +1,263 @@ +//! Migration that adds support for unified full viewing keys. +use std::collections::HashSet; + +use rusqlite::{self, params, NO_PARAMS}; +use schemer; +use schemer_rusqlite::RusqliteMigration; +use secrecy::{ExposeSecret, SecretVec}; +use uuid::Uuid; + +use zcash_client_backend::{address::RecipientAddress, keys::UnifiedSpendingKey}; +use zcash_primitives::{consensus, zip32::AccountId}; + +#[cfg(feature = "transparent-inputs")] +use zcash_primitives::legacy::keys::IncomingViewingKey; + +#[cfg(feature = "transparent-inputs")] +use zcash_client_backend::encoding::AddressCodec; + +use crate::wallet::{ + init::{migrations::utxos_table, WalletMigrationError}, + PoolType, +}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( + 0xbe57ef3b, + 0x388e, + 0x42ea, + b"\x97\xe2\x67\x8d\xaf\xcf\x97\x54", +); + +pub(super) struct Migration

{ + pub(super) params: P, + pub(super) seed: Option>, +} + +impl

schemer::Migration for Migration

{ + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [utxos_table::MIGRATION_ID].into_iter().collect() + } + + fn description(&self) -> &'static str { + "Add support for unified full viewing keys" + } +} + +impl RusqliteMigration for Migration

{ + 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 = ufvk.transparent().and_then(|k| { + k.derive_external_ivk() + .ok() + .map(|k| k.default_address().0.encode(&self.params)) + }); + #[cfg(not(feature = "transparent-inputs"))] + let taddress_str: Option = None; + + transaction.execute_named( + "INSERT INTO accounts_new (account, ufvk, address, transparent_address) + VALUES (:account, :ufvk, :address, :transparent_address)", + &[ + (":account", &::from(account)), + (":ufvk", &ufvk_str), + (":address", &address_str), + (":transparent_address", &taddress_str), + ], + )?; + } else { + return Err(WalletMigrationError::SeedRequired); + } + } + + transaction.execute_batch( + "DROP TABLE accounts; + ALTER TABLE accounts_new RENAME TO accounts;", + )?; + + // + // Update the sent_notes table to inclue an output_pool column that + // is respected by the uniqueness constraint + // + + transaction.execute_batch( + "CREATE TABLE sent_notes_new ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_pool 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_pool, output_index) + );", + )?; + + // we query in a nested scope so that the col_names iterator is correctly + // dropped and doesn't maintain a lock on the table. + let has_output_pool = { + let mut stmt_fetch_columns = transaction.prepare("PRAGMA TABLE_INFO('sent_notes')")?; + let mut col_names = stmt_fetch_columns.query_map(NO_PARAMS, |row| { + let col_name: String = row.get(1)?; + Ok(col_name) + })?; + + col_names.any(|cname| cname == Ok("output_pool".to_string())) + }; + + if has_output_pool { + transaction.execute_batch( + "INSERT INTO sent_notes_new + (id_note, tx, output_pool, output_index, from_account, address, value, memo) + SELECT id_note, tx, output_pool, output_index, from_account, address, value, memo + FROM sent_notes;" + )?; + } else { + let mut stmt_fetch_sent_notes = transaction.prepare( + "SELECT id_note, tx, output_index, from_account, address, value, memo + FROM sent_notes", + )?; + + let mut stmt_insert_sent_note = transaction.prepare( + "INSERT INTO sent_notes_new + (id_note, tx, output_pool, output_index, from_account, address, value, memo) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + )?; + + let mut rows = stmt_fetch_sent_notes.query(NO_PARAMS)?; + while let Some(row) = rows.next()? { + let id_note: i64 = row.get(0)?; + let tx_ref: i64 = row.get(1)?; + let output_index: i64 = row.get(2)?; + let account_id: u32 = row.get(3)?; + let address: String = row.get(4)?; + let value: i64 = row.get(5)?; + let memo: Option> = row.get(6)?; + + let decoded_address = + RecipientAddress::decode(&self.params, &address).ok_or_else(|| { + WalletMigrationError::CorruptedData(format!( + "Could not decode {} as a valid Zcash address.", + address + )) + })?; + let output_pool = match decoded_address { + RecipientAddress::Shielded(_) => Ok(PoolType::Sapling.typecode()), + RecipientAddress::Transparent(_) => Ok(PoolType::Transparent.typecode()), + RecipientAddress::Unified(_) => Err(WalletMigrationError::CorruptedData( + "Unified addresses should not yet appear in the sent_notes table." + .to_string(), + )), + }?; + + stmt_insert_sent_note.execute(params![ + id_note, + tx_ref, + output_pool, + output_index, + account_id, + address, + value, + memo + ])?; + } + } + + transaction.execute_batch( + "DROP TABLE sent_notes; + ALTER TABLE sent_notes_new RENAME TO sent_notes;", + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + // TODO: something better than just panic? + panic!("Cannot revert this migration."); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/utxos_table.rs b/zcash_client_sqlite/src/wallet/init/migrations/utxos_table.rs new file mode 100644 index 000000000..718039429 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/utxos_table.rs @@ -0,0 +1,59 @@ +//! The migration that adds initial support for transparent UTXOs to the wallet. +use std::collections::HashSet; + +use rusqlite; +use schemer; +use schemer_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use crate::wallet::init::{migrations::initial_setup, WalletMigrationError}; + +pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( + 0xa2e0ed2e, + 0x8852, + 0x475e, + b"\xb0\xa4\xf1\x54\xb1\x5b\x9d\xbe", +); + +pub(super) struct Migration; + +impl schemer::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [initial_setup::MIGRATION_ID].into_iter().collect() + } + + fn description(&self) -> &'static str { + "Add support for receiving transparent UTXOs." + } +} + +impl RusqliteMigration for Migration { + 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(()) + } +}