Move wallet migrations into individual modules.
This commit is contained in:
parent
4e631697c4
commit
ccf9e00b00
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue