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
extra_flags: orchard
rustflags: '--cfg zcash_unstable="orchard"'
- state: NU6
rustflags: '--cfg zcash_unstable="nu6"'

11
Cargo.lock generated
View File

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

View File

@ -122,6 +122,7 @@ panic = 'abort'
codegen-units = 1
[patch.crates-io]
orchard = { git = "https://github.com/zcash/orchard", rev = "e74879dd0ad0918f4ffe0826e03905cd819981bd" }
incrementalmerkletree = { git = "https://github.com/nuttycom/incrementalmerkletree", rev = "fa147c89c6c98a03bba745538f4e68d4eaed5146" }
shardtree = { git = "https://github.com/nuttycom/incrementalmerkletree", rev = "fa147c89c6c98a03bba745538f4e68d4eaed5146" }
orchard = { git = "https://github.com/zcash/orchard", rev = "33474bdbfd7268e1f84718078d47f63d01a879d5" }
incrementalmerkletree = { git = "https://github.com/zcash/incrementalmerkletree", rev = "e1a7a80212c22e5a8912d05860f7eb6899c56a7c" }
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`
- `AccountBalance::with_orchard_balance_mut`
- `AccountBirthday::orchard_frontier`
- `AccountKind`
- `AccountSource`
- `BlockMetadata::orchard_tree_size`
- `DecryptedTransaction::{new, tx(), orchard_outputs()}`
- `NoteRetention`
- `ScannedBlock::orchard`
- `ScannedBlockCommitments::orchard`
- `SeedRelevance`
- `SentTransaction::new`
- `SpendableNotes`
- `ORCHARD_SHARD_HEIGHT`
@ -29,6 +30,7 @@ and this library adheres to Rust's notion of
- `WalletSummary::next_orchard_subtree_index`
- `chain::ChainState`
- `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}`
- `impl Debug for chain::CommitmentTreeRoot`
- `zcash_client_backend::fees`:
- `orchard`
- `ChangeValue::orchard`
@ -59,6 +61,8 @@ and this library adheres to Rust's notion of
- Arguments to `ScannedBlock::from_parts` have changed.
- Changes to the `WalletRead` trait:
- Added `Account` associated type.
- Added `validate_seed` method.
- Added `is_seed_relevant_to_any_derived_accounts` method.
- Added `get_account` method.
- Added `get_derived_account` method.
- `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`
- `fn with_orchard_tree_mut`
- `fn put_orchard_subtree_roots`
- Added method `WalletRead::validate_seed`
- Removed `Error::AccountNotFound` variant.
- `WalletSummary::new` now takes an additional `next_orchard_subtree_index`
argument when the `orchard` feature flag is enabled.

View File

@ -64,6 +64,7 @@ use std::{
};
use incrementalmerkletree::{frontier::Frontier, Retention};
use nonempty::NonEmpty;
use secrecy::SecretVec;
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
use zip32::fingerprint::SeedFingerprint;
@ -75,7 +76,9 @@ use self::{
use crate::{
address::UnifiedAddress,
decrypt::DecryptedOutput,
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
keys::{
UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedIncomingViewingKey, UnifiedSpendingKey,
},
proto::service::TreeState,
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
ShieldedProtocol,
@ -320,7 +323,7 @@ impl AccountBalance {
/// The kinds of accounts supported by `zcash_client_backend`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AccountKind {
pub enum AccountSource {
/// An account derived from a known seed.
Derived {
seed_fingerprint: SeedFingerprint,
@ -338,40 +341,57 @@ pub trait Account<AccountId: Copy> {
/// Returns whether this account is derived or imported, and the derivation parameters
/// if applicable.
fn kind(&self) -> AccountKind;
fn source(&self) -> AccountSource;
/// 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
/// they are unable to maintain an accurate balance.
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) {
fn id(&self) -> A {
self.0
}
fn kind(&self) -> AccountKind {
AccountKind::Imported
fn source(&self) -> AccountSource {
AccountSource::Imported
}
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
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 {
self.0
}
fn kind(&self) -> AccountKind {
AccountKind::Imported
fn source(&self) -> AccountSource {
AccountSource::Imported
}
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>,
) -> 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.
fn get_account_for_ufvk(
&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.
#[derive(Debug, Clone, Copy)]
pub struct BlockMetadata {
@ -1609,8 +1654,8 @@ pub mod testing {
chain::{ChainState, CommitmentTreeRoot},
scanning::ScanRange,
AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery,
ScannedBlock, SentTransaction, SpendableNotes, WalletCommitmentTrees, WalletRead,
WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
ScannedBlock, SeedRelevance, SentTransaction, SpendableNotes, WalletCommitmentTrees,
WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
};
#[cfg(feature = "transparent-inputs")]
@ -1703,6 +1748,13 @@ pub mod testing {
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(
&self,
_ufvk: &UnifiedFullViewingKey,

View File

@ -155,7 +155,10 @@ use std::ops::Range;
use incrementalmerkletree::frontier::Frontier;
use subtle::ConditionallySelectable;
use zcash_primitives::consensus::{self, BlockHeight};
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
};
use crate::{
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
/// added, and the root hash of the complete subtree.
#[derive(Debug)]
pub struct CommitmentTreeRoot<H> {
subtree_end_height: BlockHeight,
root_hash: H,
@ -291,6 +295,7 @@ impl ScanSummary {
#[derive(Debug, Clone)]
pub struct ChainState {
block_height: BlockHeight,
block_hash: BlockHash,
final_sapling_tree: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
#[cfg(feature = "orchard")]
final_orchard_tree:
@ -299,9 +304,10 @@ pub struct ChainState {
impl ChainState {
/// Construct a new empty chain state.
pub fn empty(block_height: BlockHeight) -> Self {
pub fn empty(block_height: BlockHeight, block_hash: BlockHash) -> Self {
Self {
block_height,
block_hash,
final_sapling_tree: Frontier::empty(),
#[cfg(feature = "orchard")]
final_orchard_tree: Frontier::empty(),
@ -311,6 +317,7 @@ impl ChainState {
/// Construct a new [`ChainState`] from its constituent parts.
pub fn new(
block_height: BlockHeight,
block_hash: BlockHash,
final_sapling_tree: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
#[cfg(feature = "orchard")] final_orchard_tree: Frontier<
orchard::tree::MerkleHashOrchard,
@ -319,6 +326,7 @@ impl ChainState {
) -> Self {
Self {
block_height,
block_hash,
final_sapling_tree,
#[cfg(feature = "orchard")]
final_orchard_tree,
@ -330,6 +338,11 @@ impl ChainState {
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
/// [`Self::block_height`].
pub fn final_sapling_tree(

View File

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

View File

@ -83,8 +83,3 @@ pub use zcash_protocol::{PoolType, ShieldedProtocol};
#[cfg(test)]
#[macro_use]
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(
&self,
) -> io::Result<CommitmentTree<Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>> {
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),
if self.sapling_tree.is_empty() {
Ok(CommitmentTree::empty())
} else {
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.
@ -280,25 +284,43 @@ impl service::TreeState {
&self,
) -> io::Result<CommitmentTree<MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>>
{
let orchard_tree_bytes = hex::decode(&self.orchard_tree).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Hex decoding of Orchard tree bytes failed: {:?}", e),
)
})?;
read_commitment_tree::<MerkleHashOrchard, _, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>(
&orchard_tree_bytes[..],
)
if self.orchard_tree.is_empty() {
Ok(CommitmentTree::empty())
} else {
let orchard_tree_bytes = hex::decode(&self.orchard_tree).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Hex decoding of Orchard tree bytes failed: {:?}", e),
)
})?;
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`].
///
/// [`scan_cached_blocks`]: crate::data_api::chain::scan_cached_blocks
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(
self.height
.try_into()
.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(),
#[cfg(feature = "orchard")]
self.orchard_tree()?.to_frontier(),

View File

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

View File

@ -45,6 +45,7 @@ tracing.workspace = true
# - Serialization
byteorder.workspace = true
nonempty.workspace = true
prost.workspace = true
group.workspace = true
jubjub.workspace = true
@ -82,7 +83,6 @@ bls12_381.workspace = true
incrementalmerkletree = { workspace = true, features = ["test-dependencies"] }
pasta_curves.workspace = true
shardtree = { workspace = true, features = ["legacy-api", "test-dependencies"] }
nonempty.workspace = true
orchard = { workspace = true, features = ["test-dependencies"] }
proptest.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.
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.
TableNotEmpty,
@ -138,8 +134,6 @@ impl fmt::Display for SqliteClientError {
}
SqliteClientError::Protobuf(e) => write!(f, "Failed to parse protobuf-encoded record: {}", e),
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) =>
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),

View File

@ -37,6 +37,7 @@ use maybe_rayon::{
prelude::{IndexedParallelIterator, ParallelIterator},
slice::ParallelSliceMut,
};
use nonempty::NonEmpty;
use rusqlite::{self, Connection};
use secrecy::{ExposeSecret, SecretVec};
use shardtree::{error::ShardTreeError, ShardTree};
@ -60,9 +61,9 @@ use zcash_client_backend::{
self,
chain::{BlockSource, ChainState, CommitmentTreeRoot},
scanning::{ScanPriority, ScanRange},
Account, AccountBirthday, AccountKind, BlockMetadata, DecryptedTransaction, InputSource,
NullifierQuery, ScannedBlock, SentTransaction, SpendableNotes, WalletCommitmentTrees,
WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
Account, AccountBirthday, AccountSource, BlockMetadata, DecryptedTransaction, InputSource,
NullifierQuery, ScannedBlock, SeedRelevance, SentTransaction, SpendableNotes,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
},
keys::{
AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey,
@ -316,43 +317,18 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
seed: &SecretVec<u8>,
) -> Result<bool, Self::Error> {
if let Some(account) = self.get_account(account_id)? {
if let AccountKind::Derived {
if let AccountSource::Derived {
seed_fingerprint,
account_index,
} = account.kind()
} = account.source()
{
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;
let usk = UnifiedSpendingKey::from_seed(
wallet::seed_matches_derived_account(
&self.params,
&seed.expose_secret()[..],
seed,
&seed_fingerprint,
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 {
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(
&self,
ufvk: &UnifiedFullViewingKey,
@ -527,7 +552,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
let account_id = wallet::add_account(
wdb.conn.0,
&wdb.params,
AccountKind::Derived {
AccountSource::Derived {
seed_fingerprint,
account_index,
},

View File

@ -6,12 +6,14 @@ use std::{collections::BTreeMap, convert::Infallible};
use std::fs::File;
use group::ff::Field;
use incrementalmerkletree::Retention;
use nonempty::NonEmpty;
use prost::Message;
use rand_chacha::ChaChaRng;
use rand_core::{CryptoRng, RngCore, SeedableRng};
use rusqlite::{params, Connection};
use secrecy::{Secret, SecretVec};
use shardtree::error::ShardTreeError;
use tempfile::NamedTempFile;
#[cfg(feature = "unstable")]
@ -28,13 +30,14 @@ use zcash_client_backend::{
address::Address,
data_api::{
self,
chain::{scan_cached_blocks, BlockSource, ScanSummary},
chain::{scan_cached_blocks, BlockSource, CommitmentTreeRoot, ScanSummary},
wallet::{
create_proposed_transactions, create_spend_to_address,
input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector},
propose_standard_transfer_to_address, propose_transfer, spend,
},
AccountBalance, AccountBirthday, WalletRead, WalletSummary, WalletWrite,
AccountBalance, AccountBirthday, WalletCommitmentTrees, WalletRead, WalletSummary,
WalletWrite,
},
keys::UnifiedSpendingKey,
proposal::Proposal,
@ -63,6 +66,7 @@ use zcash_primitives::{
zip32::DiversifierIndex,
};
use zcash_protocol::local_consensus::LocalNetwork;
use zcash_protocol::value::{ZatBalance, Zatoshis};
use crate::{
chain::init::init_cache_database,
@ -189,7 +193,6 @@ impl<Cache> TestBuilder<Cache> {
#[derive(Clone, Debug)]
pub(crate) struct CachedBlock {
hash: BlockHash,
chain_state: ChainState,
sapling_end_size: u32,
orchard_end_size: u32,
@ -198,19 +201,13 @@ pub(crate) struct CachedBlock {
impl CachedBlock {
fn none(sapling_activation_height: BlockHeight) -> Self {
Self {
hash: BlockHash([0; 32]),
chain_state: ChainState::empty(sapling_activation_height),
chain_state: ChainState::empty(sapling_activation_height, BlockHash([0; 32])),
sapling_end_size: 0,
orchard_end_size: 0,
}
}
fn at(
hash: BlockHash,
chain_state: ChainState,
sapling_end_size: u32,
orchard_end_size: u32,
) -> Self {
fn at(chain_state: ChainState, sapling_end_size: u32, orchard_end_size: u32) -> Self {
assert_eq!(
chain_state.final_sapling_tree().tree_size() as u32,
sapling_end_size
@ -222,7 +219,6 @@ impl CachedBlock {
);
Self {
hash,
chain_state,
sapling_end_size,
orchard_end_size,
@ -257,9 +253,9 @@ impl CachedBlock {
});
Self {
hash: cb.hash(),
chain_state: ChainState::new(
cb.height(),
cb.hash(),
sapling_final_tree,
#[cfg(feature = "orchard")]
orchard_final_tree,
@ -322,6 +318,67 @@ where
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
/// given value, and inserts it into the cache.
pub(crate) fn generate_next_block<Fvk: TestFvk>(
@ -336,7 +393,7 @@ where
let (res, nf) = self.generate_block_at(
height,
prior_cached_block.hash,
prior_cached_block.chain_state.block_hash(),
fvk,
req,
value,
@ -375,7 +432,7 @@ where
// we need to generate a new prior cached block that the block to be generated can
// successfully chain from, with the provided tree sizes.
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 {
let final_sapling_tree =
(prior_cached_block.sapling_end_size..initial_sapling_tree_size).fold(
@ -399,9 +456,9 @@ where
);
prior_cached_block = CachedBlock::at(
prev_hash,
ChainState::new(
height - 1,
prev_hash,
final_sapling_tree,
#[cfg(feature = "orchard")]
final_orchard_tree,
@ -451,7 +508,7 @@ where
let cb = fake_compact_block_spending(
&self.network(),
height,
prior_cached_block.hash,
prior_cached_block.chain_state.block_hash(),
note,
fvk,
to.into(),
@ -507,7 +564,7 @@ where
let cb = fake_compact_block_from_tx(
height,
prior_cached_block.hash,
prior_cached_block.chain_state.block_hash(),
tx_index,
tx,
prior_cached_block.sapling_end_size,
@ -612,6 +669,11 @@ impl<Cache> TestState<Cache> {
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.
pub(crate) fn sapling_activation_height(&self) -> BlockHeight {
self.db_data
@ -958,6 +1020,105 @@ impl<Cache> TestState<Cache> {
)
.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.

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)),
Ok(None)
);
let tx_history = st.get_tx_history().unwrap();
assert_eq!(tx_history.len(), 2);
}
#[cfg(feature = "transparent-inputs")]

View File

@ -66,6 +66,7 @@
use incrementalmerkletree::Retention;
use rusqlite::{self, named_params, params, OptionalExtension};
use secrecy::{ExposeSecret, SecretVec};
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
use zip32::fingerprint::SeedFingerprint;
@ -75,15 +76,16 @@ use std::io::{self, Cursor};
use std::num::NonZeroU32;
use std::ops::RangeInclusive;
use tracing::debug;
use zcash_address::unified::{Encoding, Ivk, Uivk};
use zcash_keys::keys::{AddressGenerationError, UnifiedAddressRequest};
use zcash_keys::keys::{
AddressGenerationError, UnifiedAddressRequest, UnifiedIncomingViewingKey, UnifiedSpendingKey,
};
use zcash_client_backend::{
address::{Address, UnifiedAddress},
data_api::{
scanning::{ScanPriority, ScanRange},
AccountBalance, AccountBirthday, AccountKind, BlockMetadata, Ratio, SentTransactionOutput,
WalletSummary, SAPLING_SHARD_HEIGHT,
AccountBalance, AccountBirthday, AccountSource, BlockMetadata, Ratio,
SentTransactionOutput, WalletSummary, SAPLING_SHARD_HEIGHT,
},
encoding::AddressCodec,
keys::UnifiedFullViewingKey,
@ -119,6 +121,7 @@ use {
crate::UtxoId,
rusqlite::Row,
std::collections::BTreeSet,
zcash_address::unified::{Encoding, Ivk, Uivk},
zcash_client_backend::wallet::{TransparentAddressMetadata, WalletTransparentOutput},
zcash_primitives::{
legacy::{
@ -139,13 +142,13 @@ pub(crate) mod scanning;
pub(crate) const BLOCK_SAPLING_FRONTIER_ABSENT: &[u8] = &[0x0];
fn parse_account_kind(
fn parse_account_source(
account_kind: u32,
hd_seed_fingerprint: Option<[u8; 32]>,
hd_account_index: Option<u32>,
) -> Result<AccountKind, SqliteClientError> {
) -> Result<AccountSource, SqliteClientError> {
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),
account_index: zip32::AccountId::try_from(account_index).map_err(|_| {
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(
"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 {
AccountKind::Derived { .. } => 0,
AccountKind::Imported => 1,
AccountSource::Derived { .. } => 0,
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,
/// because they are unable to maintain an accurate balance.
Incoming(Uivk),
Incoming(Box<UnifiedIncomingViewingKey>),
}
/// An account stored in a `zcash_client_sqlite` database.
#[derive(Debug, Clone)]
pub struct Account {
account_id: AccountId,
kind: AccountKind,
kind: AccountSource,
viewing_key: ViewingKey,
}
@ -206,7 +209,7 @@ impl Account {
) -> Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError> {
match &self.viewing_key {
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
}
fn kind(&self) -> AccountKind {
fn source(&self) -> AccountSource {
self.kind
}
fn ufvk(&self) -> Option<&UnifiedFullViewingKey> {
self.viewing_key.ufvk()
}
fn uivk(&self) -> UnifiedIncomingViewingKey {
self.viewing_key.uivk()
}
}
impl ViewingKey {
@ -233,14 +240,57 @@ impl ViewingKey {
}
}
fn uivk_str(&self, params: &impl Parameters) -> Result<String, SqliteClientError> {
fn uivk(&self) -> UnifiedIncomingViewingKey {
match self {
ViewingKey::Full(ufvk) => ufvk_to_uivk(ufvk, params),
ViewingKey::Incoming(uivk) => Ok(uivk.encode(&params.network_type())),
ViewingKey::Full(ufvk) => ufvk.as_ref().to_unified_incoming_viewing_key(),
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 {
// These constants are *incidentally* shared with the typecodes
// 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>(
conn: &rusqlite::Transaction,
params: &P,
kind: AccountKind,
kind: AccountSource,
viewing_key: ViewingKey,
birthday: AccountBirthday,
) -> Result<AccountId, SqliteClientError> {
let (hd_seed_fingerprint, hd_account_index) = match kind {
AccountKind::Derived {
AccountSource::Derived {
seed_fingerprint,
account_index,
} => (Some(seed_fingerprint), Some(account_index)),
AccountKind::Imported => (None, None),
AccountSource::Imported => (None, None),
};
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_account_index": hd_account_index.map(u32::from),
":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,
":sapling_fvk_item_cache": sapling_item,
":p2pkh_fvk_item_cache": transparent_item,
@ -734,7 +759,7 @@ pub(crate) fn get_account_for_ufvk<P: consensus::Parameters>(
],
|row| {
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
// non-null.
@ -802,7 +827,7 @@ pub(crate) fn get_derived_account<P: consensus::Parameters>(
}?;
Ok(Account {
account_id,
kind: AccountKind::Derived {
kind: AccountSource::Derived {
seed_fingerprint: *seed,
account_index,
},
@ -1511,7 +1536,7 @@ pub(crate) fn get_account<P: Parameters>(
let row = result.next()?;
match row {
Some(row) => {
let kind = parse_account_kind(
let kind = parse_account_source(
row.get("account_kind")?,
row.get("hd_seed_fingerprint")?,
row.get("hd_account_index")?,
@ -1525,15 +1550,10 @@ pub(crate) fn get_account<P: Parameters>(
))
} else {
let uivk_str: String = row.get("uivk")?;
let (network, uivk) = Uivk::decode(&uivk_str).map_err(|e| {
SqliteClientError::CorruptedData(format!("Failure to decode UIVK: {e}"))
})?;
if network != params.network_type() {
return Err(SqliteClientError::CorruptedData(
"UIVK network type does not match wallet network type".to_string(),
));
}
ViewingKey::Incoming(uivk)
ViewingKey::Incoming(Box::new(
UnifiedIncomingViewingKey::decode(params, &uivk_str[..])
.map_err(SqliteClientError::BadAccountData)?,
))
};
Ok(Some(Account {
@ -2700,7 +2720,7 @@ mod tests {
use std::num::NonZeroU32;
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 crate::{
@ -2857,7 +2877,7 @@ mod tests {
let expected_account_index = zip32::AccountId::try_from(0).unwrap();
assert_matches!(
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.
use std::fmt;
use std::rc::Rc;
use schemer::{Migrator, MigratorError};
use schemer_rusqlite::RusqliteAdapter;
@ -8,11 +9,14 @@ use secrecy::SecretVec;
use shardtree::error::ShardTreeError;
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 super::commitment_tree;
use crate::WalletDb;
use crate::{error::SqliteClientError, WalletDb};
mod migrations;
@ -21,6 +25,17 @@ pub enum WalletMigrationError {
/// The seed is required for the migration.
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.
CorruptedData(String),
@ -73,6 +88,12 @@ impl fmt::Display for WalletMigrationError {
"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) => {
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.
///
/// 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 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
/// 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
/// 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
///
/// ```
/// use secrecy::Secret;
/// use tempfile::NamedTempFile;
/// # use std::error::Error;
/// # use secrecy::SecretVec;
/// # use tempfile::NamedTempFile;
/// use zcash_primitives::consensus::Network;
/// use zcash_client_sqlite::{
/// WalletDb,
/// wallet::init::init_wallet_db,
/// wallet::init::{WalletMigrationError, init_wallet_db},
/// };
///
/// let data_file = NamedTempFile::new().unwrap();
/// let mut db = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap();
/// init_wallet_db(&mut db, Some(Secret::new(vec![]))).unwrap();
/// # fn main() -> Result<(), Box<dyn Error>> {
/// # let data_file = NamedTempFile::new().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
// 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>>,
target_migrations: &[Uuid],
) -> Result<(), MigratorError<WalletMigrationError>> {
let seed = seed.map(Rc::new);
// Turn off foreign keys, and ensure that table replacement/modification
// does not break views
wdb.conn
@ -161,7 +281,7 @@ fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
let mut migrator = Migrator::new(adapter);
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.");
if target_migrations.is_empty() {
migrator.up(None)?;
@ -173,6 +293,24 @@ fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
wdb.conn
.execute("PRAGMA foreign_keys = ON", [])
.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(())
}
@ -204,7 +342,7 @@ mod tests {
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")]
use {
@ -552,14 +690,14 @@ mod tests {
// v_received_notes
"CREATE VIEW v_received_notes AS
SELECT
id,
tx,
sapling_received_notes.id AS id_within_pool_table,
sapling_received_notes.tx,
2 AS pool,
sapling_received_notes.output_index AS output_index,
account_id,
value,
sapling_received_notes.value,
is_change,
memo,
sapling_received_notes.memo,
spent,
sent_notes.id AS sent_note_id
FROM sapling_received_notes
@ -568,14 +706,14 @@ mod tests {
(sapling_received_notes.tx, 2, sapling_received_notes.output_index)
UNION
SELECT
id,
tx,
orchard_received_notes.id AS id_within_pool_table,
orchard_received_notes.tx,
3 AS pool,
orchard_received_notes.action_index AS output_index,
account_id,
value,
orchard_received_notes.value,
is_change,
memo,
orchard_received_notes.memo,
spent,
sent_notes.id AS sent_note_id
FROM orchard_received_notes
@ -650,11 +788,12 @@ mod tests {
"CREATE VIEW v_transactions AS
WITH
notes AS (
SELECT v_received_notes.id AS id,
v_received_notes.account_id AS account_id,
-- Shielded notes received in this transaction
SELECT v_received_notes.account_id AS account_id,
transactions.block AS block,
transactions.txid AS txid,
v_received_notes.pool AS pool,
id_within_pool_table,
v_received_notes.value AS value,
CASE
WHEN v_received_notes.is_change THEN 1
@ -673,22 +812,24 @@ mod tests {
JOIN transactions
ON transactions.id_tx = v_received_notes.tx
UNION
SELECT utxos.id AS id,
utxos.received_by_account_id AS account_id,
-- Transparent TXOs received in this transaction
SELECT utxos.received_by_account_id AS account_id,
utxos.height AS block,
utxos.prevout_txid AS txid,
0 AS pool,
utxos.id AS id_within_pool_table,
utxos.value_zat AS value,
0 AS is_change,
1 AS received_count,
0 AS memo_present
FROM utxos
UNION
SELECT v_received_notes.id AS id,
v_received_notes.account_id AS account_id,
-- Shielded notes spent in this transaction
SELECT v_received_notes.account_id AS account_id,
transactions.block AS block,
transactions.txid AS txid,
v_received_notes.pool AS pool,
id_within_pool_table,
-v_received_notes.value AS value,
0 AS is_change,
0 AS received_count,
@ -697,11 +838,12 @@ mod tests {
JOIN transactions
ON transactions.id_tx = v_received_notes.spent
UNION
SELECT utxos.id AS id,
utxos.received_by_account_id AS account_id,
-- Transparent TXOs spent in this transaction
SELECT utxos.received_by_account_id AS account_id,
transactions.block AS block,
transactions.txid AS txid,
0 AS pool,
utxos.id AS id_within_pool_table,
-utxos.value_zat AS value,
0 AS is_change,
0 AS received_count,
@ -710,6 +852,8 @@ mod tests {
JOIN transactions
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 (
SELECT sent_notes.from_account_id AS account_id,
transactions.txid AS txid,
@ -1282,15 +1426,23 @@ mod tests {
#[test]
#[cfg(feature = "transparent-inputs")]
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 data_file = NamedTempFile::new().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 other_seed = [7; 32];
assert_matches!(
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);
@ -1301,10 +1453,22 @@ mod tests {
db_data.get_account(account_id),
Ok(Some(account)) if matches!(
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] {
if let Some(Address::Unified(tvua)) =
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>(
params: &P,
seed: Option<SecretVec<u8>>,
seed: Option<Rc<SecretVec<u8>>>,
) -> Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>> {
let seed = Rc::new(seed);
// initial_setup
// / \
// utxos_table ufvk_support

View File

@ -1,11 +1,11 @@
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 schemer_rusqlite::RusqliteMigration;
use secrecy::{ExposeSecret, SecretVec};
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_primitives::consensus;
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(crate) struct Migration<P: consensus::Parameters> {
pub(super) seed: Rc<Option<SecretVec<u8>>>,
pub(super) seed: Option<Rc<SecretVec<u8>>>,
pub(super) params: P,
}
@ -45,11 +45,11 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
type Error = 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]),
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(
&format!(r#"
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| {
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())
.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 rows = q.query([])?;
while let Some(row) = rows.next()? {
@ -110,19 +127,31 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
})?,
)
.map_err(|_| {
WalletMigrationError::CorruptedData(
"Unable to derive spending key from seed.".to_string(),
)
if seed_is_relevant {
WalletMigrationError::CorruptedData(
"Unable to derive spending key from seed.".to_string(),
)
} else {
WalletMigrationError::SeedNotRelevant
}
})?;
let expected_ufvk = usk.to_unified_full_viewing_key();
if ufvk != expected_ufvk.encode(&self.params) {
return Err(WalletMigrationError::CorruptedData(
"UFVK does not match expected value.".to_string(),
));
return Err(if seed_is_relevant {
WalletMigrationError::CorruptedData(
"UFVK does not match expected value.".to_string(),
)
} else {
WalletMigrationError::SeedNotRelevant
});
}
let uivk = ufvk_to_uivk(&ufvk_parsed, &self.params)
.map_err(|e| WalletMigrationError::CorruptedData(e.to_string()))?;
// We made it past one derived account, so the seed must be relevant.
seed_is_relevant = true;
let uivk = ufvk_parsed
.to_unified_incoming_viewing_key()
.encode(&self.params);
#[cfg(feature = "transparent-inputs")]
let transparent_item = ufvk_parsed.transparent().map(|k| k.serialize());

View File

@ -70,14 +70,14 @@ impl RusqliteMigration for Migration {
&format!(
"CREATE VIEW v_received_notes AS
SELECT
id,
tx,
sapling_received_notes.id AS id_within_pool_table,
sapling_received_notes.tx,
{sapling_pool_code} AS pool,
sapling_received_notes.output_index AS output_index,
account_id,
value,
sapling_received_notes.value,
is_change,
memo,
sapling_received_notes.memo,
spent,
sent_notes.id AS sent_note_id
FROM sapling_received_notes
@ -86,14 +86,14 @@ impl RusqliteMigration for Migration {
(sapling_received_notes.tx, {sapling_pool_code}, sapling_received_notes.output_index)
UNION
SELECT
id,
tx,
orchard_received_notes.id AS id_within_pool_table,
orchard_received_notes.tx,
{orchard_pool_code} AS pool,
orchard_received_notes.action_index AS output_index,
account_id,
value,
orchard_received_notes.value,
is_change,
memo,
orchard_received_notes.memo,
spent,
sent_notes.id AS sent_note_id
FROM orchard_received_notes
@ -110,11 +110,12 @@ impl RusqliteMigration for Migration {
CREATE VIEW v_transactions AS
WITH
notes AS (
SELECT v_received_notes.id AS id,
v_received_notes.account_id AS account_id,
-- Shielded notes received in this transaction
SELECT v_received_notes.account_id AS account_id,
transactions.block AS block,
transactions.txid AS txid,
v_received_notes.pool AS pool,
id_within_pool_table,
v_received_notes.value AS value,
CASE
WHEN v_received_notes.is_change THEN 1
@ -133,22 +134,24 @@ impl RusqliteMigration for Migration {
JOIN transactions
ON transactions.id_tx = v_received_notes.tx
UNION
SELECT utxos.id AS id,
utxos.received_by_account_id AS account_id,
-- Transparent TXOs received in this transaction
SELECT utxos.received_by_account_id AS account_id,
utxos.height AS block,
utxos.prevout_txid AS txid,
{transparent_pool_code} AS pool,
utxos.id AS id_within_pool_table,
utxos.value_zat AS value,
0 AS is_change,
1 AS received_count,
0 AS memo_present
FROM utxos
UNION
SELECT v_received_notes.id AS id,
v_received_notes.account_id AS account_id,
-- Shielded notes spent in this transaction
SELECT v_received_notes.account_id AS account_id,
transactions.block AS block,
transactions.txid AS txid,
v_received_notes.pool AS pool,
id_within_pool_table,
-v_received_notes.value AS value,
0 AS is_change,
0 AS received_count,
@ -157,11 +160,12 @@ impl RusqliteMigration for Migration {
JOIN transactions
ON transactions.id_tx = v_received_notes.spent
UNION
SELECT utxos.id AS id,
utxos.received_by_account_id AS account_id,
-- Transparent TXOs spent in this transaction
SELECT utxos.received_by_account_id AS account_id,
transactions.block AS block,
transactions.txid AS txid,
{transparent_pool_code} AS pool,
utxos.id AS id_within_pool_table,
-utxos.value_zat AS value,
0 AS is_change,
0 AS received_count,
@ -170,6 +174,8 @@ impl RusqliteMigration for Migration {
JOIN transactions
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 (
SELECT sent_notes.from_account_id AS account_id,
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) params: P,
pub(super) seed: Rc<Option<SecretVec<u8>>>,
pub(super) seed: Option<Rc<SecretVec<u8>>>,
}
impl<P> schemer::Migration for Migration<P> {
@ -68,20 +68,43 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
let mut stmt_fetch_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 mut rows = stmt_fetch_accounts.query([])?;
while let Some(row) = rows.next()? {
// 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
// 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 = AccountId::try_from(account).map_err(|_| {
WalletMigrationError::CorruptedData("Account ID is invalid".to_owned())
})?;
let usk =
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 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()))?;
let (idx, expected_address) = dfvk.default_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 {:?}.",
address,
Address::Sapling(expected_address).encode(&self.params),
idx)));
idx))
} else {
WalletMigrationError::SeedNotRelevant
});
}
}
Address::Transparent(_) => {
@ -111,15 +138,22 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
Address::Unified(decoded_address) => {
let (expected_address, idx) = ufvk.default_address(ua_request)?;
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 {:?}.",
address,
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 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 {
const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Orchard;
const TABLES_PREFIX: &'static str = ORCHARD_TABLES_PREFIX;
// const MERKLE_TREE_DEPTH: u8 = {orchard::NOTE_COMMITMENT_TREE_DEPTH as u8};
type Sk = SpendingKey;
type Fvk = FullViewingKey;

View File

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

View File

@ -579,12 +579,14 @@ pub(crate) fn update_chain_tip<P: consensus::Parameters>(
#[cfg(test)]
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 secrecy::SecretVec;
use zcash_client_backend::data_api::{
chain::CommitmentTreeRoot,
chain::{ChainState, CommitmentTreeRoot},
scanning::{spanning_tree::testing::scan_range, ScanPriority},
AccountBirthday, Ratio, WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT,
};
@ -611,9 +613,7 @@ pub(crate) mod tests {
zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT,
};
// FIXME: This requires fixes to the test framework.
#[test]
#[cfg(feature = "orchard")]
fn sapling_scan_complete() {
scan_complete::<SaplingPoolTester>();
}
@ -624,55 +624,75 @@ pub(crate) mod tests {
scan_complete::<OrchardPoolTester>();
}
// FIXME: This requires fixes to the test framework.
#[allow(dead_code)]
fn scan_complete<T: ShieldedPoolTester>() {
use ScanPriority::*;
let initial_height_offset = 310;
let mut st = TestBuilder::new()
.with_block_cache()
.with_test_account(AccountBirthday::from_sapling_activation)
.build();
let dfvk = T::test_account_fvk(&st);
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
// 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.
// 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_orchard_tree_size = (0x1 << 16) * 3 + 5;
let initial_height = sapling_activation_height + 310;
let initial_sapling_tree_size: u32 = (0x1 << 16) * 3 + 5;
let initial_orchard_tree_size: u32 = (0x1 << 16) * 3 + 5;
// 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 initial_height = sapling_activation_height + initial_height_offset;
st.generate_block_at(
initial_height,
BlockHash([0; 32]),
prior_block_hash,
&dfvk,
AddressType::DefaultExternal,
value,

View File

@ -10,13 +10,23 @@ and this library adheres to Rust's notion of
- `zcash_keys::address::Address::has_receiver`
- `impl Display 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
- `zcash_keys::keys::AddressGenerationError` has a new variant
`DiversifierSpaceExhausted`.
- `zcash_keys::keys::UnifiedFullViewingKey::{find_address, default_address}`
- `zcash_keys::keys::UnifiedFullViewingKey::{find_address, default_address}`
now return `Result<(UnifiedAddress, DiversifierIndex), AddressGenerationError>`
(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
- `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`
### 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
`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 {
use proptest::prelude::*;
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 encoding;
#[cfg(any(
feature = "orchard",
feature = "sapling",
feature = "transparent-inputs"
))]
pub mod keys;