Merge pull request #600 from nuttycom/wallet/migrations

Add database migrations to zcash_client_sqlite.
This commit is contained in:
str4d 2022-08-18 23:31:16 +01:00 committed by GitHub
commit 59d361f494
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1039 additions and 216 deletions

View File

@ -21,3 +21,5 @@ codegen-units = 1
[patch.crates-io]
zcash_encoding = { path = "components/zcash_encoding" }
zcash_note_encryption = { path = "components/zcash_note_encryption" }
schemer = { git = "https://github.com/aschampion/schemer.git", rev = "2370c96f43eda5b2cb267bbc9355f0fefff850bb" }
schemer-rusqlite = { git = "https://github.com/aschampion/schemer.git", rev = "2370c96f43eda5b2cb267bbc9355f0fefff850bb" }

View File

@ -37,6 +37,7 @@ and this library adheres to Rust's notion of
- `TransactionRequest::new` for constructing a request from `Vec<Payment>`.
- `TransactionRequest::payments` for accessing the `Payments` that make up a
request.
- `zcash_client_backend::encoding::KeyError`
- New experimental APIs that should be considered unstable, and are
likely to be modified and/or moved to a different module in a future
release:
@ -113,7 +114,11 @@ and this library adheres to Rust's notion of
derived from ZIP 316 UFVKs and UIVKs.
- `welding_rig::scan_block` now uses batching for trial-decryption of
transaction outputs.
- The return type of the following methods in `zcash_client_backend::encoding`
have been changed to improve error reporting:
- `decode_extended_spending_key`
- `decode_extended_full_viewing_key`
- `decode_payment_address`
### Removed
- `zcash_client_backend::data_api`:

View File

@ -9,16 +9,12 @@ use zcash_primitives::{
fn parse_viewing_key(s: &str) -> Result<(ExtendedFullViewingKey, bool), &'static str> {
decode_extended_full_viewing_key(mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, s)
.ok()
.flatten()
.map(|vk| (vk, true))
.or_else(|| {
.or_else(|_| {
decode_extended_full_viewing_key(testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, s)
.ok()
.flatten()
.map(|vk| (vk, false))
})
.ok_or("Invalid Sapling viewing key")
.map_err(|_| "Invalid Sapling viewing key")
}
fn parse_diversifier_index(s: &str) -> Result<DiversifierIndex, &'static str> {

View File

@ -26,15 +26,55 @@ where
bech32::encode(hrp, data.to_base32(), Variant::Bech32).expect("hrp is invalid")
}
fn bech32_decode<T, F>(hrp: &str, s: &str, read: F) -> Result<Option<T>, Error>
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Bech32DecodeError {
Bech32Error(Error),
IncorrectVariant(Variant),
ReadError,
HrpMismatch { expected: String, actual: String },
}
impl From<Error> for Bech32DecodeError {
fn from(err: Error) -> Self {
Bech32DecodeError::Bech32Error(err)
}
}
impl fmt::Display for Bech32DecodeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
Bech32DecodeError::Bech32Error(e) => write!(f, "{}", e),
Bech32DecodeError::IncorrectVariant(variant) => write!(
f,
"Incorrect bech32 encoding (wrong variant: {:?})",
variant
),
Bech32DecodeError::ReadError => {
write!(f, "Failed to decode key from its binary representation.")
}
Bech32DecodeError::HrpMismatch { expected, actual } => write!(
f,
"Key was encoded for a different network: expected {}, got {}.",
expected, actual
),
}
}
}
fn bech32_decode<T, F>(hrp: &str, s: &str, read: F) -> Result<T, Bech32DecodeError>
where
F: Fn(Vec<u8>) -> Option<T>,
{
match bech32::decode(s)? {
(decoded_hrp, data, Variant::Bech32) if decoded_hrp == hrp => {
Vec::<u8>::from_base32(&data).map(read)
}
_ => Ok(None),
let (decoded_hrp, data, variant) = bech32::decode(s)?;
if variant != Variant::Bech32 {
Err(Bech32DecodeError::IncorrectVariant(variant))
} else if decoded_hrp != hrp {
Err(Bech32DecodeError::HrpMismatch {
expected: hrp.to_string(),
actual: decoded_hrp,
})
} else {
read(Vec::<u8>::from_base32(&data)?).ok_or(Bech32DecodeError::ReadError)
}
}
@ -121,7 +161,7 @@ pub fn encode_extended_spending_key(hrp: &str, extsk: &ExtendedSpendingKey) -> S
pub fn decode_extended_spending_key(
hrp: &str,
s: &str,
) -> Result<Option<ExtendedSpendingKey>, Error> {
) -> Result<ExtendedSpendingKey, Bech32DecodeError> {
bech32_decode(hrp, s, |data| ExtendedSpendingKey::read(&data[..]).ok())
}
@ -155,7 +195,7 @@ pub fn encode_extended_full_viewing_key(hrp: &str, extfvk: &ExtendedFullViewingK
pub fn decode_extended_full_viewing_key(
hrp: &str,
s: &str,
) -> Result<Option<ExtendedFullViewingKey>, Error> {
) -> Result<ExtendedFullViewingKey, Bech32DecodeError> {
bech32_decode(hrp, s, |data| ExtendedFullViewingKey::read(&data[..]).ok())
}
@ -240,11 +280,11 @@ pub fn encode_payment_address_p<P: consensus::Parameters>(
/// HRP_SAPLING_PAYMENT_ADDRESS,
/// "ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk",
/// ),
/// Ok(Some(pa)),
/// Ok(pa),
/// );
/// ```
/// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress
pub fn decode_payment_address(hrp: &str, s: &str) -> Result<Option<PaymentAddress>, Error> {
pub fn decode_payment_address(hrp: &str, s: &str) -> Result<PaymentAddress, Bech32DecodeError> {
bech32_decode(hrp, s, |data| {
if data.len() != 43 {
return None;
@ -392,6 +432,7 @@ mod tests {
use super::{
decode_extended_full_viewing_key, decode_extended_spending_key, decode_payment_address,
encode_extended_full_viewing_key, encode_extended_spending_key, encode_payment_address,
Bech32DecodeError,
};
#[test]
@ -414,7 +455,7 @@ mod tests {
encoded_main
)
.unwrap(),
Some(extsk.clone())
extsk
);
assert_eq!(
@ -430,7 +471,7 @@ mod tests {
encoded_test
)
.unwrap(),
Some(extsk)
extsk
);
}
@ -454,7 +495,7 @@ mod tests {
encoded_main
)
.unwrap(),
Some(extfvk.clone())
extfvk
);
assert_eq!(
@ -470,7 +511,7 @@ mod tests {
encoded_test
)
.unwrap(),
Some(extfvk)
extfvk
);
}
@ -500,7 +541,7 @@ mod tests {
encoded_main
)
.unwrap(),
Some(addr.clone())
addr
);
assert_eq!(
@ -513,7 +554,7 @@ mod tests {
encoded_test
)
.unwrap(),
Some(addr)
addr
);
}
@ -535,9 +576,8 @@ mod tests {
decode_payment_address(
constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS,
&encoded_main
)
.unwrap(),
None
),
Err(Bech32DecodeError::ReadError)
);
}
}

View File

@ -793,7 +793,7 @@ mod tests {
let expected = TransactionRequest {
payments: vec![
Payment {
recipient_address: RecipientAddress::Shielded(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap().unwrap()),
recipient_address: RecipientAddress::Shielded(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: Amount::from_u64(376876902796286).unwrap(),
memo: None,
label: None,
@ -811,7 +811,7 @@ mod tests {
let req = TransactionRequest {
payments: vec![
Payment {
recipient_address: RecipientAddress::Shielded(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap().unwrap()),
recipient_address: RecipientAddress::Shielded(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: Amount::from_u64(0).unwrap(),
memo: None,
label: None,

View File

@ -19,7 +19,10 @@ and this library adheres to Rust's notion of
### Changed
- Various **BREAKING CHANGES** have been made to the database tables. These will
require migrations, which may need to be performed in multiple steps.
require migrations, which may need to be performed in multiple steps. Migrations
will now be automatically performed for any user using
`zcash_client_sqlite::wallet::init_wallet_db` and it is recommended to use this
method to maintain the state of the database going forward.
- The `extfvk` column in the `accounts` table has been replaced by a `ufvk`
column. Values for this column should be derived from the wallet's seed and
the account number; the Sapling component of the resulting Unified Full
@ -48,6 +51,12 @@ and this library adheres to Rust's notion of
method to be used in contexts where a transaction has just been
constructed, rather than only in the case that a transaction has
been decrypted after being retrieved from the network.
- `zcash_client_sqlite::wallet::init_wallet_db` has been modified to
take the wallet seed as an argument so that it can correctly perform
migrations that require re-deriving key material. In particular for
this upgrade, the seed is used to derive UFVKs to replace the currently
stored Sapling extfvks (without loss of information) as part of the
migration process.
### Removed
- `zcash_client_sqlite::wallet`:

View File

@ -22,7 +22,11 @@ protobuf = "~2.27.1" # MSRV 1.52.1
rand_core = "0.6"
rusqlite = { version = "0.24", features = ["bundled", "time"] }
secp256k1 = { version = "0.21" }
schemer = "0.1.2"
schemer-rusqlite = "0.1.1"
secrecy = "0.8"
time = "0.2"
uuid = "1.1"
zcash_client_backend = { version = "0.5", path = "../zcash_client_backend" }
zcash_primitives = { version = "0.7", path = "../zcash_primitives" }

View File

@ -67,6 +67,7 @@ where
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use secrecy::Secret;
use tempfile::NamedTempFile;
use zcash_primitives::{
@ -97,8 +98,8 @@ mod tests {
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).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);
@ -176,8 +177,8 @@ mod tests {
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).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);
@ -246,8 +247,8 @@ mod tests {
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).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);
@ -316,8 +317,8 @@ mod tests {
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).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);
@ -385,8 +386,8 @@ mod tests {
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).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);
@ -440,8 +441,8 @@ mod tests {
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).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);
@ -492,8 +493,8 @@ mod tests {
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).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

@ -3,7 +3,10 @@
use std::error;
use std::fmt;
use zcash_client_backend::{data_api, encoding::TransparentCodecError};
use zcash_client_backend::{
data_api,
encoding::{Bech32DecodeError, TransparentCodecError},
};
use zcash_primitives::consensus::BlockHeight;
use crate::{NoteId, PRUNING_HEIGHT};
@ -27,8 +30,8 @@ pub enum SqliteClientError {
/// Illegal attempt to reinitialize an already-initialized wallet database.
TableNotEmpty,
/// Bech32 decoding error
Bech32(bech32::Error),
/// A Bech32-encoded key or address decoding error
Bech32DecodeError(Bech32DecodeError),
/// Base58 decoding error
Base58(bs58::decode::Error),
@ -58,7 +61,7 @@ impl error::Error for SqliteClientError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self {
SqliteClientError::InvalidMemo(e) => Some(e),
SqliteClientError::Bech32(e) => Some(e),
SqliteClientError::Bech32DecodeError(Bech32DecodeError::Bech32Error(e)) => Some(e),
SqliteClientError::DbError(e) => Some(e),
SqliteClientError::Io(e) => Some(e),
_ => None,
@ -78,7 +81,7 @@ impl fmt::Display for SqliteClientError {
write!(f, "The note ID associated with an inserted witness must correspond to a received note."),
SqliteClientError::RequestedRewindInvalid(h, r) =>
write!(f, "A rewind must be either of less than {} blocks, or at least back to block {} for your wallet; the requested height was {}.", PRUNING_HEIGHT, h, r),
SqliteClientError::Bech32(e) => write!(f, "{}", e),
SqliteClientError::Bech32DecodeError(e) => write!(f, "{}", e),
SqliteClientError::Base58(e) => write!(f, "{}", e),
SqliteClientError::TransparentAddress(e) => write!(f, "{}", e),
SqliteClientError::TableNotEmpty => write!(f, "Table is not empty"),
@ -102,9 +105,9 @@ impl From<std::io::Error> for SqliteClientError {
}
}
impl From<bech32::Error> for SqliteClientError {
fn from(e: bech32::Error) -> Self {
SqliteClientError::Bech32(e)
impl From<Bech32DecodeError> for SqliteClientError {
fn from(e: Bech32DecodeError) -> Self {
SqliteClientError::Bech32DecodeError(e)
}
}

View File

@ -181,17 +181,15 @@ pub(crate) fn get_unified_full_viewing_keys<P: consensus::Parameters>(
.conn
.prepare("SELECT account, ufvk FROM accounts ORDER BY account ASC")?;
let rows = stmt_fetch_accounts
.query_map(NO_PARAMS, |row| {
let acct: u32 = row.get(0)?;
let account = AccountId::from(acct);
let ufvk_str: String = row.get(1)?;
let ufvk = UnifiedFullViewingKey::decode(&wdb.params, &ufvk_str)
.map_err(SqliteClientError::CorruptedData);
let rows = stmt_fetch_accounts.query_map(NO_PARAMS, |row| {
let acct: u32 = row.get(0)?;
let account = AccountId::from(acct);
let ufvk_str: String = row.get(1)?;
let ufvk = UnifiedFullViewingKey::decode(&wdb.params, &ufvk_str)
.map_err(SqliteClientError::CorruptedData);
Ok((account, ufvk))
})
.map_err(SqliteClientError::from)?;
Ok((account, ufvk))
})?;
let mut res: HashMap<AccountId, UnifiedFullViewingKey> = HashMap::new();
for row in rows {
@ -1229,6 +1227,7 @@ pub fn insert_sent_utxo<'a, P: consensus::Parameters>(
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use secrecy::Secret;
use tempfile::NamedTempFile;
use zcash_primitives::transaction::components::Amount;
@ -1242,8 +1241,8 @@ mod tests {
#[test]
fn empty_database_has_no_balance() {
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).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);

File diff suppressed because it is too large Load Diff

View File

@ -157,6 +157,7 @@ pub fn select_spendable_sapling_notes<P>(
#[allow(deprecated)]
mod tests {
use rusqlite::Connection;
use secrecy::Secret;
use std::collections::HashMap;
use tempfile::NamedTempFile;
@ -205,8 +206,8 @@ mod tests {
#[test]
fn create_to_address_fails_on_incorrect_extsk() {
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
let acct0 = AccountId::from(0);
let acct1 = AccountId::from(1);
@ -289,8 +290,8 @@ mod tests {
#[test]
fn create_to_address_fails_with_no_blocks() {
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
@ -327,8 +328,8 @@ mod tests {
#[test]
fn create_to_address_fails_on_insufficient_balance() {
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
init_blocks_table(
&db_data,
BlockHeight::from(1u32),
@ -385,8 +386,8 @@ mod tests {
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
@ -520,8 +521,8 @@ mod tests {
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
@ -651,8 +652,8 @@ mod tests {
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), network).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);
@ -763,8 +764,8 @@ mod tests {
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet
let account_id = AccountId::from(0);

View File

@ -288,7 +288,6 @@ impl DiversifiableFullViewingKey {
/// Returns the payment address corresponding to the smallest valid diversifier index,
/// along with that index.
// TODO: See if this is only used in tests.
pub fn default_address(&self) -> (zip32::DiversifierIndex, PaymentAddress) {
zip32::sapling_default_address(&self.fvk, &self.dk)
}