Merge pull request #1258 from nuttycom/sqlite_wallet/fuzzy_key_matching

Return partial matches when using `WalletRead::get_account_for_ufvk`.
This commit is contained in:
Kris Nuttycombe 2024-03-12 11:12:34 -06:00 committed by GitHub
commit 5e810d3689
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 172 additions and 43 deletions

View File

@ -13,6 +13,7 @@ and this library adheres to Rust's notion of
changes related to `Orchard` below are introduced under this feature
flag.
- `zcash_client_backend::data_api`:
- `Account`
- `AccountBalance::with_orchard_balance_mut`
- `AccountBirthday::orchard_frontier`
- `BlockMetadata::orchard_tree_size`
@ -49,7 +50,10 @@ and this library adheres to Rust's notion of
- Arguments to `BlockMetadata::from_parts` have changed.
- Arguments to `ScannedBlock::from_parts` have changed.
- Changes to the `WalletRead` trait:
- Added `Account` associated type.
- Added `get_orchard_nullifiers`
- `get_account_for_ufvk` now returns an `Self::Account` instead of a bare
`AccountId`
- Changes to the `InputSource` trait:
- `select_spendable_notes` now takes its `target_value` argument as a
`NonNegativeAmount`. Also, the values of the returned map are also

View File

@ -311,6 +311,35 @@ impl AccountBalance {
}
}
/// A set of capabilities that a client account must provide.
pub trait Account<AccountId: Copy> {
/// Returns the unique identifier for the account.
fn id(&self) -> AccountId;
/// Returns the UFVK that the wallet backend has stored for the account, if any.
fn ufvk(&self) -> Option<&UnifiedFullViewingKey>;
}
impl<A: Copy> Account<A> for (A, UnifiedFullViewingKey) {
fn id(&self) -> A {
self.0
}
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
Some(&self.1)
}
}
impl<A: Copy> Account<A> for (A, Option<UnifiedFullViewingKey>) {
fn id(&self) -> A {
self.0
}
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
self.1.as_ref()
}
}
/// A polymorphic ratio type, usually used for rational numbers.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Ratio<T> {
@ -507,6 +536,9 @@ pub trait WalletRead {
/// will be interpreted as belonging to that account.
type AccountId: Copy + Debug + Eq + Hash;
/// The concrete account type used by this wallet backend.
type Account: Account<Self::AccountId>;
/// Verifies that the given seed corresponds to the viewing key for the specified account.
///
/// Returns:
@ -617,11 +649,11 @@ pub trait WalletRead {
&self,
) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error>;
/// Returns the account id corresponding to a given [`UnifiedFullViewingKey`], if any.
/// Returns the account corresponding to a given [`UnifiedFullViewingKey`], if any.
fn get_account_for_ufvk(
&self,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::AccountId>, Self::Error>;
) -> Result<Option<Self::Account>, Self::Error>;
/// Returns the wallet balances and sync status for an account given the specified minimum
/// number of confirmations, or `Ok(None)` if the wallet has no balance data available.
@ -1466,6 +1498,7 @@ pub mod testing {
impl WalletRead for MockWalletDb {
type Error = ();
type AccountId = u32;
type Account = (Self::AccountId, UnifiedFullViewingKey);
fn validate_seed(
&self,
@ -1551,7 +1584,7 @@ pub mod testing {
fn get_account_for_ufvk(
&self,
_ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::AccountId>, Self::Error> {
) -> Result<Option<Self::Account>, Self::Error> {
Ok(None)
}

View File

@ -45,8 +45,8 @@ use super::InputSource;
use crate::{
address::Address,
data_api::{
error::Error, SentTransaction, SentTransactionOutput, WalletCommitmentTrees, WalletRead,
WalletWrite,
error::Error, Account, SentTransaction, SentTransactionOutput, WalletCommitmentTrees,
WalletRead, WalletWrite,
},
decrypt_transaction,
fees::{self, DustOutputPolicy},
@ -269,7 +269,7 @@ where
wallet_db,
params,
StandardFeeRule::PreZip313,
account,
account.id(),
min_confirmations,
to,
amount,
@ -380,7 +380,7 @@ where
let proposal = propose_transfer(
wallet_db,
params,
account,
account.id(),
input_selector,
request,
min_confirmations,
@ -694,7 +694,8 @@ where
let account = wallet_db
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
.map_err(Error::DataSource)?
.ok_or(Error::KeyNotRecognized)?;
.ok_or(Error::KeyNotRecognized)?
.id();
let (sapling_anchor, sapling_inputs) =
if proposal_step.involves(PoolType::Shielded(ShieldedProtocol::Sapling)) {

View File

@ -283,6 +283,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for WalletDb<C, P> {
type Error = SqliteClientError;
type AccountId = AccountId;
type Account = (AccountId, Option<UnifiedFullViewingKey>);
fn validate_seed(
&self,
@ -400,7 +401,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
fn get_account_for_ufvk(
&self,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<AccountId>, Self::Error> {
) -> Result<Option<Self::Account>, Self::Error> {
wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk)
}

View File

@ -295,7 +295,7 @@ struct AccountSqlValues<'a> {
account_type: u32,
hd_seed_fingerprint: Option<&'a [u8]>,
hd_account_index: Option<u32>,
ufvk: Option<String>,
ufvk: Option<&'a UnifiedFullViewingKey>,
uivk: String,
}
@ -309,14 +309,14 @@ fn get_sql_values_for_account_parameters<'a, P: consensus::Parameters>(
account_type: AccountType::Zip32.into(),
hd_seed_fingerprint: Some(hdaccount.hd_seed_fingerprint().as_bytes()),
hd_account_index: Some(hdaccount.account_index().into()),
ufvk: Some(hdaccount.ufvk().encode(params)),
ufvk: Some(hdaccount.ufvk()),
uivk: ufvk_to_uivk(hdaccount.ufvk(), params)?,
},
Account::Imported(ImportedAccount::Full(ufvk)) => AccountSqlValues {
account_type: AccountType::Imported.into(),
hd_seed_fingerprint: None,
hd_account_index: None,
ufvk: Some(ufvk.encode(params)),
ufvk: Some(ufvk),
uivk: ufvk_to_uivk(ufvk, params)?,
},
Account::Imported(ImportedAccount::Incoming(uivk)) => AccountSqlValues {
@ -361,21 +361,49 @@ pub(crate) fn add_account<P: consensus::Parameters>(
birthday: AccountBirthday,
) -> Result<AccountId, SqliteClientError> {
let args = get_sql_values_for_account_parameters(&account, params)?;
let account_id: AccountId = conn.query_row(r#"
INSERT INTO accounts (account_type, hd_seed_fingerprint, hd_account_index, ufvk, uivk, birthday_height, recover_until_height)
VALUES (:account_type, :hd_seed_fingerprint, :hd_account_index, :ufvk, :uivk, :birthday_height, :recover_until_height)
let orchard_item = args
.ufvk
.and_then(|ufvk| ufvk.orchard().map(|k| k.to_bytes()));
let sapling_item = args
.ufvk
.and_then(|ufvk| ufvk.sapling().map(|k| k.to_bytes()));
#[cfg(feature = "transparent-inputs")]
let transparent_item = args
.ufvk
.and_then(|ufvk| ufvk.transparent().map(|k| k.serialize()));
#[cfg(not(feature = "transparent-inputs"))]
let transparent_item: Option<Vec<u8>> = None;
let account_id: AccountId = conn.query_row(
r#"
INSERT INTO accounts (
account_type, hd_seed_fingerprint, hd_account_index,
ufvk, uivk,
orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache,
birthday_height, recover_until_height
)
VALUES (
:account_type, :hd_seed_fingerprint, :hd_account_index,
:ufvk, :uivk,
:orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache,
:birthday_height, :recover_until_height
)
RETURNING id;
"#,
named_params![
":account_type": args.account_type,
":hd_seed_fingerprint": args.hd_seed_fingerprint,
":hd_account_index": args.hd_account_index,
":ufvk": args.ufvk,
":ufvk": args.ufvk.map(|ufvk| ufvk.encode(params)),
":uivk": args.uivk,
":orchard_fvk_item_cache": orchard_item,
":sapling_fvk_item_cache": sapling_item,
":p2pkh_fvk_item_cache": transparent_item,
":birthday_height": u32::from(birthday.height()),
":recover_until_height": birthday.recover_until().map(u32::from)
],
|row| Ok(AccountId(row.get(0)?))
|row| Ok(AccountId(row.get(0)?)),
)?;
// If a birthday frontier is available, insert it into the note commitment tree. If the
@ -703,17 +731,52 @@ pub(crate) fn get_account_for_ufvk<P: consensus::Parameters>(
conn: &rusqlite::Connection,
params: &P,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<AccountId>, SqliteClientError> {
conn.query_row(
"SELECT id FROM accounts WHERE ufvk = ?",
[&ufvk.encode(params)],
) -> Result<Option<(AccountId, Option<UnifiedFullViewingKey>)>, SqliteClientError> {
#[cfg(feature = "transparent-inputs")]
let transparent_item = ufvk.transparent().map(|k| k.serialize());
#[cfg(not(feature = "transparent-inputs"))]
let transparent_item: Option<Vec<u8>> = None;
let mut stmt = conn.prepare(
"SELECT id, ufvk
FROM accounts
WHERE orchard_fvk_item_cache = :orchard_fvk_item_cache
OR sapling_fvk_item_cache = :sapling_fvk_item_cache
OR p2pkh_fvk_item_cache = :p2pkh_fvk_item_cache",
)?;
let accounts = stmt
.query_and_then::<_, SqliteClientError, _, _>(
named_params![
":orchard_fvk_item_cache": ufvk.orchard().map(|k| k.to_bytes()),
":sapling_fvk_item_cache": ufvk.sapling().map(|k| k.to_bytes()),
":p2pkh_fvk_item_cache": transparent_item,
],
|row| {
let acct = row.get(0)?;
Ok(AccountId(acct))
let account_id = row.get::<_, u32>(0).map(AccountId)?;
Ok((
account_id,
row.get::<_, Option<String>>(1)?
.map(|ufvk_str| UnifiedFullViewingKey::decode(params, &ufvk_str))
.transpose()
.map_err(|e| {
SqliteClientError::CorruptedData(format!(
"Could not decode unified full viewing key for account {:?}: {}",
account_id, e
))
})?,
))
},
)
.optional()
.map_err(SqliteClientError::from)
)?
.collect::<Result<Vec<_>, _>>()?;
if accounts.len() > 1 {
Err(SqliteClientError::CorruptedData(
"Mutiple account records matched the provided UFVK".to_owned(),
))
} else {
Ok(accounts.into_iter().next())
}
}
pub(crate) trait ScanProgress {

View File

@ -229,6 +229,9 @@ mod tests {
hd_account_index INTEGER,
ufvk TEXT,
uivk TEXT NOT NULL,
orchard_fvk_item_cache BLOB,
sapling_fvk_item_cache BLOB,
p2pkh_fvk_item_cache BLOB,
birthday_height INTEGER NOT NULL,
recover_until_height INTEGER,
CHECK ( (account_type = 0 AND hd_seed_fingerprint IS NOT NULL AND hd_account_index IS NOT NULL AND ufvk IS NOT NULL) OR (account_type = 1 AND hd_seed_fingerprint IS NULL AND hd_account_index IS NULL) )

View File

@ -58,6 +58,9 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
hd_account_index INTEGER,
ufvk TEXT,
uivk TEXT NOT NULL,
orchard_fvk_item_cache BLOB,
sapling_fvk_item_cache BLOB,
p2pkh_fvk_item_cache BLOB,
birthday_height INTEGER NOT NULL,
recover_until_height INTEGER,
CHECK (
@ -116,19 +119,40 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
let uivk = ufvk_to_uivk(&ufvk_parsed, &self.params)
.map_err(|e| WalletMigrationError::CorruptedData(e.to_string()))?;
transaction.execute(r#"
INSERT INTO accounts_new (id, account_type, hd_seed_fingerprint, hd_account_index, ufvk, uivk, birthday_height, recover_until_height)
VALUES (:account_id, :account_type, :seed_id, :account_index, :ufvk, :uivk, :birthday_height, :recover_until_height);
"#, named_params![
#[cfg(feature = "transparent-inputs")]
let transparent_item = ufvk_parsed.transparent().map(|k| k.serialize());
#[cfg(not(feature = "transparent-inputs"))]
let transparent_item: Option<Vec<u8>> = None;
transaction.execute(
r#"
INSERT INTO accounts_new (
id, account_type, hd_seed_fingerprint, hd_account_index,
ufvk, uivk,
orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache,
birthday_height, recover_until_height
)
VALUES (
:account_id, :account_type, :seed_id, :account_index,
:ufvk, :uivk,
:orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache,
:birthday_height, :recover_until_height
);
"#,
named_params![
":account_id": account_id,
":account_type": account_type,
":seed_id": seed_id.as_bytes(),
":account_index": account_index,
":ufvk": ufvk,
":uivk": uivk,
":orchard_fvk_item_cache": ufvk_parsed.orchard().map(|k| k.to_bytes()),
":sapling_fvk_item_cache": ufvk_parsed.sapling().map(|k| k.to_bytes()),
":p2pkh_fvk_item_cache": transparent_item,
":birthday_height": birthday_height,
":recover_until_height": recover_until_height,
])?;
],
)?;
}
} else {
return Err(WalletMigrationError::SeedRequired);