Merge remote-tracking branch 'upstream/main' into memory_wallet_db
This commit is contained in:
commit
d013322aa1
|
@ -34,7 +34,6 @@ jobs:
|
|||
|
||||
- state: Orchard
|
||||
extra_flags: orchard
|
||||
rustflags: '--cfg zcash_unstable="orchard"'
|
||||
- state: NU6
|
||||
rustflags: '--cfg zcash_unstable="nu6"'
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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."
|
||||
);
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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(¶ms.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(¶ms.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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}`
|
||||
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
|
||||
|
|
|
@ -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
|
@ -16,4 +16,10 @@
|
|||
|
||||
pub mod address;
|
||||
pub mod encoding;
|
||||
|
||||
#[cfg(any(
|
||||
feature = "orchard",
|
||||
feature = "sapling",
|
||||
feature = "transparent-inputs"
|
||||
))]
|
||||
pub mod keys;
|
||||
|
|
Loading…
Reference in New Issue