librustzcash/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs

301 lines
13 KiB
Rust

//! Migration that adds support for unified full viewing keys.
use std::{collections::HashSet, rc::Rc};
use rusqlite::{self, named_params, params};
use schemer;
use schemer_rusqlite::RusqliteMigration;
use secrecy::{ExposeSecret, SecretVec};
use uuid::Uuid;
use zcash_client_backend::{
address::Address, keys::UnifiedSpendingKey, PoolType, ShieldedProtocol,
};
use zcash_keys::keys::UnifiedAddressRequest;
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::initial_setup, WalletMigrationError},
pool_code,
},
UA_TRANSPARENT,
};
pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xbe57ef3b_388e_42ea_97e2_678dafcf9754);
pub(super) struct Migration<P> {
pub(super) params: P,
pub(super) seed: Option<Rc<SecretVec<u8>>>,
}
impl<P> schemer::Migration for Migration<P> {
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 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")?;
// We track whether we have determined seed relevance or not, in order to
// correctly report errors when checking the seed against an account:
//
// - If we encounter an error with the first account, we can assert that the seed
// is not relevant to the wallet by assuming that:
// - All accounts are from the same seed (which is historically the only use
// case that this migration supported), and
// - All accounts in the wallet must have been able to derive their USKs (in
// order to derive UIVKs).
//
// - Once the seed has been determined to be relevant (because it matched the
// first account), any subsequent account derivation failure is proving wrong
// our second assumption above, and we report this as corrupted data.
let mut seed_is_relevant = false;
let ua_request =
UnifiedAddressRequest::unsafe_new_without_expiry(false, true, UA_TRANSPARENT);
let mut rows = stmt_fetch_accounts.query([])?;
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::try_from(account).map_err(|_| {
WalletMigrationError::CorruptedData("Account ID is invalid".to_owned())
})?;
let usk =
UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), account)
.map_err(|_| {
if seed_is_relevant {
WalletMigrationError::CorruptedData(
"Unable to derive spending key from seed.".to_string(),
)
} else {
WalletMigrationError::SeedNotRelevant
}
})?;
let ufvk = usk.to_unified_full_viewing_key();
let address: String = row.get(1)?;
let decoded = Address::decode(&self.params, &address).ok_or_else(|| {
WalletMigrationError::CorruptedData(format!(
"Could not decode {} as a valid Zcash address.",
address
))
})?;
match decoded {
Address::Sapling(decoded_address) => {
let dfvk = ufvk.sapling().ok_or_else(||
WalletMigrationError::CorruptedData("Derivation should have produced a UFVK containing a Sapling component.".to_owned()))?;
let (idx, expected_address) = dfvk.default_address();
if *decoded_address != expected_address {
return Err(if seed_is_relevant {
WalletMigrationError::CorruptedData(
format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.",
address,
Address::from(expected_address).encode(&self.params),
idx))
} else {
WalletMigrationError::SeedNotRelevant
});
}
}
Address::Transparent(_) => {
return Err(WalletMigrationError::CorruptedData(
"Address field value decoded to a transparent address; should have been Sapling or unified.".to_string()));
}
Address::Unified(decoded_address) => {
let (expected_address, idx) = ufvk.default_address(ua_request)?;
if *decoded_address != expected_address {
return Err(if seed_is_relevant {
WalletMigrationError::CorruptedData(
format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.",
address,
Address::from(expected_address).encode(&self.params),
idx))
} else {
WalletMigrationError::SeedNotRelevant
});
}
}
}
// We made it past one derived account, so the seed must be relevant.
seed_is_relevant = true;
let ufvk_str: String = ufvk.encode(&self.params);
let address_str: String = ufvk.default_address(ua_request)?.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(
"INSERT INTO accounts_new (account, ufvk, address, transparent_address)
VALUES (:account, :ufvk, :address, :transparent_address)",
named_params![
":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 include 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([], |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([])?;
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 = Address::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 {
Address::Sapling(_) => {
Ok(pool_code(PoolType::Shielded(ShieldedProtocol::Sapling)))
}
Address::Transparent(_) => Ok(pool_code(PoolType::Transparent)),
Address::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> {
Err(WalletMigrationError::CannotRevert(MIGRATION_ID))
}
}