Merge pull request #656 from nuttycom/sqlite/migration_organization

Move wallet migrations into individual modules.
This commit is contained in:
Kris Nuttycombe 2022-10-03 08:10:08 -06:00 committed by GitHub
commit 81e0d482d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 485 additions and 430 deletions

View File

@ -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<P> {
params: P,
seed: Option<SecretVec<u8>>,
}
impl<P> WalletMigration2<P> {
fn id() -> Uuid {
Uuid::parse_str("be57ef3b-388e-42ea-97e2-678dafcf9754").unwrap()
}
}
impl<P> Migration for WalletMigration2<P> {
fn id(&self) -> Uuid {
WalletMigration2::<P>::id()
}
fn dependencies(&self) -> HashSet<Uuid> {
["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<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> {
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<String> = 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<String> = None;
transaction.execute_named(
"INSERT INTO accounts_new (account, ufvk, address, transparent_address)
VALUES (:account, :ufvk, :address, :transparent_address)",
&[
(":account", &<u32>::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<Vec<u8>> = 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

View File

@ -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<P: consensus::Parameters + 'static>(
params: &P,
seed: Option<SecretVec<u8>>,
) -> Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>> {
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 {

View File

@ -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<P> {
pub(super) params: P,
@ -24,13 +27,13 @@ pub(crate) struct Migration<P> {
impl<P> schemer::Migration for Migration<P> {
fn id(&self) -> Uuid {
migration_id()
MIGRATION_ID
}
fn dependencies(&self) -> HashSet<Uuid> {
let mut deps = HashSet::new();
deps.insert(WalletMigration2::<P>::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::<Network>::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(

View File

@ -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<P: consensus::Parameters> {
pub(crate) struct Migration<P: consensus::Parameters> {
pub(crate) params: P,
}
impl<P: consensus::Parameters> Migration for AddressesTableMigration<P> {
impl<P: consensus::Parameters> schemer::Migration for Migration<P> {
fn id(&self) -> Uuid {
ADDRESSES_TABLE_MIGRATION
MIGRATION_ID
}
fn dependencies(&self) -> HashSet<Uuid> {
["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<P: consensus::Parameters> Migration for AddressesTableMigration<P> {
}
}
impl<P: consensus::Parameters> RusqliteMigration for AddressesTableMigration<P> {
impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {

View File

@ -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<Uuid> {
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.");
}
}

View File

@ -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<P> {
pub(super) params: P,
pub(super) seed: Option<SecretVec<u8>>,
}
impl<P> schemer::Migration for Migration<P> {
fn id(&self) -> Uuid {
MIGRATION_ID
}
fn dependencies(&self) -> HashSet<Uuid> {
[utxos_table::MIGRATION_ID].into_iter().collect()
}
fn description(&self) -> &'static str {
"Add support for unified full viewing keys"
}
}
impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
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<String> = 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<String> = None;
transaction.execute_named(
"INSERT INTO accounts_new (account, ufvk, address, transparent_address)
VALUES (:account, :ufvk, :address, :transparent_address)",
&[
(":account", &<u32>::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<Vec<u8>> = 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.");
}
}

View File

@ -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<Uuid> {
[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(())
}
}