Allow the seed to be an optional argument to database init.

This adds a new `WalletMigrationError` type so that we have
a good place to report whether or not the seed is required.
This commit is contained in:
Kris Nuttycombe 2022-08-18 10:03:22 -06:00
parent 61fb732e7b
commit 913d572087
4 changed files with 154 additions and 88 deletions

View File

@ -99,7 +99,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
@ -178,7 +178,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
@ -248,7 +248,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
@ -318,7 +318,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
@ -387,7 +387,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
@ -442,7 +442,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
@ -494,7 +494,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);

View File

@ -1242,7 +1242,7 @@ mod tests {
fn empty_database_has_no_balance() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
tests::init_test_accounts_table(&db_data);

View File

@ -1,9 +1,10 @@
//! Functions for initializing the various databases.
use rusqlite::{params, types::ToSql, Connection, Transaction, NO_PARAMS};
use rusqlite::{self, params, types::ToSql, Connection, Transaction, NO_PARAMS};
use schemer::{migration, Migration, Migrator, MigratorError};
use schemer_rusqlite::{RusqliteAdapter, RusqliteMigration};
use secrecy::{ExposeSecret, SecretVec};
use std::collections::{HashMap, HashSet};
use std::fmt;
use uuid::Uuid;
use zcash_primitives::{
@ -25,6 +26,50 @@ use {
zcash_primitives::legacy::keys::IncomingViewingKey,
};
#[derive(Debug)]
pub enum WalletMigrationError {
/// The seed is required for the migration.
SeedRequired,
/// Decoding of an existing value from its serialized form has failed.
CorruptedData(String),
/// Wrapper for rusqlite errors.
DbError(rusqlite::Error),
}
impl From<rusqlite::Error> for WalletMigrationError {
fn from(e: rusqlite::Error) -> Self {
WalletMigrationError::DbError(e)
}
}
impl fmt::Display for WalletMigrationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
WalletMigrationError::SeedRequired => {
write!(
f,
"The wallet seed is required in order to update the database."
)
}
WalletMigrationError::CorruptedData(reason) => {
write!(f, "Wallet database is corrupted: {}", reason)
}
WalletMigrationError::DbError(e) => write!(f, "{}", e),
}
}
}
impl std::error::Error for WalletMigrationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self {
WalletMigrationError::DbError(e) => Some(e),
_ => None,
}
}
}
struct WalletMigration0;
migration!(
@ -35,9 +80,9 @@ migration!(
);
impl RusqliteMigration for WalletMigration0 {
type Error = SqliteClientError;
type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), SqliteClientError> {
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {
transaction.execute_batch(
"CREATE TABLE IF NOT EXISTS accounts (
account INTEGER PRIMARY KEY,
@ -102,7 +147,7 @@ impl RusqliteMigration for WalletMigration0 {
Ok(())
}
fn down(&self, _transaction: &Transaction) -> Result<(), SqliteClientError> {
fn down(&self, _transaction: &Transaction) -> Result<(), WalletMigrationError> {
// We should never down-migrate the first migration, as that can irreversibly
// destroy data.
panic!("Cannot revert the initial migration.");
@ -119,9 +164,9 @@ migration!(
);
impl RusqliteMigration for WalletMigration1 {
type Error = SqliteClientError;
type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), SqliteClientError> {
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {
transaction.execute_batch(
"CREATE TABLE IF NOT EXISTS utxos (
id_utxo INTEGER PRIMARY KEY,
@ -139,7 +184,7 @@ impl RusqliteMigration for WalletMigration1 {
Ok(())
}
fn down(&self, transaction: &Transaction) -> Result<(), SqliteClientError> {
fn down(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {
transaction.execute_batch("DROP TABLE utxos;")?;
Ok(())
}
@ -147,7 +192,7 @@ impl RusqliteMigration for WalletMigration1 {
struct WalletMigration2<P: consensus::Parameters> {
params: P,
seed: SecretVec<u8>,
seed: Option<SecretVec<u8>>,
}
impl<P: consensus::Parameters> Migration for WalletMigration2<P> {
@ -168,9 +213,9 @@ impl<P: consensus::Parameters> Migration for WalletMigration2<P> {
}
impl<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> {
type Error = SqliteClientError;
type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), SqliteClientError> {
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {
//
// Update the accounts table to store ufvks rather than extfvks
//
@ -189,52 +234,67 @@ impl<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> {
let mut rows = stmt_fetch_accounts.query(NO_PARAMS)?;
while let Some(row) = rows.next()? {
let account: u32 = row.get(0)?;
let account = AccountId::from(account);
let usk =
UnifiedSpendingKey::from_seed(&self.params, self.seed.expose_secret(), account)
.unwrap();
let ufvk = usk.to_unified_full_viewing_key();
// 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(|| {
SqliteClientError::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(SqliteClientError::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(SqliteClientError::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(SqliteClientError::CorruptedData(
format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.",
address,
RecipientAddress::Unified(expected_address).encode(&self.params),
idx)));
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)));
}
}
}
add_account_internal::<P, WalletMigrationError>(
&self.params,
transaction,
"accounts_new",
account,
&ufvk,
)?;
} else {
return Err(WalletMigrationError::SeedRequired);
}
add_account_internal(&self.params, transaction, "accounts_new", account, &ufvk)?;
}
transaction.execute_batch(
"DROP TABLE accounts;
ALTER TABLE accounts_new RENAME TO accounts;",
@ -304,7 +364,7 @@ impl<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> {
let decoded_address =
RecipientAddress::decode(&self.params, &address).ok_or_else(|| {
SqliteClientError::CorruptedData(format!(
WalletMigrationError::CorruptedData(format!(
"Could not decode {} as a valid Zcash address.",
address
))
@ -312,7 +372,7 @@ impl<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> {
let output_pool = match decoded_address {
RecipientAddress::Shielded(_) => Ok(PoolType::Sapling.typecode()),
RecipientAddress::Transparent(_) => Ok(PoolType::Transparent.typecode()),
RecipientAddress::Unified(_) => Err(SqliteClientError::CorruptedData(
RecipientAddress::Unified(_) => Err(WalletMigrationError::CorruptedData(
"Unified addresses should not yet appear in the sent_notes table."
.to_string(),
)),
@ -339,7 +399,7 @@ impl<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> {
Ok(())
}
fn down(&self, _transaction: &Transaction) -> Result<(), SqliteClientError> {
fn down(&self, _transaction: &Transaction) -> Result<(), WalletMigrationError> {
// TODO: something better than just panic?
panic!("Cannot revert this migration.");
}
@ -372,7 +432,7 @@ impl<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> {
///
/// let data_file = NamedTempFile::new().unwrap();
/// let mut db = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap();
/// init_wallet_db(&mut db, Secret::new(vec![])).unwrap();
/// init_wallet_db(&mut db, Some(Secret::new(vec![]))).unwrap();
/// ```
// TODO: It would be possible to make the transition from providing transparent support to no
// longer providing transparent support safe, by including a migration that verifies that no
@ -382,11 +442,11 @@ impl<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> {
// library *not* compiled with the `transparent-inputs` feature flag, and fail if any are present.
pub fn init_wallet_db<P: consensus::Parameters + 'static>(
wdb: &mut WalletDb<P>,
seed: SecretVec<u8>,
) -> Result<(), MigratorError<SqliteClientError>> {
seed: Option<SecretVec<u8>>,
) -> Result<(), MigratorError<WalletMigrationError>> {
wdb.conn
.execute("PRAGMA foreign_keys = OFF", NO_PARAMS)
.map_err(|e| MigratorError::Adapter(SqliteClientError::from(e)))?;
.map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
let adapter = RusqliteAdapter::new(&mut wdb.conn, Some("schemer_migrations".to_string()));
adapter.init().expect("Migrations table setup succeeds.");
@ -404,7 +464,7 @@ pub fn init_wallet_db<P: consensus::Parameters + 'static>(
migrator.up(None)?;
wdb.conn
.execute("PRAGMA foreign_keys = ON", NO_PARAMS)
.map_err(|e| MigratorError::Adapter(SqliteClientError::from(e)))?;
.map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
Ok(())
}
@ -443,7 +503,7 @@ pub fn init_wallet_db<P: consensus::Parameters + 'static>(
///
/// let data_file = NamedTempFile::new().unwrap();
/// let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap();
/// init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
/// init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
///
/// let seed = [0u8; 32]; // insecure; replace with a strong random seed
/// let account = AccountId::from(0);
@ -470,20 +530,26 @@ pub fn init_accounts_table<P: consensus::Parameters>(
// Insert accounts atomically
wdb.conn.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
for (account, key) in keys.iter() {
add_account_internal(&wdb.params, &wdb.conn, "accounts", *account, key)?;
add_account_internal::<P, SqliteClientError>(
&wdb.params,
&wdb.conn,
"accounts",
*account,
key,
)?;
}
wdb.conn.execute("COMMIT", NO_PARAMS)?;
Ok(())
}
fn add_account_internal<P: consensus::Parameters>(
fn add_account_internal<P: consensus::Parameters, E: From<rusqlite::Error>>(
network: &P,
conn: &Connection,
accounts_table: &'static str,
account: AccountId,
key: &UnifiedFullViewingKey,
) -> Result<(), SqliteClientError> {
) -> Result<(), E> {
let ufvk_str: String = key.encode(network);
let address_str: String = key.default_address().0.encode(network);
#[cfg(feature = "transparent-inputs")]
@ -579,9 +645,6 @@ mod tests {
keys::{sapling, UnifiedFullViewingKey, UnifiedSpendingKey},
};
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::legacy::keys as transparent;
use zcash_primitives::{
block::BlockHash,
consensus::{BlockHeight, Parameters},
@ -597,6 +660,9 @@ mod tests {
use super::{init_accounts_table, init_blocks_table, init_wallet_db};
#[cfg(feature = "transparent-inputs")]
use {crate::wallet::PoolType, zcash_primitives::legacy::keys as transparent};
#[test]
fn init_migrate_from_0_3_0() {
fn init_0_3_0<P>(
@ -710,7 +776,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_0_3_0(&mut db_data, &extfvk, account).unwrap();
init_wallet_db(&mut db_data, Secret::new(seed.to_vec())).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))).unwrap();
}
#[test]
@ -857,7 +923,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_autoshielding(&db_data, &extfvk, account).unwrap();
init_wallet_db(&mut db_data, Secret::new(seed.to_vec())).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))).unwrap();
}
#[test]
@ -993,7 +1059,7 @@ mod tests {
wdb.conn.execute(
"INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value)
VALUES (0, ?, 0, ?, ?, 0)",
&[PoolType::Transparent.typecode(), u32::from(account).to_sql()?, taddr.to_sql()?])?;
&[PoolType::Transparent.typecode().to_sql()?, u32::from(account).to_sql()?, taddr.to_sql()?])?;
}
Ok(())
@ -1005,14 +1071,14 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_main(&db_data, &secret_key.to_unified_full_viewing_key(), account).unwrap();
init_wallet_db(&mut db_data, Secret::new(seed.to_vec())).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))).unwrap();
}
#[test]
fn init_accounts_table_only_works_once() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// We can call the function as many times as we want with no data
init_accounts_table(&db_data, &HashMap::new()).unwrap();
@ -1052,7 +1118,7 @@ mod tests {
fn init_blocks_table_only_works_once() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// First call with data should initialise the blocks table
init_blocks_table(
@ -1079,7 +1145,7 @@ mod tests {
fn init_accounts_table_stores_correct_address() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
let seed = [0u8; 32];

View File

@ -207,7 +207,7 @@ mod tests {
fn create_to_address_fails_on_incorrect_extsk() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
let acct0 = AccountId::from(0);
let acct1 = AccountId::from(1);
@ -291,7 +291,7 @@ mod tests {
fn create_to_address_fails_with_no_blocks() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
@ -329,7 +329,7 @@ mod tests {
fn create_to_address_fails_on_insufficient_balance() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
init_blocks_table(
&db_data,
BlockHeight::from(1u32),
@ -387,7 +387,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
@ -522,7 +522,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
@ -653,7 +653,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
@ -765,7 +765,7 @@ mod tests {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Secret::new(vec![])).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);