Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return partial matches when using WalletRead::get_account_for_ufvk. #1258

Merged
merged 3 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions zcash_client_backend/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down
39 changes: 36 additions & 3 deletions zcash_client_backend/src/data_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,35 @@
}
}

/// 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

Check warning on line 325 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L324-L325

Added lines #L324 - L325 were not covered by tests
}

fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
Some(&self.1)

Check warning on line 329 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L328-L329

Added lines #L328 - L329 were not covered by tests
}
}

impl<A: Copy> Account<A> for (A, Option<UnifiedFullViewingKey>) {
fn id(&self) -> A {
self.0
}

fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
self.1.as_ref()

Check warning on line 339 in zcash_client_backend/src/data_api.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api.rs#L338-L339

Added lines #L338 - L339 were not covered by tests
}
}

/// A polymorphic ratio type, usually used for rational numbers.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Ratio<T> {
Expand Down Expand Up @@ -507,6 +536,9 @@
/// 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:
Expand Down Expand Up @@ -617,11 +649,11 @@
&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.
Expand Down Expand Up @@ -1466,6 +1498,7 @@
impl WalletRead for MockWalletDb {
type Error = ();
type AccountId = u32;
type Account = (Self::AccountId, UnifiedFullViewingKey);

fn validate_seed(
&self,
Expand Down Expand Up @@ -1551,7 +1584,7 @@
fn get_account_for_ufvk(
&self,
_ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::AccountId>, Self::Error> {
) -> Result<Option<Self::Account>, Self::Error> {
Ok(None)
}

Expand Down
11 changes: 6 additions & 5 deletions zcash_client_backend/src/data_api/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -269,7 +269,7 @@ where
wallet_db,
params,
StandardFeeRule::PreZip313,
account,
account.id(),
min_confirmations,
to,
amount,
Expand Down Expand Up @@ -380,7 +380,7 @@ where
let proposal = propose_transfer(
wallet_db,
params,
account,
account.id(),
input_selector,
request,
min_confirmations,
Expand Down Expand Up @@ -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)) {
Expand Down
3 changes: 2 additions & 1 deletion zcash_client_sqlite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,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,
Expand Down Expand Up @@ -375,7 +376,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)
}

Expand Down
105 changes: 84 additions & 21 deletions zcash_client_sqlite/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@
/// Returns the default Unified Address for the account,
/// along with the diversifier index that generated it.
///
/// The diversifier index may be non-zero if the Unified Address includes a Sapling
/// The diversifier index may be non-zero if the Unified Address includes a Sapling
/// receiver, and there was no valid Sapling receiver at diversifier index zero.
pub fn default_address(
&self,
Expand Down Expand Up @@ -294,11 +294,11 @@
account_type: u32,
hd_seed_fingerprint: Option<&'a [u8]>,
hd_account_index: Option<u32>,
ufvk: Option<String>,
ufvk: Option<&'a UnifiedFullViewingKey>,
uivk: String,
}

/// Returns (account_type, hd_seed_fingerprint, hd_account_index, ufvk, uivk) for a given account.
/// Returns (account_type, hd_seed_fingerprint, hd_account_index, ufvk, uivk) for a given account.
fn get_sql_values_for_account_parameters<'a, P: consensus::Parameters>(
account: &'a Account,
params: &P,
Expand All @@ -308,14 +308,14 @@
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),

Check warning on line 318 in zcash_client_sqlite/src/wallet.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/wallet.rs#L318

Added line #L318 was not covered by tests
uivk: ufvk_to_uivk(ufvk, params)?,
},
Account::Imported(ImportedAccount::Incoming(uivk)) => AccountSqlValues {
Expand Down Expand Up @@ -360,21 +360,49 @@
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;

Check warning on line 375 in zcash_client_sqlite/src/wallet.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/wallet.rs#L375

Added line #L375 was not covered by tests

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
Expand Down Expand Up @@ -702,17 +730,52 @@
conn: &rusqlite::Connection,
params: &P,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<AccountId>, SqliteClientError> {
conn.query_row(
"SELECT id FROM accounts WHERE ufvk = ?",
[&ufvk.encode(params)],
|row| {
let acct = row.get(0)?;
Ok(AccountId(acct))
},
)
.optional()
.map_err(SqliteClientError::from)
) -> 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;

Check warning on line 737 in zcash_client_sqlite/src/wallet.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/wallet.rs#L737

Added line #L737 was not covered by tests

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 account_id = row.get::<_, u32>(0).map(AccountId)?;
Ok((

Check warning on line 756 in zcash_client_sqlite/src/wallet.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/wallet.rs#L756

Added line #L756 was not covered by tests
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

Check warning on line 764 in zcash_client_sqlite/src/wallet.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/wallet.rs#L762-L764

Added lines #L762 - L764 were not covered by tests
))
})?,
))
},
)?
.collect::<Result<Vec<_>, _>>()?;

if accounts.len() > 1 {
Err(SqliteClientError::CorruptedData(
"Mutiple account records matched the provided UFVK".to_owned(),

Check warning on line 774 in zcash_client_sqlite/src/wallet.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_sqlite/src/wallet.rs#L773-L774

Added lines #L773 - L774 were not covered by tests
))
} else {
Ok(accounts.into_iter().next())
}
}

pub(crate) trait ScanProgress {
Expand Down
3 changes: 3 additions & 0 deletions zcash_client_sqlite/src/wallet/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) )
Expand Down
50 changes: 37 additions & 13 deletions zcash_client_sqlite/src/wallet/init/migrations/full_account_ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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![
":account_id": account_id,
":account_type": account_type,
":seed_id": seed_id.as_bytes(),
":account_index": account_index,
":ufvk": ufvk,
":uivk": uivk,
":birthday_height": birthday_height,
":recover_until_height": recover_until_height,
])?;
#[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);
Expand Down
Loading