Merge remote-tracking branch 'upstream/main' into memory_wallet_db

This commit is contained in:
Kris Nuttycombe 2024-03-20 11:33:10 -06:00
commit d013322aa1
28 changed files with 1536 additions and 463 deletions

View File

@ -34,7 +34,6 @@ jobs:
- state: Orchard - state: Orchard
extra_flags: orchard extra_flags: orchard
rustflags: '--cfg zcash_unstable="orchard"'
- state: NU6 - state: NU6
rustflags: '--cfg zcash_unstable="nu6"' rustflags: '--cfg zcash_unstable="nu6"'

11
Cargo.lock generated
View File

@ -1087,10 +1087,12 @@ dependencies = [
[[package]] [[package]]
name = "incrementalmerkletree" name = "incrementalmerkletree"
version = "0.5.0" version = "0.5.0"
source = "git+https://github.com/nuttycom/incrementalmerkletree?rev=fa147c89c6c98a03bba745538f4e68d4eaed5146#fa147c89c6c98a03bba745538f4e68d4eaed5146" source = "git+https://github.com/zcash/incrementalmerkletree?rev=e1a7a80212c22e5a8912d05860f7eb6899c56a7c#e1a7a80212c22e5a8912d05860f7eb6899c56a7c"
dependencies = [ dependencies = [
"either", "either",
"proptest", "proptest",
"rand",
"rand_core",
] ]
[[package]] [[package]]
@ -1475,7 +1477,7 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "orchard" name = "orchard"
version = "0.7.1" version = "0.7.1"
source = "git+https://github.com/zcash/orchard?rev=e74879dd0ad0918f4ffe0826e03905cd819981bd#e74879dd0ad0918f4ffe0826e03905cd819981bd" source = "git+https://github.com/zcash/orchard?rev=33474bdbfd7268e1f84718078d47f63d01a879d5#33474bdbfd7268e1f84718078d47f63d01a879d5"
dependencies = [ dependencies = [
"aes", "aes",
"bitvec", "bitvec",
@ -2103,8 +2105,7 @@ dependencies = [
[[package]] [[package]]
name = "sapling-crypto" name = "sapling-crypto"
version = "0.1.2" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/zcash/sapling-crypto?rev=22412ae07644813253feb064d1692b0823242853#22412ae07644813253feb064d1692b0823242853"
checksum = "0db258736b34dfc6bec50fab2afb94c1845d90370b38309ebf9bb166cc951251"
dependencies = [ dependencies = [
"aes", "aes",
"bellman", "bellman",
@ -2244,7 +2245,7 @@ dependencies = [
[[package]] [[package]]
name = "shardtree" name = "shardtree"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/nuttycom/incrementalmerkletree?rev=fa147c89c6c98a03bba745538f4e68d4eaed5146#fa147c89c6c98a03bba745538f4e68d4eaed5146" source = "git+https://github.com/zcash/incrementalmerkletree?rev=e1a7a80212c22e5a8912d05860f7eb6899c56a7c#e1a7a80212c22e5a8912d05860f7eb6899c56a7c"
dependencies = [ dependencies = [
"assert_matches", "assert_matches",
"bitflags 2.4.1", "bitflags 2.4.1",

View File

@ -122,6 +122,7 @@ panic = 'abort'
codegen-units = 1 codegen-units = 1
[patch.crates-io] [patch.crates-io]
orchard = { git = "https://github.com/zcash/orchard", rev = "e74879dd0ad0918f4ffe0826e03905cd819981bd" } orchard = { git = "https://github.com/zcash/orchard", rev = "33474bdbfd7268e1f84718078d47f63d01a879d5" }
incrementalmerkletree = { git = "https://github.com/nuttycom/incrementalmerkletree", rev = "fa147c89c6c98a03bba745538f4e68d4eaed5146" } incrementalmerkletree = { git = "https://github.com/zcash/incrementalmerkletree", rev = "e1a7a80212c22e5a8912d05860f7eb6899c56a7c" }
shardtree = { git = "https://github.com/nuttycom/incrementalmerkletree", rev = "fa147c89c6c98a03bba745538f4e68d4eaed5146" } sapling = { git = "https://github.com/zcash/sapling-crypto", package = "sapling-crypto", rev = "22412ae07644813253feb064d1692b0823242853" }
shardtree = { git = "https://github.com/zcash/incrementalmerkletree", rev = "e1a7a80212c22e5a8912d05860f7eb6899c56a7c" }

View File

@ -16,12 +16,13 @@ and this library adheres to Rust's notion of
- `Account` - `Account`
- `AccountBalance::with_orchard_balance_mut` - `AccountBalance::with_orchard_balance_mut`
- `AccountBirthday::orchard_frontier` - `AccountBirthday::orchard_frontier`
- `AccountKind` - `AccountSource`
- `BlockMetadata::orchard_tree_size` - `BlockMetadata::orchard_tree_size`
- `DecryptedTransaction::{new, tx(), orchard_outputs()}` - `DecryptedTransaction::{new, tx(), orchard_outputs()}`
- `NoteRetention` - `NoteRetention`
- `ScannedBlock::orchard` - `ScannedBlock::orchard`
- `ScannedBlockCommitments::orchard` - `ScannedBlockCommitments::orchard`
- `SeedRelevance`
- `SentTransaction::new` - `SentTransaction::new`
- `SpendableNotes` - `SpendableNotes`
- `ORCHARD_SHARD_HEIGHT` - `ORCHARD_SHARD_HEIGHT`
@ -29,6 +30,7 @@ and this library adheres to Rust's notion of
- `WalletSummary::next_orchard_subtree_index` - `WalletSummary::next_orchard_subtree_index`
- `chain::ChainState` - `chain::ChainState`
- `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}` - `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}`
- `impl Debug for chain::CommitmentTreeRoot`
- `zcash_client_backend::fees`: - `zcash_client_backend::fees`:
- `orchard` - `orchard`
- `ChangeValue::orchard` - `ChangeValue::orchard`
@ -59,6 +61,8 @@ and this library adheres to Rust's notion of
- Arguments to `ScannedBlock::from_parts` have changed. - Arguments to `ScannedBlock::from_parts` have changed.
- Changes to the `WalletRead` trait: - Changes to the `WalletRead` trait:
- Added `Account` associated type. - Added `Account` associated type.
- Added `validate_seed` method.
- Added `is_seed_relevant_to_any_derived_accounts` method.
- Added `get_account` method. - Added `get_account` method.
- Added `get_derived_account` method. - Added `get_derived_account` method.
- `get_account_for_ufvk` now returns `Self::Account` instead of a bare - `get_account_for_ufvk` now returns `Self::Account` instead of a bare
@ -80,7 +84,6 @@ 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`
- Removed `Error::AccountNotFound` variant. - Removed `Error::AccountNotFound` variant.
- `WalletSummary::new` now takes an additional `next_orchard_subtree_index` - `WalletSummary::new` now takes an additional `next_orchard_subtree_index`
argument when the `orchard` feature flag is enabled. argument when the `orchard` feature flag is enabled.

View File

@ -64,6 +64,7 @@ use std::{
}; };
use incrementalmerkletree::{frontier::Frontier, Retention}; use incrementalmerkletree::{frontier::Frontier, Retention};
use nonempty::NonEmpty;
use secrecy::SecretVec; use secrecy::SecretVec;
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree}; use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
use zip32::fingerprint::SeedFingerprint; use zip32::fingerprint::SeedFingerprint;
@ -75,7 +76,9 @@ use self::{
use crate::{ use crate::{
address::UnifiedAddress, address::UnifiedAddress,
decrypt::DecryptedOutput, decrypt::DecryptedOutput,
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{
UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedIncomingViewingKey, UnifiedSpendingKey,
},
proto::service::TreeState, proto::service::TreeState,
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx}, wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
ShieldedProtocol, ShieldedProtocol,
@ -320,7 +323,7 @@ impl AccountBalance {
/// The kinds of accounts supported by `zcash_client_backend`. /// The kinds of accounts supported by `zcash_client_backend`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AccountKind { pub enum AccountSource {
/// An account derived from a known seed. /// An account derived from a known seed.
Derived { Derived {
seed_fingerprint: SeedFingerprint, seed_fingerprint: SeedFingerprint,
@ -338,40 +341,57 @@ pub trait Account<AccountId: Copy> {
/// Returns whether this account is derived or imported, and the derivation parameters /// Returns whether this account is derived or imported, and the derivation parameters
/// if applicable. /// if applicable.
fn kind(&self) -> AccountKind; fn source(&self) -> AccountSource;
/// Returns the UFVK that the wallet backend has stored for the account, if any. /// Returns the UFVK that the wallet backend has stored for the account, if any.
/// ///
/// Accounts for which this returns `None` cannot be used in wallet contexts, because /// Accounts for which this returns `None` cannot be used in wallet contexts, because
/// they are unable to maintain an accurate balance. /// they are unable to maintain an accurate balance.
fn ufvk(&self) -> Option<&UnifiedFullViewingKey>; fn ufvk(&self) -> Option<&UnifiedFullViewingKey>;
/// Returns the UIVK that the wallet backend has stored for the account.
///
/// All accounts are required to have at least an incoming viewing key. This gives no
/// indication about whether an account can be used in a wallet context; for that, use
/// [`Account::ufvk`].
fn uivk(&self) -> UnifiedIncomingViewingKey;
} }
#[cfg(any(test, feature = "test-dependencies"))]
impl<A: Copy> Account<A> for (A, UnifiedFullViewingKey) { impl<A: Copy> Account<A> for (A, UnifiedFullViewingKey) {
fn id(&self) -> A { fn id(&self) -> A {
self.0 self.0
} }
fn kind(&self) -> AccountKind { fn source(&self) -> AccountSource {
AccountKind::Imported AccountSource::Imported
} }
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
Some(&self.1) Some(&self.1)
} }
fn uivk(&self) -> UnifiedIncomingViewingKey {
self.1.to_unified_incoming_viewing_key()
}
} }
impl<A: Copy> Account<A> for (A, Option<UnifiedFullViewingKey>) { #[cfg(any(test, feature = "test-dependencies"))]
impl<A: Copy> Account<A> for (A, UnifiedIncomingViewingKey) {
fn id(&self) -> A { fn id(&self) -> A {
self.0 self.0
} }
fn kind(&self) -> AccountKind { fn source(&self) -> AccountSource {
AccountKind::Imported AccountSource::Imported
} }
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
self.1.as_ref() None
}
fn uivk(&self) -> UnifiedIncomingViewingKey {
self.1.clone()
} }
} }
@ -724,6 +744,16 @@ pub trait WalletRead {
seed: &SecretVec<u8>, seed: &SecretVec<u8>,
) -> Result<bool, Self::Error>; ) -> Result<bool, Self::Error>;
/// Checks whether the given seed is relevant to any of the derived accounts (where
/// [`Account::source`] is [`AccountSource::Derived`]) in the wallet.
///
/// This API does not check whether the seed is relevant to any imported account,
/// because that would require brute-forcing the ZIP 32 account index space.
fn seed_relevance_to_derived_accounts(
&self,
seed: &SecretVec<u8>,
) -> Result<SeedRelevance<Self::AccountId>, Self::Error>;
/// Returns the account corresponding to a given [`UnifiedFullViewingKey`], if any. /// Returns the account corresponding to a given [`UnifiedFullViewingKey`], if any.
fn get_account_for_ufvk( fn get_account_for_ufvk(
&self, &self,
@ -884,6 +914,21 @@ pub trait WalletRead {
} }
} }
/// The relevance of a seed to a given wallet.
///
/// This is the return type for [`WalletRead::seed_relevance_to_derived_accounts`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SeedRelevance<A: Copy> {
/// The seed is relevant to at least one derived account within the wallet.
Relevant { account_ids: NonEmpty<A> },
/// The seed is not relevant to any of the derived accounts within the wallet.
NotRelevant,
/// The wallet contains no derived accounts.
NoDerivedAccounts,
/// The wallet contains no accounts.
NoAccounts,
}
/// Metadata describing the sizes of the zcash note commitment trees as of a particular block. /// Metadata describing the sizes of the zcash note commitment trees as of a particular block.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct BlockMetadata { pub struct BlockMetadata {
@ -1609,8 +1654,8 @@ pub mod testing {
chain::{ChainState, CommitmentTreeRoot}, chain::{ChainState, CommitmentTreeRoot},
scanning::ScanRange, scanning::ScanRange,
AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery, AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery,
ScannedBlock, SentTransaction, SpendableNotes, WalletCommitmentTrees, WalletRead, ScannedBlock, SeedRelevance, SentTransaction, SpendableNotes, WalletCommitmentTrees,
WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
}; };
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
@ -1703,6 +1748,13 @@ pub mod testing {
Ok(false) Ok(false)
} }
fn seed_relevance_to_derived_accounts(
&self,
_seed: &SecretVec<u8>,
) -> Result<SeedRelevance<Self::AccountId>, Self::Error> {
Ok(SeedRelevance::NoAccounts)
}
fn get_account_for_ufvk( fn get_account_for_ufvk(
&self, &self,
_ufvk: &UnifiedFullViewingKey, _ufvk: &UnifiedFullViewingKey,

View File

@ -155,7 +155,10 @@ use std::ops::Range;
use incrementalmerkletree::frontier::Frontier; use incrementalmerkletree::frontier::Frontier;
use subtle::ConditionallySelectable; use subtle::ConditionallySelectable;
use zcash_primitives::consensus::{self, BlockHeight}; use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
};
use crate::{ use crate::{
data_api::{NullifierQuery, WalletWrite}, data_api::{NullifierQuery, WalletWrite},
@ -172,6 +175,7 @@ use super::WalletRead;
/// ///
/// This stores the block height at which the leaf that completed the subtree was /// This stores the block height at which the leaf that completed the subtree was
/// added, and the root hash of the complete subtree. /// added, and the root hash of the complete subtree.
#[derive(Debug)]
pub struct CommitmentTreeRoot<H> { pub struct CommitmentTreeRoot<H> {
subtree_end_height: BlockHeight, subtree_end_height: BlockHeight,
root_hash: H, root_hash: H,
@ -291,6 +295,7 @@ impl ScanSummary {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ChainState { pub struct ChainState {
block_height: BlockHeight, block_height: BlockHeight,
block_hash: BlockHash,
final_sapling_tree: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>, final_sapling_tree: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
final_orchard_tree: final_orchard_tree:
@ -299,9 +304,10 @@ pub struct ChainState {
impl ChainState { impl ChainState {
/// Construct a new empty chain state. /// Construct a new empty chain state.
pub fn empty(block_height: BlockHeight) -> Self { pub fn empty(block_height: BlockHeight, block_hash: BlockHash) -> Self {
Self { Self {
block_height, block_height,
block_hash,
final_sapling_tree: Frontier::empty(), final_sapling_tree: Frontier::empty(),
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
final_orchard_tree: Frontier::empty(), final_orchard_tree: Frontier::empty(),
@ -311,6 +317,7 @@ impl ChainState {
/// Construct a new [`ChainState`] from its constituent parts. /// Construct a new [`ChainState`] from its constituent parts.
pub fn new( pub fn new(
block_height: BlockHeight, block_height: BlockHeight,
block_hash: BlockHash,
final_sapling_tree: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>, final_sapling_tree: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
#[cfg(feature = "orchard")] final_orchard_tree: Frontier< #[cfg(feature = "orchard")] final_orchard_tree: Frontier<
orchard::tree::MerkleHashOrchard, orchard::tree::MerkleHashOrchard,
@ -319,6 +326,7 @@ impl ChainState {
) -> Self { ) -> Self {
Self { Self {
block_height, block_height,
block_hash,
final_sapling_tree, final_sapling_tree,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
final_orchard_tree, final_orchard_tree,
@ -330,6 +338,11 @@ impl ChainState {
self.block_height self.block_height
} }
/// Return the hash of the block.
pub fn block_hash(&self) -> BlockHash {
self.block_hash
}
/// Returns the frontier of the Sapling note commitment tree as of the end of the block at /// Returns the frontier of the Sapling note commitment tree as of the end of the block at
/// [`Self::block_height`]. /// [`Self::block_height`].
pub fn final_sapling_tree( pub fn final_sapling_tree(

View File

@ -157,6 +157,13 @@ impl WalletRead for MemoryWalletDb {
todo!() todo!()
} }
fn seed_relevance_to_derived_accounts(
&self,
seed: &SecretVec<u8>,
) -> Result<super::SeedRelevance<Self::AccountId>, Self::Error> {
todo!()
}
fn get_account_for_ufvk( fn get_account_for_ufvk(
&self, &self,
ufvk: &UnifiedFullViewingKey, ufvk: &UnifiedFullViewingKey,

View File

@ -83,8 +83,3 @@ pub use zcash_protocol::{PoolType, ShieldedProtocol};
#[cfg(test)] #[cfg(test)]
#[macro_use] #[macro_use]
extern crate assert_matches; extern crate assert_matches;
#[cfg(all(feature = "orchard", not(zcash_unstable = "orchard")))]
core::compile_error!(
"The `orchard` feature flag requires the `zcash_unstable=\"orchard\"` RUSTFLAG."
);

View File

@ -263,15 +263,19 @@ impl service::TreeState {
pub fn sapling_tree( pub fn sapling_tree(
&self, &self,
) -> io::Result<CommitmentTree<Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>> { ) -> io::Result<CommitmentTree<Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>> {
let sapling_tree_bytes = hex::decode(&self.sapling_tree).map_err(|e| { if self.sapling_tree.is_empty() {
io::Error::new( Ok(CommitmentTree::empty())
io::ErrorKind::InvalidData, } else {
format!("Hex decoding of Sapling tree bytes failed: {:?}", e), let sapling_tree_bytes = hex::decode(&self.sapling_tree).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Hex decoding of Sapling tree bytes failed: {:?}", e),
)
})?;
read_commitment_tree::<Node, _, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>(
&sapling_tree_bytes[..],
) )
})?; }
read_commitment_tree::<Node, _, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>(
&sapling_tree_bytes[..],
)
} }
/// Deserializes and returns the Sapling note commitment tree field of the tree state. /// Deserializes and returns the Sapling note commitment tree field of the tree state.
@ -280,25 +284,43 @@ impl service::TreeState {
&self, &self,
) -> io::Result<CommitmentTree<MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>> ) -> io::Result<CommitmentTree<MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>>
{ {
let orchard_tree_bytes = hex::decode(&self.orchard_tree).map_err(|e| { if self.orchard_tree.is_empty() {
io::Error::new( Ok(CommitmentTree::empty())
io::ErrorKind::InvalidData, } else {
format!("Hex decoding of Orchard tree bytes failed: {:?}", e), let orchard_tree_bytes = hex::decode(&self.orchard_tree).map_err(|e| {
) io::Error::new(
})?; io::ErrorKind::InvalidData,
read_commitment_tree::<MerkleHashOrchard, _, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>( format!("Hex decoding of Orchard tree bytes failed: {:?}", e),
&orchard_tree_bytes[..], )
) })?;
read_commitment_tree::<
MerkleHashOrchard,
_,
{ orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
>(&orchard_tree_bytes[..])
}
} }
/// Parses this tree state into a [`ChainState`] for use with [`scan_cached_blocks`]. /// Parses this tree state into a [`ChainState`] for use with [`scan_cached_blocks`].
/// ///
/// [`scan_cached_blocks`]: crate::data_api::chain::scan_cached_blocks /// [`scan_cached_blocks`]: crate::data_api::chain::scan_cached_blocks
pub fn to_chain_state(&self) -> io::Result<ChainState> { pub fn to_chain_state(&self) -> io::Result<ChainState> {
let mut hash_bytes = hex::decode(&self.hash).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Block hash is not valid hex: {:?}", e),
)
})?;
// Zcashd hex strings for block hashes are byte-reversed.
hash_bytes.reverse();
Ok(ChainState::new( Ok(ChainState::new(
self.height self.height
.try_into() .try_into()
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid block height"))?, .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid block height"))?,
BlockHash::try_from_slice(&hash_bytes).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "Invalid block hash length.")
})?,
self.sapling_tree()?.to_frontier(), self.sapling_tree()?.to_frontier(),
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
self.orchard_tree()?.to_frontier(), self.orchard_tree()?.to_frontier(),

View File

@ -32,10 +32,12 @@ and this library adheres to Rust's notion of
- Added `UnknownZip32Derivation` - Added `UnknownZip32Derivation`
- Added `BadAccountData` - Added `BadAccountData`
- Removed `DiversifierIndexOutOfRange` - Removed `DiversifierIndexOutOfRange`
- Removed `InvalidNoteId`
- `zcash_client_sqlite::wallet`: - `zcash_client_sqlite::wallet`:
- `init::WalletMigrationError` has added variants: - `init::WalletMigrationError` has added variants:
- `WalletMigrationError::AddressGeneration` - `WalletMigrationError::AddressGeneration`
- `WalletMigrationError::CannotRevert` - `WalletMigrationError::CannotRevert`
- `WalletMigrationError::SeedNotRelevant`
- The `v_transactions` and `v_tx_outputs` views now include Orchard notes. - The `v_transactions` and `v_tx_outputs` views now include Orchard notes.
## [0.9.1] - 2024-03-09 ## [0.9.1] - 2024-03-09

View File

@ -45,6 +45,7 @@ tracing.workspace = true
# - Serialization # - Serialization
byteorder.workspace = true byteorder.workspace = true
nonempty.workspace = true
prost.workspace = true prost.workspace = true
group.workspace = true group.workspace = true
jubjub.workspace = true jubjub.workspace = true
@ -82,7 +83,6 @@ bls12_381.workspace = true
incrementalmerkletree = { workspace = true, features = ["test-dependencies"] } incrementalmerkletree = { workspace = true, features = ["test-dependencies"] }
pasta_curves.workspace = true pasta_curves.workspace = true
shardtree = { workspace = true, features = ["legacy-api", "test-dependencies"] } shardtree = { workspace = true, features = ["legacy-api", "test-dependencies"] }
nonempty.workspace = true
orchard = { workspace = true, features = ["test-dependencies"] } orchard = { workspace = true, features = ["test-dependencies"] }
proptest.workspace = true proptest.workspace = true
rand_chacha.workspace = true rand_chacha.workspace = true

View File

@ -30,10 +30,6 @@ pub enum SqliteClientError {
/// The rcm value for a note cannot be decoded to a valid JubJub point. /// The rcm value for a note cannot be decoded to a valid JubJub point.
InvalidNote, InvalidNote,
/// The note id associated with a witness being stored corresponds to a
/// sent note, not a received note.
InvalidNoteId,
/// Illegal attempt to reinitialize an already-initialized wallet database. /// Illegal attempt to reinitialize an already-initialized wallet database.
TableNotEmpty, TableNotEmpty,
@ -138,8 +134,6 @@ impl fmt::Display for SqliteClientError {
} }
SqliteClientError::Protobuf(e) => write!(f, "Failed to parse protobuf-encoded record: {}", e), SqliteClientError::Protobuf(e) => write!(f, "Failed to parse protobuf-encoded record: {}", e),
SqliteClientError::InvalidNote => write!(f, "Invalid note"), SqliteClientError::InvalidNote => write!(f, "Invalid note"),
SqliteClientError::InvalidNoteId =>
write!(f, "The note ID associated with an inserted witness must correspond to a received note."),
SqliteClientError::RequestedRewindInvalid(h, r) => 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_DEPTH, 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_DEPTH, h, r),
SqliteClientError::Bech32DecodeError(e) => write!(f, "{}", e), SqliteClientError::Bech32DecodeError(e) => write!(f, "{}", e),

View File

@ -37,6 +37,7 @@ use maybe_rayon::{
prelude::{IndexedParallelIterator, ParallelIterator}, prelude::{IndexedParallelIterator, ParallelIterator},
slice::ParallelSliceMut, slice::ParallelSliceMut,
}; };
use nonempty::NonEmpty;
use rusqlite::{self, Connection}; use rusqlite::{self, Connection};
use secrecy::{ExposeSecret, SecretVec}; use secrecy::{ExposeSecret, SecretVec};
use shardtree::{error::ShardTreeError, ShardTree}; use shardtree::{error::ShardTreeError, ShardTree};
@ -60,9 +61,9 @@ use zcash_client_backend::{
self, self,
chain::{BlockSource, ChainState, CommitmentTreeRoot}, chain::{BlockSource, ChainState, CommitmentTreeRoot},
scanning::{ScanPriority, ScanRange}, scanning::{ScanPriority, ScanRange},
Account, AccountBirthday, AccountKind, BlockMetadata, DecryptedTransaction, InputSource, Account, AccountBirthday, AccountSource, BlockMetadata, DecryptedTransaction, InputSource,
NullifierQuery, ScannedBlock, SentTransaction, SpendableNotes, WalletCommitmentTrees, NullifierQuery, ScannedBlock, SeedRelevance, SentTransaction, SpendableNotes,
WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
}, },
keys::{ keys::{
AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey, AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey,
@ -316,43 +317,18 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
seed: &SecretVec<u8>, seed: &SecretVec<u8>,
) -> Result<bool, Self::Error> { ) -> Result<bool, Self::Error> {
if let Some(account) = self.get_account(account_id)? { if let Some(account) = self.get_account(account_id)? {
if let AccountKind::Derived { if let AccountSource::Derived {
seed_fingerprint, seed_fingerprint,
account_index, account_index,
} = account.kind() } = account.source()
{ {
let seed_fingerprint_match = wallet::seed_matches_derived_account(
SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
SqliteClientError::BadAccountData(
"Seed must be between 32 and 252 bytes in length.".to_owned(),
)
})? == seed_fingerprint;
let usk = UnifiedSpendingKey::from_seed(
&self.params, &self.params,
&seed.expose_secret()[..], seed,
&seed_fingerprint,
account_index, account_index,
&account.uivk(),
) )
.map_err(|_| SqliteClientError::KeyDerivationError(account_index))?;
// 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.
let ufvk_match = UnifiedAddressRequest::all().map_or(
Ok::<_, Self::Error>(false),
|ua_request| {
Ok(usk
.to_unified_full_viewing_key()
.default_address(ua_request)?
== account.default_address(ua_request)?)
},
)?;
if seed_fingerprint_match != ufvk_match {
// If these mismatch, it suggests database corruption.
return Err(SqliteClientError::CorruptedData(format!("Seed fingerprint match: {seed_fingerprint_match}, ufvk match: {ufvk_match}")));
}
Ok(seed_fingerprint_match && ufvk_match)
} else { } else {
Err(SqliteClientError::UnknownZip32Derivation) Err(SqliteClientError::UnknownZip32Derivation)
} }
@ -362,6 +338,55 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
} }
} }
fn seed_relevance_to_derived_accounts(
&self,
seed: &SecretVec<u8>,
) -> Result<SeedRelevance<Self::AccountId>, Self::Error> {
let mut has_accounts = false;
let mut has_derived = false;
let mut relevant_account_ids = vec![];
for account_id in self.get_account_ids()? {
has_accounts = true;
let account = self.get_account(account_id)?.expect("account ID exists");
// If the account is imported, the seed _might_ be relevant, but the only
// way we could determine that is by brute-forcing the ZIP 32 account
// index space, which we're not going to do. The method name indicates to
// the caller that we only check derived accounts.
if let AccountSource::Derived {
seed_fingerprint,
account_index,
} = account.source()
{
has_derived = true;
if wallet::seed_matches_derived_account(
&self.params,
seed,
&seed_fingerprint,
account_index,
&account.uivk(),
)? {
// The seed is relevant to this account.
relevant_account_ids.push(account_id);
}
}
}
Ok(
if let Some(account_ids) = NonEmpty::from_vec(relevant_account_ids) {
SeedRelevance::Relevant { account_ids }
} else if has_derived {
SeedRelevance::NotRelevant
} else if has_accounts {
SeedRelevance::NoDerivedAccounts
} else {
SeedRelevance::NoAccounts
},
)
}
fn get_account_for_ufvk( fn get_account_for_ufvk(
&self, &self,
ufvk: &UnifiedFullViewingKey, ufvk: &UnifiedFullViewingKey,
@ -527,7 +552,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
let account_id = wallet::add_account( let account_id = wallet::add_account(
wdb.conn.0, wdb.conn.0,
&wdb.params, &wdb.params,
AccountKind::Derived { AccountSource::Derived {
seed_fingerprint, seed_fingerprint,
account_index, account_index,
}, },

View File

@ -6,12 +6,14 @@ use std::{collections::BTreeMap, convert::Infallible};
use std::fs::File; use std::fs::File;
use group::ff::Field; use group::ff::Field;
use incrementalmerkletree::Retention;
use nonempty::NonEmpty; use nonempty::NonEmpty;
use prost::Message; use prost::Message;
use rand_chacha::ChaChaRng; use rand_chacha::ChaChaRng;
use rand_core::{CryptoRng, RngCore, SeedableRng}; use rand_core::{CryptoRng, RngCore, SeedableRng};
use rusqlite::{params, Connection}; use rusqlite::{params, Connection};
use secrecy::{Secret, SecretVec}; use secrecy::{Secret, SecretVec};
use shardtree::error::ShardTreeError;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
#[cfg(feature = "unstable")] #[cfg(feature = "unstable")]
@ -28,13 +30,14 @@ use zcash_client_backend::{
address::Address, address::Address,
data_api::{ data_api::{
self, self,
chain::{scan_cached_blocks, BlockSource, ScanSummary}, chain::{scan_cached_blocks, BlockSource, CommitmentTreeRoot, ScanSummary},
wallet::{ wallet::{
create_proposed_transactions, create_spend_to_address, create_proposed_transactions, create_spend_to_address,
input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector}, input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector},
propose_standard_transfer_to_address, propose_transfer, spend, propose_standard_transfer_to_address, propose_transfer, spend,
}, },
AccountBalance, AccountBirthday, WalletRead, WalletSummary, WalletWrite, AccountBalance, AccountBirthday, WalletCommitmentTrees, WalletRead, WalletSummary,
WalletWrite,
}, },
keys::UnifiedSpendingKey, keys::UnifiedSpendingKey,
proposal::Proposal, proposal::Proposal,
@ -63,6 +66,7 @@ use zcash_primitives::{
zip32::DiversifierIndex, zip32::DiversifierIndex,
}; };
use zcash_protocol::local_consensus::LocalNetwork; use zcash_protocol::local_consensus::LocalNetwork;
use zcash_protocol::value::{ZatBalance, Zatoshis};
use crate::{ use crate::{
chain::init::init_cache_database, chain::init::init_cache_database,
@ -189,7 +193,6 @@ impl<Cache> TestBuilder<Cache> {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct CachedBlock { pub(crate) struct CachedBlock {
hash: BlockHash,
chain_state: ChainState, chain_state: ChainState,
sapling_end_size: u32, sapling_end_size: u32,
orchard_end_size: u32, orchard_end_size: u32,
@ -198,19 +201,13 @@ pub(crate) struct CachedBlock {
impl CachedBlock { impl CachedBlock {
fn none(sapling_activation_height: BlockHeight) -> Self { fn none(sapling_activation_height: BlockHeight) -> Self {
Self { Self {
hash: BlockHash([0; 32]), chain_state: ChainState::empty(sapling_activation_height, BlockHash([0; 32])),
chain_state: ChainState::empty(sapling_activation_height),
sapling_end_size: 0, sapling_end_size: 0,
orchard_end_size: 0, orchard_end_size: 0,
} }
} }
fn at( fn at(chain_state: ChainState, sapling_end_size: u32, orchard_end_size: u32) -> Self {
hash: BlockHash,
chain_state: ChainState,
sapling_end_size: u32,
orchard_end_size: u32,
) -> Self {
assert_eq!( assert_eq!(
chain_state.final_sapling_tree().tree_size() as u32, chain_state.final_sapling_tree().tree_size() as u32,
sapling_end_size sapling_end_size
@ -222,7 +219,6 @@ impl CachedBlock {
); );
Self { Self {
hash,
chain_state, chain_state,
sapling_end_size, sapling_end_size,
orchard_end_size, orchard_end_size,
@ -257,9 +253,9 @@ impl CachedBlock {
}); });
Self { Self {
hash: cb.hash(),
chain_state: ChainState::new( chain_state: ChainState::new(
cb.height(), cb.height(),
cb.hash(),
sapling_final_tree, sapling_final_tree,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard_final_tree, orchard_final_tree,
@ -322,6 +318,67 @@ where
self.cache.insert(&compact_block) self.cache.insert(&compact_block)
} }
/// Ensure that the provided chain state and subtree roots exist in the wallet's note
/// commitment tree(s). This may result in a conflict if either the provided subtree roots or
/// the chain state conflict with existing note commitment tree data.
pub(crate) fn establish_chain_state(
&mut self,
state: ChainState,
prior_sapling_roots: &[CommitmentTreeRoot<sapling::Node>],
#[cfg(feature = "orchard")] prior_orchard_roots: &[CommitmentTreeRoot<MerkleHashOrchard>],
) -> Result<(), ShardTreeError<commitment_tree::Error>> {
self.wallet_mut()
.put_sapling_subtree_roots(0, prior_sapling_roots)?;
#[cfg(feature = "orchard")]
self.wallet_mut()
.put_orchard_subtree_roots(0, prior_orchard_roots)?;
self.wallet_mut().with_sapling_tree_mut(|t| {
t.insert_frontier(
state.final_sapling_tree().clone(),
Retention::Checkpoint {
id: state.block_height(),
is_marked: false,
},
)
})?;
let final_sapling_tree_size = state.final_sapling_tree().tree_size() as u32;
#[cfg(feature = "orchard")]
self.wallet_mut().with_orchard_tree_mut(|t| {
t.insert_frontier(
state.final_orchard_tree().clone(),
Retention::Checkpoint {
id: state.block_height(),
is_marked: false,
},
)
})?;
let _final_orchard_tree_size = 0;
#[cfg(feature = "orchard")]
let _final_orchard_tree_size = state.final_orchard_tree().tree_size() as u32;
self.insert_cached_block(state, final_sapling_tree_size, _final_orchard_tree_size);
Ok(())
}
fn insert_cached_block(
&mut self,
chain_state: ChainState,
sapling_end_size: u32,
orchard_end_size: u32,
) {
self.cached_blocks.insert(
chain_state.block_height(),
CachedBlock {
chain_state,
sapling_end_size,
orchard_end_size,
},
);
}
/// Creates a fake block at the expected next height containing a single output of the /// Creates a fake block at the expected next height containing a single output of the
/// given value, and inserts it into the cache. /// given value, and inserts it into the cache.
pub(crate) fn generate_next_block<Fvk: TestFvk>( pub(crate) fn generate_next_block<Fvk: TestFvk>(
@ -336,7 +393,7 @@ where
let (res, nf) = self.generate_block_at( let (res, nf) = self.generate_block_at(
height, height,
prior_cached_block.hash, prior_cached_block.chain_state.block_hash(),
fvk, fvk,
req, req,
value, value,
@ -375,7 +432,7 @@ where
// we need to generate a new prior cached block that the block to be generated can // we need to generate a new prior cached block that the block to be generated can
// successfully chain from, with the provided tree sizes. // successfully chain from, with the provided tree sizes.
if prior_cached_block.chain_state.block_height() == height - 1 { if prior_cached_block.chain_state.block_height() == height - 1 {
assert_eq!(prev_hash, prior_cached_block.hash); assert_eq!(prev_hash, prior_cached_block.chain_state.block_hash());
} else { } else {
let final_sapling_tree = let final_sapling_tree =
(prior_cached_block.sapling_end_size..initial_sapling_tree_size).fold( (prior_cached_block.sapling_end_size..initial_sapling_tree_size).fold(
@ -399,9 +456,9 @@ where
); );
prior_cached_block = CachedBlock::at( prior_cached_block = CachedBlock::at(
prev_hash,
ChainState::new( ChainState::new(
height - 1, height - 1,
prev_hash,
final_sapling_tree, final_sapling_tree,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
final_orchard_tree, final_orchard_tree,
@ -451,7 +508,7 @@ where
let cb = fake_compact_block_spending( let cb = fake_compact_block_spending(
&self.network(), &self.network(),
height, height,
prior_cached_block.hash, prior_cached_block.chain_state.block_hash(),
note, note,
fvk, fvk,
to.into(), to.into(),
@ -507,7 +564,7 @@ where
let cb = fake_compact_block_from_tx( let cb = fake_compact_block_from_tx(
height, height,
prior_cached_block.hash, prior_cached_block.chain_state.block_hash(),
tx_index, tx_index,
tx, tx,
prior_cached_block.sapling_end_size, prior_cached_block.sapling_end_size,
@ -612,6 +669,11 @@ impl<Cache> TestState<Cache> {
self.db_data.params self.db_data.params
} }
/// Exposes the random number source for the test state
pub(crate) fn rng(&mut self) -> &mut ChaChaRng {
&mut self.rng
}
/// Convenience method for obtaining the Sapling activation height for the network under test. /// Convenience method for obtaining the Sapling activation height for the network under test.
pub(crate) fn sapling_activation_height(&self) -> BlockHeight { pub(crate) fn sapling_activation_height(&self) -> BlockHeight {
self.db_data self.db_data
@ -958,6 +1020,105 @@ impl<Cache> TestState<Cache> {
) )
.unwrap() .unwrap()
} }
/// Returns a vector of transaction summaries
pub(crate) fn get_tx_history(
&self,
) -> Result<Vec<TransactionSummary<AccountId>>, SqliteClientError> {
let mut stmt = self.wallet().conn.prepare_cached(
"SELECT *
FROM v_transactions
ORDER BY mined_height DESC, tx_index DESC",
)?;
let results = stmt
.query_and_then::<TransactionSummary<AccountId>, SqliteClientError, _, _>([], |row| {
Ok(TransactionSummary {
account_id: AccountId(row.get("account_id")?),
txid: TxId::from_bytes(row.get("txid")?),
expiry_height: row
.get::<_, Option<u32>>("expiry_height")?
.map(BlockHeight::from),
mined_height: row
.get::<_, Option<u32>>("mined_height")?
.map(BlockHeight::from),
account_value_delta: ZatBalance::from_i64(row.get("account_balance_delta")?)?,
fee_paid: row
.get::<_, Option<i64>>("fee_paid")?
.map(Zatoshis::from_nonnegative_i64)
.transpose()?,
has_change: row.get("has_change")?,
sent_note_count: row.get("sent_note_count")?,
received_note_count: row.get("received_note_count")?,
memo_count: row.get("memo_count")?,
expired_unmined: row.get("expired_unmined")?,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(results)
}
}
pub(crate) struct TransactionSummary<AccountId> {
account_id: AccountId,
txid: TxId,
expiry_height: Option<BlockHeight>,
mined_height: Option<BlockHeight>,
account_value_delta: ZatBalance,
fee_paid: Option<Zatoshis>,
has_change: bool,
sent_note_count: usize,
received_note_count: usize,
memo_count: usize,
expired_unmined: bool,
}
#[allow(dead_code)]
impl<AccountId> TransactionSummary<AccountId> {
pub(crate) fn account_id(&self) -> &AccountId {
&self.account_id
}
pub(crate) fn txid(&self) -> TxId {
self.txid
}
pub(crate) fn expiry_height(&self) -> Option<BlockHeight> {
self.expiry_height
}
pub(crate) fn mined_height(&self) -> Option<BlockHeight> {
self.mined_height
}
pub(crate) fn account_value_delta(&self) -> ZatBalance {
self.account_value_delta
}
pub(crate) fn fee_paid(&self) -> Option<Zatoshis> {
self.fee_paid
}
pub(crate) fn has_change(&self) -> bool {
self.has_change
}
pub(crate) fn sent_note_count(&self) -> usize {
self.sent_note_count
}
pub(crate) fn received_note_count(&self) -> usize {
self.received_note_count
}
pub(crate) fn expired_unmined(&self) -> bool {
self.expired_unmined
}
pub(crate) fn memo_count(&self) -> usize {
self.memo_count
}
} }
/// Trait used by tests that require a full viewing key. /// Trait used by tests that require a full viewing key.

View File

@ -271,6 +271,9 @@ pub(crate) fn send_single_step_proposed_transfer<T: ShieldedPoolTester>() {
.get_memo(NoteId::new(sent_tx_id, T::SHIELDED_PROTOCOL, 12345)), .get_memo(NoteId::new(sent_tx_id, T::SHIELDED_PROTOCOL, 12345)),
Ok(None) Ok(None)
); );
let tx_history = st.get_tx_history().unwrap();
assert_eq!(tx_history.len(), 2);
} }
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]

View File

@ -66,6 +66,7 @@
use incrementalmerkletree::Retention; use incrementalmerkletree::Retention;
use rusqlite::{self, named_params, params, OptionalExtension}; use rusqlite::{self, named_params, params, OptionalExtension};
use secrecy::{ExposeSecret, SecretVec};
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree}; use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
use zip32::fingerprint::SeedFingerprint; use zip32::fingerprint::SeedFingerprint;
@ -75,15 +76,16 @@ use std::io::{self, Cursor};
use std::num::NonZeroU32; use std::num::NonZeroU32;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use tracing::debug; use tracing::debug;
use zcash_address::unified::{Encoding, Ivk, Uivk}; use zcash_keys::keys::{
use zcash_keys::keys::{AddressGenerationError, UnifiedAddressRequest}; AddressGenerationError, UnifiedAddressRequest, UnifiedIncomingViewingKey, UnifiedSpendingKey,
};
use zcash_client_backend::{ use zcash_client_backend::{
address::{Address, UnifiedAddress}, address::{Address, UnifiedAddress},
data_api::{ data_api::{
scanning::{ScanPriority, ScanRange}, scanning::{ScanPriority, ScanRange},
AccountBalance, AccountBirthday, AccountKind, BlockMetadata, Ratio, SentTransactionOutput, AccountBalance, AccountBirthday, AccountSource, BlockMetadata, Ratio,
WalletSummary, SAPLING_SHARD_HEIGHT, SentTransactionOutput, WalletSummary, SAPLING_SHARD_HEIGHT,
}, },
encoding::AddressCodec, encoding::AddressCodec,
keys::UnifiedFullViewingKey, keys::UnifiedFullViewingKey,
@ -119,6 +121,7 @@ use {
crate::UtxoId, crate::UtxoId,
rusqlite::Row, rusqlite::Row,
std::collections::BTreeSet, std::collections::BTreeSet,
zcash_address::unified::{Encoding, Ivk, Uivk},
zcash_client_backend::wallet::{TransparentAddressMetadata, WalletTransparentOutput}, zcash_client_backend::wallet::{TransparentAddressMetadata, WalletTransparentOutput},
zcash_primitives::{ zcash_primitives::{
legacy::{ legacy::{
@ -139,13 +142,13 @@ pub(crate) mod scanning;
pub(crate) const BLOCK_SAPLING_FRONTIER_ABSENT: &[u8] = &[0x0]; pub(crate) const BLOCK_SAPLING_FRONTIER_ABSENT: &[u8] = &[0x0];
fn parse_account_kind( fn parse_account_source(
account_kind: u32, account_kind: u32,
hd_seed_fingerprint: Option<[u8; 32]>, hd_seed_fingerprint: Option<[u8; 32]>,
hd_account_index: Option<u32>, hd_account_index: Option<u32>,
) -> Result<AccountKind, SqliteClientError> { ) -> Result<AccountSource, SqliteClientError> {
match (account_kind, hd_seed_fingerprint, hd_account_index) { match (account_kind, hd_seed_fingerprint, hd_account_index) {
(0, Some(seed_fp), Some(account_index)) => Ok(AccountKind::Derived { (0, Some(seed_fp), Some(account_index)) => Ok(AccountSource::Derived {
seed_fingerprint: SeedFingerprint::from_bytes(seed_fp), seed_fingerprint: SeedFingerprint::from_bytes(seed_fp),
account_index: zip32::AccountId::try_from(account_index).map_err(|_| { account_index: zip32::AccountId::try_from(account_index).map_err(|_| {
SqliteClientError::CorruptedData( SqliteClientError::CorruptedData(
@ -153,7 +156,7 @@ fn parse_account_kind(
) )
})?, })?,
}), }),
(1, None, None) => Ok(AccountKind::Imported), (1, None, None) => Ok(AccountSource::Imported),
(0, None, None) | (1, Some(_), Some(_)) => Err(SqliteClientError::CorruptedData( (0, None, None) | (1, Some(_), Some(_)) => Err(SqliteClientError::CorruptedData(
"Wallet DB account_kind constraint violated".to_string(), "Wallet DB account_kind constraint violated".to_string(),
)), )),
@ -163,10 +166,10 @@ fn parse_account_kind(
} }
} }
fn account_kind_code(value: AccountKind) -> u32 { fn account_kind_code(value: AccountSource) -> u32 {
match value { match value {
AccountKind::Derived { .. } => 0, AccountSource::Derived { .. } => 0,
AccountKind::Imported => 1, AccountSource::Imported => 1,
} }
} }
@ -183,14 +186,14 @@ pub(crate) enum ViewingKey {
/// ///
/// Accounts that have this kind of viewing key cannot be used in wallet contexts, /// Accounts that have this kind of viewing key cannot be used in wallet contexts,
/// because they are unable to maintain an accurate balance. /// because they are unable to maintain an accurate balance.
Incoming(Uivk), Incoming(Box<UnifiedIncomingViewingKey>),
} }
/// An account stored in a `zcash_client_sqlite` database. /// An account stored in a `zcash_client_sqlite` database.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Account { pub struct Account {
account_id: AccountId, account_id: AccountId,
kind: AccountKind, kind: AccountSource,
viewing_key: ViewingKey, viewing_key: ViewingKey,
} }
@ -206,7 +209,7 @@ impl Account {
) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> { ) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> {
match &self.viewing_key { match &self.viewing_key {
ViewingKey::Full(ufvk) => ufvk.default_address(request), ViewingKey::Full(ufvk) => ufvk.default_address(request),
ViewingKey::Incoming(_uivk) => todo!(), ViewingKey::Incoming(uivk) => uivk.default_address(request),
} }
} }
} }
@ -216,13 +219,17 @@ impl zcash_client_backend::data_api::Account<AccountId> for Account {
self.account_id self.account_id
} }
fn kind(&self) -> AccountKind { fn source(&self) -> AccountSource {
self.kind self.kind
} }
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> { fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
self.viewing_key.ufvk() self.viewing_key.ufvk()
} }
fn uivk(&self) -> UnifiedIncomingViewingKey {
self.viewing_key.uivk()
}
} }
impl ViewingKey { impl ViewingKey {
@ -233,14 +240,57 @@ impl ViewingKey {
} }
} }
fn uivk_str(&self, params: &impl Parameters) -> Result<String, SqliteClientError> { fn uivk(&self) -> UnifiedIncomingViewingKey {
match self { match self {
ViewingKey::Full(ufvk) => ufvk_to_uivk(ufvk, params), ViewingKey::Full(ufvk) => ufvk.as_ref().to_unified_incoming_viewing_key(),
ViewingKey::Incoming(uivk) => Ok(uivk.encode(&params.network_type())), ViewingKey::Incoming(uivk) => uivk.as_ref().clone(),
} }
} }
} }
pub(crate) fn seed_matches_derived_account<P: consensus::Parameters>(
params: &P,
seed: &SecretVec<u8>,
seed_fingerprint: &SeedFingerprint,
account_index: zip32::AccountId,
uivk: &UnifiedIncomingViewingKey,
) -> Result<bool, SqliteClientError> {
let seed_fingerprint_match =
&SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
SqliteClientError::BadAccountData(
"Seed must be between 32 and 252 bytes in length.".to_owned(),
)
})? == seed_fingerprint;
// 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.
let uivk_match =
match UnifiedSpendingKey::from_seed(params, &seed.expose_secret()[..], account_index) {
// If we can't derive a USK from the given seed with the account's ZIP 32
// account index, then we immediately know the UIVK won't match because wallet
// accounts are required to have a known UIVK.
Err(_) => false,
Ok(usk) => UnifiedAddressRequest::all().map_or(
Ok::<_, SqliteClientError>(false),
|ua_request| {
Ok(usk
.to_unified_full_viewing_key()
.default_address(ua_request)?
== uivk.default_address(ua_request)?)
},
)?,
};
if seed_fingerprint_match != uivk_match {
// If these mismatch, it suggests database corruption.
Err(SqliteClientError::CorruptedData(format!(
"Seed fingerprint match: {seed_fingerprint_match}, uivk match: {uivk_match}"
)))
} else {
Ok(seed_fingerprint_match && uivk_match)
}
}
pub(crate) fn pool_code(pool_type: PoolType) -> i64 { pub(crate) fn pool_code(pool_type: PoolType) -> i64 {
// These constants are *incidentally* shared with the typecodes // These constants are *incidentally* shared with the typecodes
// for unified addresses, but this is exclusively an internal // for unified addresses, but this is exclusively an internal
@ -296,44 +346,19 @@ pub(crate) fn max_zip32_account_index(
) )
} }
pub(crate) fn ufvk_to_uivk<P: consensus::Parameters>(
ufvk: &UnifiedFullViewingKey,
params: &P,
) -> Result<String, SqliteClientError> {
let mut ivks: Vec<Ivk> = Vec::new();
if let Some(orchard) = ufvk.orchard() {
ivks.push(Ivk::Orchard(orchard.to_ivk(Scope::External).to_bytes()));
}
if let Some(sapling) = ufvk.sapling() {
let ivk = sapling.to_external_ivk();
ivks.push(Ivk::Sapling(ivk.to_bytes()));
}
#[cfg(feature = "transparent-inputs")]
if let Some(tfvk) = ufvk.transparent() {
let tivk = tfvk.derive_external_ivk()?;
ivks.push(Ivk::P2pkh(tivk.serialize().try_into().map_err(|_| {
SqliteClientError::BadAccountData("Unable to serialize transparent IVK.".to_string())
})?));
}
let uivk = zcash_address::unified::Uivk::try_from_items(ivks)
.map_err(|e| SqliteClientError::BadAccountData(format!("Unable to derive UIVK: {}", e)))?;
Ok(uivk.encode(&params.network_type()))
}
pub(crate) fn add_account<P: consensus::Parameters>( pub(crate) fn add_account<P: consensus::Parameters>(
conn: &rusqlite::Transaction, conn: &rusqlite::Transaction,
params: &P, params: &P,
kind: AccountKind, kind: AccountSource,
viewing_key: ViewingKey, viewing_key: ViewingKey,
birthday: AccountBirthday, birthday: AccountBirthday,
) -> Result<AccountId, SqliteClientError> { ) -> Result<AccountId, SqliteClientError> {
let (hd_seed_fingerprint, hd_account_index) = match kind { let (hd_seed_fingerprint, hd_account_index) = match kind {
AccountKind::Derived { AccountSource::Derived {
seed_fingerprint, seed_fingerprint,
account_index, account_index,
} => (Some(seed_fingerprint), Some(account_index)), } => (Some(seed_fingerprint), Some(account_index)),
AccountKind::Imported => (None, None), AccountSource::Imported => (None, None),
}; };
let orchard_item = viewing_key let orchard_item = viewing_key
@ -370,7 +395,7 @@ pub(crate) fn add_account<P: consensus::Parameters>(
":hd_seed_fingerprint": hd_seed_fingerprint.as_ref().map(|fp| fp.to_bytes()), ":hd_seed_fingerprint": hd_seed_fingerprint.as_ref().map(|fp| fp.to_bytes()),
":hd_account_index": hd_account_index.map(u32::from), ":hd_account_index": hd_account_index.map(u32::from),
":ufvk": viewing_key.ufvk().map(|ufvk| ufvk.encode(params)), ":ufvk": viewing_key.ufvk().map(|ufvk| ufvk.encode(params)),
":uivk": viewing_key.uivk_str(params)?, ":uivk": viewing_key.uivk().encode(params),
":orchard_fvk_item_cache": orchard_item, ":orchard_fvk_item_cache": orchard_item,
":sapling_fvk_item_cache": sapling_item, ":sapling_fvk_item_cache": sapling_item,
":p2pkh_fvk_item_cache": transparent_item, ":p2pkh_fvk_item_cache": transparent_item,
@ -734,7 +759,7 @@ pub(crate) fn get_account_for_ufvk<P: consensus::Parameters>(
], ],
|row| { |row| {
let account_id = row.get::<_, u32>(0).map(AccountId)?; let account_id = row.get::<_, u32>(0).map(AccountId)?;
let kind = parse_account_kind(row.get(1)?, row.get(2)?, row.get(3)?)?; let kind = parse_account_source(row.get(1)?, row.get(2)?, row.get(3)?)?;
// We looked up the account by FVK components, so the UFVK column must be // We looked up the account by FVK components, so the UFVK column must be
// non-null. // non-null.
@ -802,7 +827,7 @@ pub(crate) fn get_derived_account<P: consensus::Parameters>(
}?; }?;
Ok(Account { Ok(Account {
account_id, account_id,
kind: AccountKind::Derived { kind: AccountSource::Derived {
seed_fingerprint: *seed, seed_fingerprint: *seed,
account_index, account_index,
}, },
@ -1511,7 +1536,7 @@ pub(crate) fn get_account<P: Parameters>(
let row = result.next()?; let row = result.next()?;
match row { match row {
Some(row) => { Some(row) => {
let kind = parse_account_kind( let kind = parse_account_source(
row.get("account_kind")?, row.get("account_kind")?,
row.get("hd_seed_fingerprint")?, row.get("hd_seed_fingerprint")?,
row.get("hd_account_index")?, row.get("hd_account_index")?,
@ -1525,15 +1550,10 @@ pub(crate) fn get_account<P: Parameters>(
)) ))
} else { } else {
let uivk_str: String = row.get("uivk")?; let uivk_str: String = row.get("uivk")?;
let (network, uivk) = Uivk::decode(&uivk_str).map_err(|e| { ViewingKey::Incoming(Box::new(
SqliteClientError::CorruptedData(format!("Failure to decode UIVK: {e}")) UnifiedIncomingViewingKey::decode(params, &uivk_str[..])
})?; .map_err(SqliteClientError::BadAccountData)?,
if network != params.network_type() { ))
return Err(SqliteClientError::CorruptedData(
"UIVK network type does not match wallet network type".to_string(),
));
}
ViewingKey::Incoming(uivk)
}; };
Ok(Some(Account { Ok(Some(Account {
@ -2700,7 +2720,7 @@ mod tests {
use std::num::NonZeroU32; use std::num::NonZeroU32;
use sapling::zip32::ExtendedSpendingKey; use sapling::zip32::ExtendedSpendingKey;
use zcash_client_backend::data_api::{AccountBirthday, AccountKind, WalletRead}; use zcash_client_backend::data_api::{AccountBirthday, AccountSource, WalletRead};
use zcash_primitives::{block::BlockHash, transaction::components::amount::NonNegativeAmount}; use zcash_primitives::{block::BlockHash, transaction::components::amount::NonNegativeAmount};
use crate::{ use crate::{
@ -2857,7 +2877,7 @@ mod tests {
let expected_account_index = zip32::AccountId::try_from(0).unwrap(); let expected_account_index = zip32::AccountId::try_from(0).unwrap();
assert_matches!( assert_matches!(
account_parameters.kind, account_parameters.kind,
AccountKind::Derived{account_index, ..} if account_index == expected_account_index AccountSource::Derived{account_index, ..} if account_index == expected_account_index
); );
} }

View File

@ -1,6 +1,7 @@
//! Functions for initializing the various databases. //! Functions for initializing the various databases.
use std::fmt; use std::fmt;
use std::rc::Rc;
use schemer::{Migrator, MigratorError}; use schemer::{Migrator, MigratorError};
use schemer_rusqlite::RusqliteAdapter; use schemer_rusqlite::RusqliteAdapter;
@ -8,11 +9,14 @@ use secrecy::SecretVec;
use shardtree::error::ShardTreeError; use shardtree::error::ShardTreeError;
use uuid::Uuid; use uuid::Uuid;
use zcash_client_backend::keys::AddressGenerationError; use zcash_client_backend::{
data_api::{SeedRelevance, WalletRead},
keys::AddressGenerationError,
};
use zcash_primitives::{consensus, transaction::components::amount::BalanceError}; use zcash_primitives::{consensus, transaction::components::amount::BalanceError};
use super::commitment_tree; use super::commitment_tree;
use crate::WalletDb; use crate::{error::SqliteClientError, WalletDb};
mod migrations; mod migrations;
@ -21,6 +25,17 @@ pub enum WalletMigrationError {
/// The seed is required for the migration. /// The seed is required for the migration.
SeedRequired, SeedRequired,
/// A seed was provided that is not relevant to any of the accounts within the wallet.
///
/// Specifically, it is not relevant to any account for which [`Account::source`] is
/// [`AccountSource::Derived`]. We do not check whether the seed is relevant to any
/// imported account, because that would require brute-forcing the ZIP 32 account
/// index space.
///
/// [`Account::source`]: zcash_client_backend::data_api::Account::source
/// [`AccountSource::Derived`]: zcash_client_backend::data_api::AccountSource::Derived
SeedNotRelevant,
/// Decoding of an existing value from its serialized form has failed. /// Decoding of an existing value from its serialized form has failed.
CorruptedData(String), CorruptedData(String),
@ -73,6 +88,12 @@ impl fmt::Display for WalletMigrationError {
"The wallet seed is required in order to update the database." "The wallet seed is required in order to update the database."
) )
} }
WalletMigrationError::SeedNotRelevant => {
write!(
f,
"The provided seed is not relevant to any derived accounts in the database."
)
}
WalletMigrationError::CorruptedData(reason) => { WalletMigrationError::CorruptedData(reason) => {
write!(f, "Wallet database is corrupted: {}", reason) write!(f, "Wallet database is corrupted: {}", reason)
} }
@ -101,6 +122,62 @@ impl std::error::Error for WalletMigrationError {
} }
} }
/// Helper to enable calling regular `WalletDb` methods inside the migration code.
///
/// In this context we can know the full set of errors that are generated by any call we
/// make, so we mark errors as unreachable instead of adding new `WalletMigrationError`
/// variants.
fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> WalletMigrationError {
match e {
SqliteClientError::CorruptedData(e) => WalletMigrationError::CorruptedData(e),
SqliteClientError::Protobuf(e) => WalletMigrationError::CorruptedData(e.to_string()),
SqliteClientError::InvalidNote => {
WalletMigrationError::CorruptedData("invalid note".into())
}
SqliteClientError::Bech32DecodeError(e) => {
WalletMigrationError::CorruptedData(e.to_string())
}
#[cfg(feature = "transparent-inputs")]
SqliteClientError::HdwalletError(e) => WalletMigrationError::CorruptedData(e.to_string()),
SqliteClientError::TransparentAddress(e) => {
WalletMigrationError::CorruptedData(e.to_string())
}
SqliteClientError::DbError(e) => WalletMigrationError::DbError(e),
SqliteClientError::Io(e) => WalletMigrationError::CorruptedData(e.to_string()),
SqliteClientError::InvalidMemo(e) => WalletMigrationError::CorruptedData(e.to_string()),
SqliteClientError::AddressGeneration(e) => WalletMigrationError::AddressGeneration(e),
SqliteClientError::BadAccountData(e) => WalletMigrationError::CorruptedData(e),
SqliteClientError::CommitmentTree(e) => WalletMigrationError::CommitmentTree(e),
SqliteClientError::UnsupportedPoolType(pool) => WalletMigrationError::CorruptedData(
format!("Wallet DB contains unsupported pool type {}", pool),
),
SqliteClientError::BalanceError(e) => WalletMigrationError::BalanceError(e),
SqliteClientError::TableNotEmpty => unreachable!("wallet already initialized"),
SqliteClientError::BlockConflict(_)
| SqliteClientError::NonSequentialBlocks
| SqliteClientError::RequestedRewindInvalid(_, _)
| SqliteClientError::KeyDerivationError(_)
| SqliteClientError::AccountIdDiscontinuity
| SqliteClientError::AccountIdOutOfRange
| SqliteClientError::CacheMiss(_) => {
unreachable!("we only call WalletRead methods; mutations can't occur")
}
#[cfg(feature = "transparent-inputs")]
SqliteClientError::AddressNotRecognized(_) => {
unreachable!("we only call WalletRead methods; mutations can't occur")
}
SqliteClientError::AccountUnknown => {
unreachable!("all accounts are known in migration context")
}
SqliteClientError::UnknownZip32Derivation => {
unreachable!("we don't call methods that require operating on imported accounts")
}
SqliteClientError::ChainHeightUnknown => {
unreachable!("we don't call methods that require a known chain height")
}
}
}
/// Sets up the internal structure of the data database. /// Sets up the internal structure of the data database.
/// ///
/// This procedure will automatically perform migration operations to update the wallet database to /// This procedure will automatically perform migration operations to update the wallet database to
@ -109,26 +186,67 @@ impl std::error::Error for WalletMigrationError {
/// operation of this procedure is idempotent, so it is safe (though not required) to invoke this /// operation of this procedure is idempotent, so it is safe (though not required) to invoke this
/// operation every time the wallet is opened. /// operation every time the wallet is opened.
/// ///
/// In order to correctly apply migrations to accounts derived from a seed, sometimes the
/// optional `seed` argument is required. This function should first be invoked with
/// `seed` set to `None`; if a pending migration requires the seed, the function returns
/// `Err(schemer::MigratorError::Migration { error: WalletMigrationError::SeedRequired, .. })`.
/// The caller can then re-call this function with the necessary seed.
///
/// > Note that currently only one seed can be provided; as such, wallets containing
/// > accounts derived from several different seeds are unsupported, and will result in an
/// > error. Support for multi-seed wallets is being tracked in [zcash/librustzcash#1284].
///
/// When the `seed` argument is provided, the seed is checked against the database for
/// _relevance_: if any account in the wallet for which [`Account::source`] is
/// [`AccountSource::Derived`] can be derived from the given seed, the seed is relevant to
/// the wallet. If the given seed is not relevant, the function returns
/// `Err(schemer::MigratorError::Migration { error: WalletMigrationError::SeedNotRelevant, .. })`
/// or `Err(schemer::MigratorError::Adapter(WalletMigrationError::SeedNotRelevant))`.
///
/// We do not check whether the seed is relevant to any imported account, because that
/// would require brute-forcing the ZIP 32 account index space. Consequentially, imported
/// accounts are not migrated.
///
/// It is safe to use a wallet database previously created without the ability to create /// It is safe to use a wallet database previously created without the ability to create
/// transparent spends with a build that enables transparent spends (via use of the /// transparent spends with a build that enables transparent spends (via use of the
/// `transparent-inputs` feature flag.) The reverse is unsafe, as wallet balance calculations would /// `transparent-inputs` feature flag.) The reverse is unsafe, as wallet balance calculations would
/// ignore the transparent UTXOs already controlled by the wallet. /// ignore the transparent UTXOs already controlled by the wallet.
/// ///
/// [zcash/librustzcash#1284]: https://github.com/zcash/librustzcash/issues/1284
/// [`Account::source`]: zcash_client_backend::data_api::Account::source
/// [`AccountSource::Derived`]: zcash_client_backend::data_api::AccountSource::Derived
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// use secrecy::Secret; /// # use std::error::Error;
/// use tempfile::NamedTempFile; /// # use secrecy::SecretVec;
/// # use tempfile::NamedTempFile;
/// use zcash_primitives::consensus::Network; /// use zcash_primitives::consensus::Network;
/// use zcash_client_sqlite::{ /// use zcash_client_sqlite::{
/// WalletDb, /// WalletDb,
/// wallet::init::init_wallet_db, /// wallet::init::{WalletMigrationError, init_wallet_db},
/// }; /// };
/// ///
/// let data_file = NamedTempFile::new().unwrap(); /// # fn main() -> Result<(), Box<dyn Error>> {
/// let mut db = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); /// # let data_file = NamedTempFile::new().unwrap();
/// init_wallet_db(&mut db, Some(Secret::new(vec![]))).unwrap(); /// # let get_data_db_path = || data_file.path();
/// # let load_seed = || -> Result<_, String> { Ok(SecretVec::new(vec![])) };
/// let mut db = WalletDb::for_path(get_data_db_path(), Network::TestNetwork)?;
/// match init_wallet_db(&mut db, None) {
/// Err(e)
/// if matches!(
/// e.source().and_then(|e| e.downcast_ref()),
/// Some(&WalletMigrationError::SeedRequired)
/// ) =>
/// {
/// let seed = load_seed()?;
/// init_wallet_db(&mut db, Some(seed))
/// }
/// res => res,
/// }?;
/// # Ok(())
/// # }
/// ``` /// ```
// TODO: It would be possible to make the transition from providing transparent support to no // 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 // longer providing transparent support safe, by including a migration that verifies that no
@ -148,6 +266,8 @@ fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
seed: Option<SecretVec<u8>>, seed: Option<SecretVec<u8>>,
target_migrations: &[Uuid], target_migrations: &[Uuid],
) -> Result<(), MigratorError<WalletMigrationError>> { ) -> Result<(), MigratorError<WalletMigrationError>> {
let seed = seed.map(Rc::new);
// Turn off foreign keys, and ensure that table replacement/modification // Turn off foreign keys, and ensure that table replacement/modification
// does not break views // does not break views
wdb.conn wdb.conn
@ -161,7 +281,7 @@ fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
let mut migrator = Migrator::new(adapter); let mut migrator = Migrator::new(adapter);
migrator migrator
.register_multiple(migrations::all_migrations(&wdb.params, seed)) .register_multiple(migrations::all_migrations(&wdb.params, seed.clone()))
.expect("Wallet migration registration should have been successful."); .expect("Wallet migration registration should have been successful.");
if target_migrations.is_empty() { if target_migrations.is_empty() {
migrator.up(None)?; migrator.up(None)?;
@ -173,6 +293,24 @@ fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
wdb.conn wdb.conn
.execute("PRAGMA foreign_keys = ON", []) .execute("PRAGMA foreign_keys = ON", [])
.map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?; .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?;
// Now that the migration succeeded, check whether the seed is relevant to the wallet.
if let Some(seed) = seed {
match wdb
.seed_relevance_to_derived_accounts(&seed)
.map_err(sqlite_client_error_to_wallet_migration_error)?
{
SeedRelevance::Relevant { .. } => (),
// Every seed is relevant to a wallet with no accounts; this is most likely a
// new wallet database being initialized for the first time.
SeedRelevance::NoAccounts => (),
// No seed is relevant to a wallet that only has imported accounts.
SeedRelevance::NotRelevant | SeedRelevance::NoDerivedAccounts => {
return Err(WalletMigrationError::SeedNotRelevant.into())
}
}
}
Ok(()) Ok(())
} }
@ -204,7 +342,7 @@ mod tests {
testing::TestBuilder, wallet::scanning::priority_code, WalletDb, DEFAULT_UA_REQUEST, testing::TestBuilder, wallet::scanning::priority_code, WalletDb, DEFAULT_UA_REQUEST,
}; };
use super::init_wallet_db; use super::{init_wallet_db, WalletMigrationError};
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use {
@ -552,14 +690,14 @@ mod tests {
// v_received_notes // v_received_notes
"CREATE VIEW v_received_notes AS "CREATE VIEW v_received_notes AS
SELECT SELECT
id, sapling_received_notes.id AS id_within_pool_table,
tx, sapling_received_notes.tx,
2 AS pool, 2 AS pool,
sapling_received_notes.output_index AS output_index, sapling_received_notes.output_index AS output_index,
account_id, account_id,
value, sapling_received_notes.value,
is_change, is_change,
memo, sapling_received_notes.memo,
spent, spent,
sent_notes.id AS sent_note_id sent_notes.id AS sent_note_id
FROM sapling_received_notes FROM sapling_received_notes
@ -568,14 +706,14 @@ mod tests {
(sapling_received_notes.tx, 2, sapling_received_notes.output_index) (sapling_received_notes.tx, 2, sapling_received_notes.output_index)
UNION UNION
SELECT SELECT
id, orchard_received_notes.id AS id_within_pool_table,
tx, orchard_received_notes.tx,
3 AS pool, 3 AS pool,
orchard_received_notes.action_index AS output_index, orchard_received_notes.action_index AS output_index,
account_id, account_id,
value, orchard_received_notes.value,
is_change, is_change,
memo, orchard_received_notes.memo,
spent, spent,
sent_notes.id AS sent_note_id sent_notes.id AS sent_note_id
FROM orchard_received_notes FROM orchard_received_notes
@ -650,11 +788,12 @@ mod tests {
"CREATE VIEW v_transactions AS "CREATE VIEW v_transactions AS
WITH WITH
notes AS ( notes AS (
SELECT v_received_notes.id AS id, -- Shielded notes received in this transaction
v_received_notes.account_id AS account_id, SELECT v_received_notes.account_id AS account_id,
transactions.block AS block, transactions.block AS block,
transactions.txid AS txid, transactions.txid AS txid,
v_received_notes.pool AS pool, v_received_notes.pool AS pool,
id_within_pool_table,
v_received_notes.value AS value, v_received_notes.value AS value,
CASE CASE
WHEN v_received_notes.is_change THEN 1 WHEN v_received_notes.is_change THEN 1
@ -673,22 +812,24 @@ mod tests {
JOIN transactions JOIN transactions
ON transactions.id_tx = v_received_notes.tx ON transactions.id_tx = v_received_notes.tx
UNION UNION
SELECT utxos.id AS id, -- Transparent TXOs received in this transaction
utxos.received_by_account_id AS account_id, SELECT utxos.received_by_account_id AS account_id,
utxos.height AS block, utxos.height AS block,
utxos.prevout_txid AS txid, utxos.prevout_txid AS txid,
0 AS pool, 0 AS pool,
utxos.id AS id_within_pool_table,
utxos.value_zat AS value, utxos.value_zat AS value,
0 AS is_change, 0 AS is_change,
1 AS received_count, 1 AS received_count,
0 AS memo_present 0 AS memo_present
FROM utxos FROM utxos
UNION UNION
SELECT v_received_notes.id AS id, -- Shielded notes spent in this transaction
v_received_notes.account_id AS account_id, SELECT v_received_notes.account_id AS account_id,
transactions.block AS block, transactions.block AS block,
transactions.txid AS txid, transactions.txid AS txid,
v_received_notes.pool AS pool, v_received_notes.pool AS pool,
id_within_pool_table,
-v_received_notes.value AS value, -v_received_notes.value AS value,
0 AS is_change, 0 AS is_change,
0 AS received_count, 0 AS received_count,
@ -697,11 +838,12 @@ mod tests {
JOIN transactions JOIN transactions
ON transactions.id_tx = v_received_notes.spent ON transactions.id_tx = v_received_notes.spent
UNION UNION
SELECT utxos.id AS id, -- Transparent TXOs spent in this transaction
utxos.received_by_account_id AS account_id, SELECT utxos.received_by_account_id AS account_id,
transactions.block AS block, transactions.block AS block,
transactions.txid AS txid, transactions.txid AS txid,
0 AS pool, 0 AS pool,
utxos.id AS id_within_pool_table,
-utxos.value_zat AS value, -utxos.value_zat AS value,
0 AS is_change, 0 AS is_change,
0 AS received_count, 0 AS received_count,
@ -710,6 +852,8 @@ mod tests {
JOIN transactions JOIN transactions
ON transactions.id_tx = utxos.spent_in_tx ON transactions.id_tx = utxos.spent_in_tx
), ),
-- Obtain a count of the notes that the wallet created in each transaction,
-- not counting change notes.
sent_note_counts AS ( sent_note_counts AS (
SELECT sent_notes.from_account_id AS account_id, SELECT sent_notes.from_account_id AS account_id,
transactions.txid AS txid, transactions.txid AS txid,
@ -1282,15 +1426,23 @@ mod tests {
#[test] #[test]
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
fn account_produces_expected_ua_sequence() { fn account_produces_expected_ua_sequence() {
use zcash_client_backend::data_api::{AccountBirthday, AccountKind, WalletRead}; use zcash_client_backend::data_api::{AccountBirthday, AccountSource, WalletRead};
let network = Network::MainNetwork; let network = Network::MainNetwork;
let data_file = NamedTempFile::new().unwrap(); let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap(); let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap();
assert_matches!(init_wallet_db(&mut db_data, None), Ok(_));
// Prior to adding any accounts, every seed phrase is relevant to the wallet.
let seed = test_vectors::UNIFIED[0].root_seed; let seed = test_vectors::UNIFIED[0].root_seed;
let other_seed = [7; 32];
assert_matches!( assert_matches!(
init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))), init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
Ok(_) Ok(())
);
assert_matches!(
init_wallet_db(&mut db_data, Some(Secret::new(other_seed.to_vec()))),
Ok(())
); );
let birthday = AccountBirthday::from_sapling_activation(&network); let birthday = AccountBirthday::from_sapling_activation(&network);
@ -1301,10 +1453,22 @@ mod tests {
db_data.get_account(account_id), db_data.get_account(account_id),
Ok(Some(account)) if matches!( Ok(Some(account)) if matches!(
account.kind, account.kind,
AccountKind::Derived{account_index, ..} if account_index == zip32::AccountId::ZERO, AccountSource::Derived{account_index, ..} if account_index == zip32::AccountId::ZERO,
) )
); );
// After adding an account, only the real seed phrase is relevant to the wallet.
assert_matches!(
init_wallet_db(&mut db_data, Some(Secret::new(seed.to_vec()))),
Ok(())
);
assert_matches!(
init_wallet_db(&mut db_data, Some(Secret::new(other_seed.to_vec()))),
Err(schemer::MigratorError::Adapter(
WalletMigrationError::SeedNotRelevant
))
);
for tv in &test_vectors::UNIFIED[..3] { for tv in &test_vectors::UNIFIED[..3] {
if let Some(Address::Unified(tvua)) = if let Some(Address::Unified(tvua)) =
Address::decode(&Network::MainNetwork, tv.unified_addr) Address::decode(&Network::MainNetwork, tv.unified_addr)

View File

@ -32,9 +32,8 @@ use super::WalletMigrationError;
pub(super) fn all_migrations<P: consensus::Parameters + 'static>( pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
params: &P, params: &P,
seed: Option<SecretVec<u8>>, seed: Option<Rc<SecretVec<u8>>>,
) -> Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>> { ) -> Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>> {
let seed = Rc::new(seed);
// initial_setup // initial_setup
// / \ // / \
// utxos_table ufvk_support // utxos_table ufvk_support

View File

@ -1,11 +1,11 @@
use std::{collections::HashSet, rc::Rc}; use std::{collections::HashSet, rc::Rc};
use crate::wallet::{account_kind_code, init::WalletMigrationError, ufvk_to_uivk}; use crate::wallet::{account_kind_code, init::WalletMigrationError};
use rusqlite::{named_params, Transaction}; use rusqlite::{named_params, Transaction};
use schemer_rusqlite::RusqliteMigration; use schemer_rusqlite::RusqliteMigration;
use secrecy::{ExposeSecret, SecretVec}; use secrecy::{ExposeSecret, SecretVec};
use uuid::Uuid; use uuid::Uuid;
use zcash_client_backend::{data_api::AccountKind, keys::UnifiedSpendingKey}; use zcash_client_backend::{data_api::AccountSource, keys::UnifiedSpendingKey};
use zcash_keys::keys::UnifiedFullViewingKey; use zcash_keys::keys::UnifiedFullViewingKey;
use zcash_primitives::consensus; use zcash_primitives::consensus;
use zip32::fingerprint::SeedFingerprint; use zip32::fingerprint::SeedFingerprint;
@ -17,7 +17,7 @@ use super::{add_account_birthdays, receiving_key_scopes, v_transactions_note_uni
pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x1b104345_f27e_42da_a9e3_1de22694da43); pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0x1b104345_f27e_42da_a9e3_1de22694da43);
pub(crate) struct Migration<P: consensus::Parameters> { pub(crate) struct Migration<P: consensus::Parameters> {
pub(super) seed: Rc<Option<SecretVec<u8>>>, pub(super) seed: Option<Rc<SecretVec<u8>>>,
pub(super) params: P, pub(super) params: P,
} }
@ -45,11 +45,11 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
type Error = WalletMigrationError; type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> { fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {
let account_kind_derived = account_kind_code(AccountKind::Derived { let account_kind_derived = account_kind_code(AccountSource::Derived {
seed_fingerprint: SeedFingerprint::from_bytes([0; 32]), seed_fingerprint: SeedFingerprint::from_bytes([0; 32]),
account_index: zip32::AccountId::ZERO, account_index: zip32::AccountId::ZERO,
}); });
let account_kind_imported = account_kind_code(AccountKind::Imported); let account_kind_imported = account_kind_code(AccountSource::Imported);
transaction.execute_batch( transaction.execute_batch(
&format!(r#" &format!(r#"
PRAGMA foreign_keys = OFF; PRAGMA foreign_keys = OFF;
@ -83,9 +83,26 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
if transaction.query_row("SELECT COUNT(*) FROM accounts", [], |row| { if transaction.query_row("SELECT COUNT(*) FROM accounts", [], |row| {
Ok(row.get::<_, u32>(0)? > 0) Ok(row.get::<_, u32>(0)? > 0)
})? { })? {
if let Some(seed) = &self.seed.as_ref() { if let Some(seed) = &self.seed {
let seed_id = SeedFingerprint::from_seed(seed.expose_secret()) let seed_id = SeedFingerprint::from_seed(seed.expose_secret())
.expect("Seed is between 32 and 252 bytes in length."); .expect("Seed is between 32 and 252 bytes in length.");
// We track whether we have determined seed relevance or not, in order to
// correctly report errors when checking the seed against an account:
//
// - If we encounter an error with the first account, we can assert that
// the seed is not relevant to the wallet by assuming that:
// - All accounts are from the same seed (which is historically the only
// use case that this migration supported), and
// - All accounts in the wallet must have been able to derive their USKs
// (in order to derive UIVKs).
//
// - Once the seed has been determined to be relevant (because it matched
// the first account), any subsequent account derivation failure is
// proving wrong our second assumption above, and we report this as
// corrupted data.
let mut seed_is_relevant = false;
let mut q = transaction.prepare("SELECT * FROM accounts")?; let mut q = transaction.prepare("SELECT * FROM accounts")?;
let mut rows = q.query([])?; let mut rows = q.query([])?;
while let Some(row) = rows.next()? { while let Some(row) = rows.next()? {
@ -110,19 +127,31 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
})?, })?,
) )
.map_err(|_| { .map_err(|_| {
WalletMigrationError::CorruptedData( if seed_is_relevant {
"Unable to derive spending key from seed.".to_string(), WalletMigrationError::CorruptedData(
) "Unable to derive spending key from seed.".to_string(),
)
} else {
WalletMigrationError::SeedNotRelevant
}
})?; })?;
let expected_ufvk = usk.to_unified_full_viewing_key(); let expected_ufvk = usk.to_unified_full_viewing_key();
if ufvk != expected_ufvk.encode(&self.params) { if ufvk != expected_ufvk.encode(&self.params) {
return Err(WalletMigrationError::CorruptedData( return Err(if seed_is_relevant {
"UFVK does not match expected value.".to_string(), WalletMigrationError::CorruptedData(
)); "UFVK does not match expected value.".to_string(),
)
} else {
WalletMigrationError::SeedNotRelevant
});
} }
let uivk = ufvk_to_uivk(&ufvk_parsed, &self.params) // We made it past one derived account, so the seed must be relevant.
.map_err(|e| WalletMigrationError::CorruptedData(e.to_string()))?; seed_is_relevant = true;
let uivk = ufvk_parsed
.to_unified_incoming_viewing_key()
.encode(&self.params);
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let transparent_item = ufvk_parsed.transparent().map(|k| k.serialize()); let transparent_item = ufvk_parsed.transparent().map(|k| k.serialize());

View File

@ -70,14 +70,14 @@ impl RusqliteMigration for Migration {
&format!( &format!(
"CREATE VIEW v_received_notes AS "CREATE VIEW v_received_notes AS
SELECT SELECT
id, sapling_received_notes.id AS id_within_pool_table,
tx, sapling_received_notes.tx,
{sapling_pool_code} AS pool, {sapling_pool_code} AS pool,
sapling_received_notes.output_index AS output_index, sapling_received_notes.output_index AS output_index,
account_id, account_id,
value, sapling_received_notes.value,
is_change, is_change,
memo, sapling_received_notes.memo,
spent, spent,
sent_notes.id AS sent_note_id sent_notes.id AS sent_note_id
FROM sapling_received_notes FROM sapling_received_notes
@ -86,14 +86,14 @@ impl RusqliteMigration for Migration {
(sapling_received_notes.tx, {sapling_pool_code}, sapling_received_notes.output_index) (sapling_received_notes.tx, {sapling_pool_code}, sapling_received_notes.output_index)
UNION UNION
SELECT SELECT
id, orchard_received_notes.id AS id_within_pool_table,
tx, orchard_received_notes.tx,
{orchard_pool_code} AS pool, {orchard_pool_code} AS pool,
orchard_received_notes.action_index AS output_index, orchard_received_notes.action_index AS output_index,
account_id, account_id,
value, orchard_received_notes.value,
is_change, is_change,
memo, orchard_received_notes.memo,
spent, spent,
sent_notes.id AS sent_note_id sent_notes.id AS sent_note_id
FROM orchard_received_notes FROM orchard_received_notes
@ -110,11 +110,12 @@ impl RusqliteMigration for Migration {
CREATE VIEW v_transactions AS CREATE VIEW v_transactions AS
WITH WITH
notes AS ( notes AS (
SELECT v_received_notes.id AS id, -- Shielded notes received in this transaction
v_received_notes.account_id AS account_id, SELECT v_received_notes.account_id AS account_id,
transactions.block AS block, transactions.block AS block,
transactions.txid AS txid, transactions.txid AS txid,
v_received_notes.pool AS pool, v_received_notes.pool AS pool,
id_within_pool_table,
v_received_notes.value AS value, v_received_notes.value AS value,
CASE CASE
WHEN v_received_notes.is_change THEN 1 WHEN v_received_notes.is_change THEN 1
@ -133,22 +134,24 @@ impl RusqliteMigration for Migration {
JOIN transactions JOIN transactions
ON transactions.id_tx = v_received_notes.tx ON transactions.id_tx = v_received_notes.tx
UNION UNION
SELECT utxos.id AS id, -- Transparent TXOs received in this transaction
utxos.received_by_account_id AS account_id, SELECT utxos.received_by_account_id AS account_id,
utxos.height AS block, utxos.height AS block,
utxos.prevout_txid AS txid, utxos.prevout_txid AS txid,
{transparent_pool_code} AS pool, {transparent_pool_code} AS pool,
utxos.id AS id_within_pool_table,
utxos.value_zat AS value, utxos.value_zat AS value,
0 AS is_change, 0 AS is_change,
1 AS received_count, 1 AS received_count,
0 AS memo_present 0 AS memo_present
FROM utxos FROM utxos
UNION UNION
SELECT v_received_notes.id AS id, -- Shielded notes spent in this transaction
v_received_notes.account_id AS account_id, SELECT v_received_notes.account_id AS account_id,
transactions.block AS block, transactions.block AS block,
transactions.txid AS txid, transactions.txid AS txid,
v_received_notes.pool AS pool, v_received_notes.pool AS pool,
id_within_pool_table,
-v_received_notes.value AS value, -v_received_notes.value AS value,
0 AS is_change, 0 AS is_change,
0 AS received_count, 0 AS received_count,
@ -157,11 +160,12 @@ impl RusqliteMigration for Migration {
JOIN transactions JOIN transactions
ON transactions.id_tx = v_received_notes.spent ON transactions.id_tx = v_received_notes.spent
UNION UNION
SELECT utxos.id AS id, -- Transparent TXOs spent in this transaction
utxos.received_by_account_id AS account_id, SELECT utxos.received_by_account_id AS account_id,
transactions.block AS block, transactions.block AS block,
transactions.txid AS txid, transactions.txid AS txid,
{transparent_pool_code} AS pool, {transparent_pool_code} AS pool,
utxos.id AS id_within_pool_table,
-utxos.value_zat AS value, -utxos.value_zat AS value,
0 AS is_change, 0 AS is_change,
0 AS received_count, 0 AS received_count,
@ -170,6 +174,8 @@ impl RusqliteMigration for Migration {
JOIN transactions JOIN transactions
ON transactions.id_tx = utxos.spent_in_tx ON transactions.id_tx = utxos.spent_in_tx
), ),
-- Obtain a count of the notes that the wallet created in each transaction,
-- not counting change notes.
sent_note_counts AS ( sent_note_counts AS (
SELECT sent_notes.from_account_id AS account_id, SELECT sent_notes.from_account_id AS account_id,
transactions.txid AS txid, transactions.txid AS txid,

View File

@ -31,7 +31,7 @@ pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xbe57ef3b_388e_42ea_97e2_
pub(super) struct Migration<P> { pub(super) struct Migration<P> {
pub(super) params: P, pub(super) params: P,
pub(super) seed: Rc<Option<SecretVec<u8>>>, pub(super) seed: Option<Rc<SecretVec<u8>>>,
} }
impl<P> schemer::Migration for Migration<P> { impl<P> schemer::Migration for Migration<P> {
@ -68,20 +68,43 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
let mut stmt_fetch_accounts = let mut stmt_fetch_accounts =
transaction.prepare("SELECT account, address FROM accounts")?; transaction.prepare("SELECT account, address FROM accounts")?;
// We track whether we have determined seed relevance or not, in order to
// correctly report errors when checking the seed against an account:
//
// - If we encounter an error with the first account, we can assert that the seed
// is not relevant to the wallet by assuming that:
// - All accounts are from the same seed (which is historically the only use
// case that this migration supported), and
// - All accounts in the wallet must have been able to derive their USKs (in
// order to derive UIVKs).
//
// - Once the seed has been determined to be relevant (because it matched the
// first account), any subsequent account derivation failure is proving wrong
// our second assumption above, and we report this as corrupted data.
let mut seed_is_relevant = false;
let ua_request = UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT); let ua_request = UnifiedAddressRequest::unsafe_new(false, true, UA_TRANSPARENT);
let mut rows = stmt_fetch_accounts.query([])?; let mut rows = stmt_fetch_accounts.query([])?;
while let Some(row) = rows.next()? { while let Some(row) = rows.next()? {
// We only need to check for the presence of the seed if we have keys that // 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 // need to be migrated; otherwise, it's fine to not supply the seed if this
// migration is being used to initialize an empty database. // migration is being used to initialize an empty database.
if let Some(seed) = &self.seed.as_ref() { if let Some(seed) = &self.seed {
let account: u32 = row.get(0)?; let account: u32 = row.get(0)?;
let account = AccountId::try_from(account).map_err(|_| { let account = AccountId::try_from(account).map_err(|_| {
WalletMigrationError::CorruptedData("Account ID is invalid".to_owned()) WalletMigrationError::CorruptedData("Account ID is invalid".to_owned())
})?; })?;
let usk = let usk =
UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), account) UnifiedSpendingKey::from_seed(&self.params, seed.expose_secret(), account)
.unwrap(); .map_err(|_| {
if seed_is_relevant {
WalletMigrationError::CorruptedData(
"Unable to derive spending key from seed.".to_string(),
)
} else {
WalletMigrationError::SeedNotRelevant
}
})?;
let ufvk = usk.to_unified_full_viewing_key(); let ufvk = usk.to_unified_full_viewing_key();
let address: String = row.get(1)?; let address: String = row.get(1)?;
@ -97,11 +120,15 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
WalletMigrationError::CorruptedData("Derivation should have produced a UFVK containing a Sapling component.".to_owned()))?; WalletMigrationError::CorruptedData("Derivation should have produced a UFVK containing a Sapling component.".to_owned()))?;
let (idx, expected_address) = dfvk.default_address(); let (idx, expected_address) = dfvk.default_address();
if decoded_address != expected_address { if decoded_address != expected_address {
return Err(WalletMigrationError::CorruptedData( return Err(if seed_is_relevant {
WalletMigrationError::CorruptedData(
format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.", format!("Decoded Sapling address {} does not match the ufvk's Sapling address {} at {:?}.",
address, address,
Address::Sapling(expected_address).encode(&self.params), Address::Sapling(expected_address).encode(&self.params),
idx))); idx))
} else {
WalletMigrationError::SeedNotRelevant
});
} }
} }
Address::Transparent(_) => { Address::Transparent(_) => {
@ -111,15 +138,22 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
Address::Unified(decoded_address) => { Address::Unified(decoded_address) => {
let (expected_address, idx) = ufvk.default_address(ua_request)?; let (expected_address, idx) = ufvk.default_address(ua_request)?;
if decoded_address != expected_address { if decoded_address != expected_address {
return Err(WalletMigrationError::CorruptedData( return Err(if seed_is_relevant {
WalletMigrationError::CorruptedData(
format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.", format!("Decoded unified address {} does not match the ufvk's default address {} at {:?}.",
address, address,
Address::Unified(expected_address).encode(&self.params), Address::Unified(expected_address).encode(&self.params),
idx))); idx))
} else {
WalletMigrationError::SeedNotRelevant
});
} }
} }
} }
// We made it past one derived account, so the seed must be relevant.
seed_is_relevant = true;
let ufvk_str: String = ufvk.encode(&self.params); let ufvk_str: String = ufvk.encode(&self.params);
let address_str: String = ufvk.default_address(ua_request)?.0.encode(&self.params); let address_str: String = ufvk.default_address(ua_request)?.0.encode(&self.params);

View File

@ -378,6 +378,7 @@ pub(crate) mod tests {
impl ShieldedPoolTester for OrchardPoolTester { impl ShieldedPoolTester for OrchardPoolTester {
const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Orchard; const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Orchard;
const TABLES_PREFIX: &'static str = ORCHARD_TABLES_PREFIX; const TABLES_PREFIX: &'static str = ORCHARD_TABLES_PREFIX;
// const MERKLE_TREE_DEPTH: u8 = {orchard::NOTE_COMMITMENT_TREE_DEPTH as u8};
type Sk = SpendingKey; type Sk = SpendingKey;
type Fvk = FullViewingKey; type Fvk = FullViewingKey;

View File

@ -399,6 +399,7 @@ pub(crate) mod tests {
impl ShieldedPoolTester for SaplingPoolTester { impl ShieldedPoolTester for SaplingPoolTester {
const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Sapling; const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Sapling;
const TABLES_PREFIX: &'static str = SAPLING_TABLES_PREFIX; const TABLES_PREFIX: &'static str = SAPLING_TABLES_PREFIX;
// const MERKLE_TREE_DEPTH: u8 = sapling::NOTE_COMMITMENT_TREE_DEPTH;
type Sk = ExtendedSpendingKey; type Sk = ExtendedSpendingKey;
type Fvk = DiversifiableFullViewingKey; type Fvk = DiversifiableFullViewingKey;

View File

@ -579,12 +579,14 @@ pub(crate) fn update_chain_tip<P: consensus::Parameters>(
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use incrementalmerkletree::{frontier::Frontier, Hashable, Level, Position}; use std::num::NonZeroU8;
use incrementalmerkletree::{frontier::Frontier, Hashable, Position};
use sapling::Node; use sapling::Node;
use secrecy::SecretVec; use secrecy::SecretVec;
use zcash_client_backend::data_api::{ use zcash_client_backend::data_api::{
chain::CommitmentTreeRoot, chain::{ChainState, CommitmentTreeRoot},
scanning::{spanning_tree::testing::scan_range, ScanPriority}, scanning::{spanning_tree::testing::scan_range, ScanPriority},
AccountBirthday, Ratio, WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT, AccountBirthday, Ratio, WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT,
}; };
@ -611,9 +613,7 @@ pub(crate) mod tests {
zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT, zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT,
}; };
// FIXME: This requires fixes to the test framework.
#[test] #[test]
#[cfg(feature = "orchard")]
fn sapling_scan_complete() { fn sapling_scan_complete() {
scan_complete::<SaplingPoolTester>(); scan_complete::<SaplingPoolTester>();
} }
@ -624,55 +624,75 @@ pub(crate) mod tests {
scan_complete::<OrchardPoolTester>(); scan_complete::<OrchardPoolTester>();
} }
// FIXME: This requires fixes to the test framework.
#[allow(dead_code)]
fn scan_complete<T: ShieldedPoolTester>() { fn scan_complete<T: ShieldedPoolTester>() {
use ScanPriority::*; use ScanPriority::*;
let initial_height_offset = 310;
let mut st = TestBuilder::new() let mut st = TestBuilder::new()
.with_block_cache() .with_block_cache()
.with_test_account(AccountBirthday::from_sapling_activation) .with_test_account(AccountBirthday::from_sapling_activation)
.build(); .build();
let dfvk = T::test_account_fvk(&st);
let sapling_activation_height = st.sapling_activation_height(); let sapling_activation_height = st.sapling_activation_height();
assert_matches!(
// In the following, we don't care what the root hashes are, they just need to be
// distinct.
T::put_subtree_roots(
&mut st,
0,
&[
CommitmentTreeRoot::from_parts(
sapling_activation_height + 100,
T::empty_tree_root(Level::from(0))
),
CommitmentTreeRoot::from_parts(
sapling_activation_height + 200,
T::empty_tree_root(Level::from(1))
),
CommitmentTreeRoot::from_parts(
sapling_activation_height + 300,
T::empty_tree_root(Level::from(2))
),
]
),
Ok(())
);
// We'll start inserting leaf notes 5 notes after the end of the third subtree, with a gap // We'll start inserting leaf notes 5 notes after the end of the third subtree, with a gap
// of 10 blocks. After `scan_cached_blocks`, the scan queue should have a requested scan // of 10 blocks. After `scan_cached_blocks`, the scan queue should have a requested scan
// range of 300..310 with `FoundNote` priority, 310..320 with `Scanned` priority. // range of 300..310 with `FoundNote` priority, 310..320 with `Scanned` priority.
// We set both Sapling and Orchard to the same initial tree size for simplicity. // We set both Sapling and Orchard to the same initial tree size for simplicity.
let initial_sapling_tree_size = (0x1 << 16) * 3 + 5; let initial_sapling_tree_size: u32 = (0x1 << 16) * 3 + 5;
let initial_orchard_tree_size = (0x1 << 16) * 3 + 5; let initial_orchard_tree_size: u32 = (0x1 << 16) * 3 + 5;
let initial_height = sapling_activation_height + 310;
// Construct a fake chain state for the end of block 300
let (prior_sapling_frontiers, sapling_initial_tree) =
Frontier::random_with_prior_subtree_roots(
st.rng(),
initial_sapling_tree_size.into(),
NonZeroU8::new(16).unwrap(),
);
let sapling_subtree_roots = prior_sapling_frontiers
.into_iter()
.zip(0u32..)
.map(|(root, i)| {
CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * (i + 1)), root)
})
.collect::<Vec<_>>();
#[cfg(feature = "orchard")]
let (prior_orchard_frontiers, orchard_initial_tree) =
Frontier::random_with_prior_subtree_roots(
st.rng(),
initial_orchard_tree_size.into(),
NonZeroU8::new(16).unwrap(),
);
#[cfg(feature = "orchard")]
let orchard_subtree_roots = prior_orchard_frontiers
.into_iter()
.zip(0u32..)
.map(|(root, i)| {
CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * (i + 1)), root)
})
.collect::<Vec<_>>();
let prior_block_hash = BlockHash([0; 32]);
st.establish_chain_state(
ChainState::new(
sapling_activation_height + initial_height_offset - 1,
prior_block_hash,
sapling_initial_tree,
#[cfg(feature = "orchard")]
orchard_initial_tree,
),
&sapling_subtree_roots,
#[cfg(feature = "orchard")]
&orchard_subtree_roots,
)
.unwrap();
let dfvk = T::test_account_fvk(&st);
let value = NonNegativeAmount::const_from_u64(50000); let value = NonNegativeAmount::const_from_u64(50000);
let initial_height = sapling_activation_height + initial_height_offset;
st.generate_block_at( st.generate_block_at(
initial_height, initial_height,
BlockHash([0; 32]), prior_block_hash,
&dfvk, &dfvk,
AddressType::DefaultExternal, AddressType::DefaultExternal,
value, value,

View File

@ -10,13 +10,23 @@ and this library adheres to Rust's notion of
- `zcash_keys::address::Address::has_receiver` - `zcash_keys::address::Address::has_receiver`
- `impl Display for zcash_keys::keys::AddressGenerationError` - `impl Display for zcash_keys::keys::AddressGenerationError`
- `impl std::error::Error for zcash_keys::keys::AddressGenerationError` - `impl std::error::Error for zcash_keys::keys::AddressGenerationError`
- `zcash_keys::keys::DecodingError`
- `zcash_keys::keys::UnifiedFullViewingKey::to_unified_incoming_viewing_key`
- `zcash_keys::keys::UnifiedIncomingViewingKey`
### Changed ### Changed
- `zcash_keys::keys::AddressGenerationError` has a new variant - `zcash_keys::keys::UnifiedFullViewingKey::{find_address, default_address}`
`DiversifierSpaceExhausted`.
- `zcash_keys::keys::UnifiedFullViewingKey::{find_address, default_address}`
now return `Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError>` now return `Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError>`
(instead of `Option<(UnifiedAddress, DiversifierIndex)>` for `find_address`). (instead of `Option<(UnifiedAddress, DiversifierIndex)>` for `find_address`).
- `zcash_keys::keys::AddressGenerationError`
- Added `DiversifierSpaceExhausted` variant.
- At least one of the `orchard`, `sapling`, or `transparent-inputs` features
must be enabled for the `keys` module to be accessible.
### Removed
- `UnifiedFullViewingKey::new` has been placed behind the `test-dependencies`
feature flag. UFVKs should only be produced by derivation from the USK, or
parsed from their string representation.
### Fixed ### Fixed
- `UnifiedFullViewingKey::find_address` can now find an address for a diversifier - `UnifiedFullViewingKey::find_address` can now find an address for a diversifier
@ -29,7 +39,7 @@ and this library adheres to Rust's notion of
- `zcash_keys::keys::UnifiedAddressRequest::all` - `zcash_keys::keys::UnifiedAddressRequest::all`
### Fixed ### Fixed
- A missing application of the `sapling` feature flag was remedied; - A missing application of the `sapling` feature flag was remedied;
prior to this fix it was not possible to use this crate without the prior to this fix it was not possible to use this crate without the
`sapling` feature enabled. `sapling` feature enabled.

View File

@ -342,7 +342,14 @@ impl Address {
} }
} }
#[cfg(any(test, feature = "test-dependencies"))] #[cfg(all(
any(
feature = "orchard",
feature = "sapling",
feature = "transparent-inputs"
),
any(test, feature = "test-dependencies")
))]
pub mod testing { pub mod testing {
use proptest::prelude::*; use proptest::prelude::*;
use zcash_primitives::consensus::Network; use zcash_primitives::consensus::Network;

File diff suppressed because it is too large Load Diff

View File

@ -16,4 +16,10 @@
pub mod address; pub mod address;
pub mod encoding; pub mod encoding;
#[cfg(any(
feature = "orchard",
feature = "sapling",
feature = "transparent-inputs"
))]
pub mod keys; pub mod keys;