Merge pull request #1213 from nuttycom/wallet/key_validation

zcash_client_backend: Add `WalletRead::validate_seed`
This commit is contained in:
str4d 2024-03-04 21:54:23 +00:00 committed by GitHub
commit e78ea02240
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 132 additions and 12 deletions

1
Cargo.lock generated
View File

@ -3085,6 +3085,7 @@ dependencies = [
"zcash_note_encryption", "zcash_note_encryption",
"zcash_primitives", "zcash_primitives",
"zcash_proofs", "zcash_proofs",
"zip32",
] ]
[[package]] [[package]]

View File

@ -50,6 +50,7 @@ and this library adheres to Rust's notion of
- `type OrchardShardStore` - `type OrchardShardStore`
- `fn with_orchard_tree_mut` - `fn with_orchard_tree_mut`
- `fn put_orchard_subtree_roots` - `fn put_orchard_subtree_roots`
- Added method `WalletRead::validate_seed`
- `zcash_client_backend::fees`: - `zcash_client_backend::fees`:
- Arguments to `ChangeStrategy::compute_balance` have changed. - Arguments to `ChangeStrategy::compute_balance` have changed.

View File

@ -492,6 +492,21 @@ pub trait WalletRead {
/// will be interpreted as belonging to that account. /// will be interpreted as belonging to that account.
type AccountId: Copy + Debug + Eq + Hash; type AccountId: Copy + Debug + Eq + Hash;
/// Verifies that the given seed corresponds to the viewing key for the specified account.
///
/// Returns:
/// - `Ok(true)` if the viewing key for the specified account can be derived from the
/// provided seed.
/// - `Ok(false)` if the derived seed does not match, or the specified account is not
/// present in the database.
/// - `Err(_)` if a Unified Spending Key cannot be derived from the seed for the
/// specified account.
fn validate_seed(
&self,
account_id: Self::AccountId,
seed: &SecretVec<u8>,
) -> Result<bool, Self::Error>;
/// Returns the height of the chain as known to the wallet as of the most recent call to /// Returns the height of the chain as known to the wallet as of the most recent call to
/// [`WalletWrite::update_chain_tip`]. /// [`WalletWrite::update_chain_tip`].
/// ///
@ -1317,14 +1332,15 @@ pub mod testing {
type Error = (); type Error = ();
type AccountId = u32; type AccountId = u32;
fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> { fn validate_seed(
Ok(None) &self,
_account_id: Self::AccountId,
_seed: &SecretVec<u8>,
) -> Result<bool, Self::Error> {
Ok(false)
} }
fn get_target_and_anchor_heights( fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
&self,
_min_confirmations: NonZeroU32,
) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
Ok(None) Ok(None)
} }
@ -1347,6 +1363,13 @@ pub mod testing {
Ok(vec![]) Ok(vec![])
} }
fn get_target_and_anchor_heights(
&self,
_min_confirmations: NonZeroU32,
) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
Ok(None)
}
fn get_min_unspent_height(&self) -> Result<Option<BlockHeight>, Self::Error> { fn get_min_unspent_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None) Ok(None)
} }

View File

@ -24,6 +24,7 @@ zcash_client_backend = { workspace = true, features = ["unstable-serialization",
zcash_encoding.workspace = true zcash_encoding.workspace = true
zcash_keys = { workspace = true, features = ["orchard", "sapling"] } zcash_keys = { workspace = true, features = ["orchard", "sapling"] }
zcash_primitives.workspace = true zcash_primitives.workspace = true
zip32.workspace = true
# Dependencies exposed in a public API: # Dependencies exposed in a public API:
# (Breaking upgrades to these require a breaking upgrade to this crate.) # (Breaking upgrades to these require a breaking upgrade to this crate.)

View File

@ -245,6 +245,32 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
type Error = SqliteClientError; type Error = SqliteClientError;
type AccountId = AccountId; type AccountId = AccountId;
fn validate_seed(
&self,
account_id: Self::AccountId,
seed: &SecretVec<u8>,
) -> Result<bool, Self::Error> {
self.get_unified_full_viewing_keys().and_then(|keys| {
keys.get(&account_id).map_or(Ok(false), |ufvk| {
let usk = UnifiedSpendingKey::from_seed(
&self.params,
&seed.expose_secret()[..],
account_id,
)
.map_err(|_| SqliteClientError::KeyDerivationError(account_id))?;
// Keys are not comparable with `Eq`, but addresses are, so we derive what should
// be equivalent addresses for each key and use those to check for key equality.
UnifiedAddressRequest::all().map_or(Ok(false), |ua_request| {
Ok(usk
.to_unified_full_viewing_key()
.default_address(ua_request)
== ufvk.default_address(ua_request))
})
})
})
}
fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> { fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
wallet::scan_queue_extrema(self.conn.borrow()) wallet::scan_queue_extrema(self.conn.borrow())
.map(|h| h.map(|range| *range.end())) .map(|h| h.map(|range| *range.end()))
@ -1204,9 +1230,11 @@ extern crate assert_matches;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use secrecy::SecretVec;
use zcash_client_backend::data_api::{AccountBirthday, WalletRead, WalletWrite}; use zcash_client_backend::data_api::{AccountBirthday, WalletRead, WalletWrite};
use crate::{testing::TestBuilder, AccountId, DEFAULT_UA_REQUEST}; use crate::{testing::TestBuilder, DEFAULT_UA_REQUEST};
use zip32::AccountId;
#[cfg(feature = "unstable")] #[cfg(feature = "unstable")]
use { use {
@ -1217,6 +1245,36 @@ mod tests {
}, },
}; };
#[test]
fn validate_seed() {
let st = TestBuilder::new()
.with_test_account(AccountBirthday::from_sapling_activation)
.build();
assert!({
let account = st.test_account().unwrap().0;
st.wallet()
.validate_seed(account, st.test_seed().unwrap())
.unwrap()
});
// check that passing an invalid account results in a failure
assert!({
let account = AccountId::try_from(1u32).unwrap();
!st.wallet()
.validate_seed(account, st.test_seed().unwrap())
.unwrap()
});
// check that passing an invalid seed results in a failure
assert!({
let account = st.test_account().unwrap().0;
!st.wallet()
.validate_seed(account, &SecretVec::new(vec![1u8; 32]))
.unwrap()
});
}
#[test] #[test]
pub(crate) fn get_next_available_address() { pub(crate) fn get_next_available_address() {
let mut st = TestBuilder::new() let mut st = TestBuilder::new()

View File

@ -9,7 +9,7 @@ use nonempty::NonEmpty;
use prost::Message; use prost::Message;
use rand_core::{OsRng, RngCore}; use rand_core::{OsRng, RngCore};
use rusqlite::{params, Connection}; use rusqlite::{params, Connection};
use secrecy::Secret; use secrecy::{Secret, SecretVec};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
#[cfg(feature = "unstable")] #[cfg(feature = "unstable")]
@ -142,7 +142,7 @@ impl<Cache> TestBuilder<Cache> {
let test_account = if let Some(birthday) = self.test_account_birthday { let test_account = if let Some(birthday) = self.test_account_birthday {
let seed = Secret::new(vec![0u8; 32]); let seed = Secret::new(vec![0u8; 32]);
let (account, usk) = db_data.create_account(&seed, birthday.clone()).unwrap(); let (account, usk) = db_data.create_account(&seed, birthday.clone()).unwrap();
Some((account, usk, birthday)) Some((seed, account, usk, birthday))
} else { } else {
None None
}; };
@ -163,7 +163,12 @@ pub(crate) struct TestState<Cache> {
latest_cached_block: Option<(BlockHeight, BlockHash, u32)>, latest_cached_block: Option<(BlockHeight, BlockHash, u32)>,
_data_file: NamedTempFile, _data_file: NamedTempFile,
db_data: WalletDb<Connection, Network>, db_data: WalletDb<Connection, Network>,
test_account: Option<(AccountId, UnifiedSpendingKey, AccountBirthday)>, test_account: Option<(
SecretVec<u8>,
AccountId,
UnifiedSpendingKey,
AccountBirthday,
)>,
} }
impl<Cache: TestCache> TestState<Cache> impl<Cache: TestCache> TestState<Cache>
@ -420,16 +425,23 @@ impl<Cache> TestState<Cache> {
.expect("Sapling activation height must be known.") .expect("Sapling activation height must be known.")
} }
/// Exposes the test seed, if enabled via [`TestBuilder::with_test_account`].
pub(crate) fn test_seed(&self) -> Option<&SecretVec<u8>> {
self.test_account.as_ref().map(|(seed, _, _, _)| seed)
}
/// Exposes the test account, if enabled via [`TestBuilder::with_test_account`]. /// Exposes the test account, if enabled via [`TestBuilder::with_test_account`].
pub(crate) fn test_account(&self) -> Option<(AccountId, UnifiedSpendingKey, AccountBirthday)> { pub(crate) fn test_account(&self) -> Option<(AccountId, UnifiedSpendingKey, AccountBirthday)> {
self.test_account.as_ref().cloned() self.test_account
.as_ref()
.map(|(_, a, k, b)| (*a, k.clone(), b.clone()))
} }
/// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`].
pub(crate) fn test_account_sapling(&self) -> Option<DiversifiableFullViewingKey> { pub(crate) fn test_account_sapling(&self) -> Option<DiversifiableFullViewingKey> {
self.test_account self.test_account
.as_ref() .as_ref()
.and_then(|(_, usk, _)| usk.to_unified_full_viewing_key().sapling().cloned()) .and_then(|(_, _, usk, _)| usk.to_unified_full_viewing_key().sapling().cloned())
} }
/// Invokes [`create_spend_to_address`] with the given arguments. /// Invokes [`create_spend_to_address`] with the given arguments.

View File

@ -6,6 +6,9 @@ and this library adheres to Rust's notion of
## [Unreleased] ## [Unreleased]
### Added
- `zcash_keys::keys::UnifiedAddressRequest::all`
## [0.1.0] - 2024-03-01 ## [0.1.0] - 2024-03-01
The entries below are relative to the `zcash_client_backend` crate as of The entries below are relative to the `zcash_client_backend` crate as of
`zcash_client_backend 0.10.0`. `zcash_client_backend 0.10.0`.

View File

@ -450,6 +450,9 @@ pub struct UnifiedAddressRequest {
} }
impl UnifiedAddressRequest { impl UnifiedAddressRequest {
/// Construct a new unified address request from its constituent parts.
///
/// Returns `None` if the resulting unified address would not include at least one shielded receiver.
pub fn new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Option<Self> { pub fn new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Option<Self> {
let has_shielded_receiver = has_orchard || has_sapling; let has_shielded_receiver = has_orchard || has_sapling;
@ -464,6 +467,24 @@ impl UnifiedAddressRequest {
} }
} }
/// Constructs a new unified address request that includes a request for a receiver of each
/// type that is supported given the active feature flags.
pub fn all() -> Option<Self> {
let _has_orchard = false;
#[cfg(feature = "orchard")]
let _has_orchard = true;
let _has_sapling = false;
#[cfg(feature = "sapling")]
let _has_sapling = true;
let _has_p2pkh = false;
#[cfg(feature = "transparent-inputs")]
let _has_p2pkh = true;
Self::new(_has_orchard, _has_sapling, _has_p2pkh)
}
/// Construct a new unified address request from its constituent parts. /// Construct a new unified address request from its constituent parts.
/// ///
/// Panics: at least one of `has_orchard` or `has_sapling` must be `true`. /// Panics: at least one of `has_orchard` or `has_sapling` must be `true`.