Merge branch 'main' into 1044-extract-zip32

This commit is contained in:
Jack Grigg 2023-12-06 18:04:36 +00:00
commit d332aacf98
22 changed files with 748 additions and 329 deletions

View File

@ -12,8 +12,16 @@ and this library adheres to Rust's notion of
- `BlockMetadata::orchard_tree_size`.
- `TransparentInputSource`
- `SaplingInputSource`
- `ScannedBlock::sapling_tree_size`.
- `ScannedBlock::orchard_tree_size`.
- `ScannedBlock::{
sapling_tree_size, orchard_tree_size, orchard_nullifier_map,
orchard_commitments, into_commitments
}`
- `Balance::{add_spendable_value, add_pending_change_value, add_pending_spendable_value}`
- `AccountBalance::{
with_sapling_balance_mut,
with_orchard_balance_mut,
add_unshielded_value
}`
- `wallet::propose_standard_transfer_to_address`
- `wallet::input_selection::Proposal::from_parts`
- `wallet::input_selection::SaplingInputs`
@ -22,8 +30,9 @@ and this library adheres to Rust's notion of
functionality and move it behind the `transparent-inputs` feature flag.
- `zcash_client_backend::fees::standard`
- `zcash_client_backend::wallet`:
- `ReceivedSaplingNote::from_parts`
- `ReceivedSaplingNote::{txid, output_index, diversifier, rseed, note_commitment_tree_position}`
- `WalletNote`
- `ReceivedNote`
- `WalletSaplingOutput::recipient_key_scope`
- `zcash_client_backend::zip321::TransactionRequest::total`
- `zcash_client_backend::zip321::parse::Param::name`
- `zcash_client_backend::proto::`
@ -41,17 +50,33 @@ and this library adheres to Rust's notion of
wallet::input_selection::{Proposal, SaplingInputs},
}`
### Moved
- `zcash_client_backend::data_api::{PoolType, ShieldedProtocol}` have
been moved into the `zcash_client_backend` root module.
- `zcash_client_backend::data_api::{NoteId, Recipient}` have
been moved into the `zcash_client_backend::wallet` module.
### Changed
- `zcash_client_backend::data_api`:
- Arguments to `BlockMetadata::from_parts` have changed to include Orchard.
- `BlockMetadata::sapling_tree_size` now returns an `Option<u32>` instead of
a `u32` for consistency with Orchard.
- `WalletShieldedOutput` has an additional type parameter which is used for
key scope. `WalletShieldedOutput::from_parts` now takes an additional
argument of this type.
- `WalletTx` has an additional type parameter as a consequence of the
`WalletShieldedOutput` change.
- `ScannedBlock` has an additional type parameter as a consequence of the
`WalletTx` change.
- Arguments to `ScannedBlock::from_parts` have changed.
- `ScannedBlock::metadata` has been renamed to `to_block_metadata` and now
returns an owned value rather than a reference.
- `ShieldedProtocol` has a new variant for `Orchard`, allowing for better
reporting to callers trying to perform actions using `Orchard` before it is
fully supported.
- Fields of `Balance` and `AccountBalance` have been made private and the values
of these fields have been made available via methods having the same names
as the previously-public fields.
- `chain::scan_cached_blocks` now returns a `ScanSummary` containing metadata
about the scanned blocks on success.
- `error::Error` enum changes:
@ -157,6 +182,8 @@ and this library adheres to Rust's notion of
- `WalletTransparentOutput::value`
### Removed
- `zcash_client_backend::wallet::ReceivedSaplingNote` has been replaced by
`zcash_client_backend::ReceivedNote`.
- `zcash_client_backend::data_api::WalletRead::is_valid_account_extfvk` has been
removed; it was unused in the ECC mobile wallet SDKs and has been superseded by
`get_account_for_ufvk`.
@ -164,6 +191,9 @@ and this library adheres to Rust's notion of
removed without replacement as it was unused, and its functionality will be
fully reproduced by `SaplingInputSource::select_spendable_sapling_notes` in a future
change.
- `zcash_client_backend::data_api::ScannedBlock::into_sapling_commitments` has been
replaced by `into_commitments` which returns both Sapling and Orchard note commitments
and associated note commitment retention information for the block.
## [0.10.0] - 2023-09-25

View File

@ -2,7 +2,7 @@
use std::{
collections::{BTreeMap, HashMap},
fmt::{self, Debug},
fmt::Debug,
io,
num::{NonZeroU32, TryFromIntError},
};
@ -18,12 +18,12 @@ use zcash_primitives::{
sapling::{self, Node, NOTE_COMMITMENT_TREE_DEPTH},
transaction::{
components::{
amount::{Amount, NonNegativeAmount},
amount::{Amount, BalanceError, NonNegativeAmount},
OutPoint,
},
Transaction, TxId,
},
zip32::AccountId,
zip32::{AccountId, Scope},
};
use crate::{
@ -31,7 +31,7 @@ use crate::{
decrypt::DecryptedOutput,
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::service::TreeState,
wallet::{ReceivedSaplingNote, WalletTransparentOutput, WalletTx},
wallet::{NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
};
use self::chain::CommitmentTreeRoot;
@ -58,19 +58,9 @@ pub enum NullifierQuery {
/// Balance information for a value within a single pool in an account.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Balance {
/// The value in the account that may currently be spent; it is possible to compute witnesses
/// for all the notes that comprise this value, and all of this value is confirmed to the
/// required confirmation depth.
pub spendable_value: NonNegativeAmount,
/// The value in the account of shielded change notes that do not yet have sufficient
/// confirmations to be spendable.
pub change_pending_confirmation: NonNegativeAmount,
/// The value in the account of all remaining received notes that either do not have sufficient
/// confirmations to be spendable, or for which witnesses cannot yet be constructed without
/// additional scanning.
pub value_pending_spendability: NonNegativeAmount,
spendable_value: NonNegativeAmount,
change_pending_confirmation: NonNegativeAmount,
value_pending_spendability: NonNegativeAmount,
}
impl Balance {
@ -81,6 +71,64 @@ impl Balance {
value_pending_spendability: NonNegativeAmount::ZERO,
};
fn check_total_adding(
&self,
value: NonNegativeAmount,
) -> Result<NonNegativeAmount, BalanceError> {
(self.spendable_value
+ self.change_pending_confirmation
+ self.value_pending_spendability
+ value)
.ok_or(BalanceError::Overflow)
}
/// Returns the value in the account that may currently be spent; it is possible to compute
/// witnesses for all the notes that comprise this value, and all of this value is confirmed to
/// the required confirmation depth.
pub fn spendable_value(&self) -> NonNegativeAmount {
self.spendable_value
}
/// Adds the specified value to the spendable total, checking for overflow.
pub fn add_spendable_value(&mut self, value: NonNegativeAmount) -> Result<(), BalanceError> {
self.check_total_adding(value)?;
self.spendable_value = (self.spendable_value + value).unwrap();
Ok(())
}
/// Returns the value in the account of shielded change notes that do not yet have sufficient
/// confirmations to be spendable.
pub fn change_pending_confirmation(&self) -> NonNegativeAmount {
self.change_pending_confirmation
}
/// Adds the specified value to the pending change total, checking for overflow.
pub fn add_pending_change_value(
&mut self,
value: NonNegativeAmount,
) -> Result<(), BalanceError> {
self.check_total_adding(value)?;
self.change_pending_confirmation = (self.change_pending_confirmation + value).unwrap();
Ok(())
}
/// Returns the value in the account of all remaining received notes that either do not have
/// sufficient confirmations to be spendable, or for which witnesses cannot yet be constructed
/// without additional scanning.
pub fn value_pending_spendability(&self) -> NonNegativeAmount {
self.value_pending_spendability
}
/// Adds the specified value to the pending spendable total, checking for overflow.
pub fn add_pending_spendable_value(
&mut self,
value: NonNegativeAmount,
) -> Result<(), BalanceError> {
self.check_total_adding(value)?;
self.value_pending_spendability = (self.value_pending_spendability + value).unwrap();
Ok(())
}
/// Returns the total value of funds represented by this [`Balance`].
pub fn total(&self) -> NonNegativeAmount {
(self.spendable_value + self.change_pending_confirmation + self.value_pending_spendability)
@ -93,7 +141,10 @@ impl Balance {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AccountBalance {
/// The value of unspent Sapling outputs belonging to the account.
pub sapling_balance: Balance,
sapling_balance: Balance,
/// The value of unspent Orchard outputs belonging to the account.
orchard_balance: Balance,
/// The value of all unspent transparent outputs belonging to the account, irrespective of
/// confirmation depth.
@ -102,19 +153,95 @@ pub struct AccountBalance {
/// possible operation on a transparent balance is to shield it, it is possible to create a
/// zero-conf transaction to perform that shielding, and the resulting shielded notes will be
/// subject to normal confirmation rules.
pub unshielded: NonNegativeAmount,
unshielded: NonNegativeAmount,
}
impl AccountBalance {
/// The [`Balance`] value having zero values for all its fields.
pub const ZERO: Self = Self {
sapling_balance: Balance::ZERO,
orchard_balance: Balance::ZERO,
unshielded: NonNegativeAmount::ZERO,
};
fn check_total(&self) -> Result<NonNegativeAmount, BalanceError> {
(self.sapling_balance.total() + self.orchard_balance.total() + self.unshielded)
.ok_or(BalanceError::Overflow)
}
/// Returns the [`Balance`] of Sapling funds in the account.
pub fn sapling_balance(&self) -> &Balance {
&self.sapling_balance
}
/// Provides a `mutable reference to the [`Balance`] of Sapling funds in the account
/// to the specified callback, checking invariants after the callback's action has been
/// evaluated.
pub fn with_sapling_balance_mut<A, E: From<BalanceError>>(
&mut self,
f: impl FnOnce(&mut Balance) -> Result<A, E>,
) -> Result<A, E> {
let result = f(&mut self.sapling_balance)?;
self.check_total()?;
Ok(result)
}
/// Returns the [`Balance`] of Orchard funds in the account.
pub fn orchard_balance(&self) -> &Balance {
&self.orchard_balance
}
/// Provides a `mutable reference to the [`Balance`] of Orchard funds in the account
/// to the specified callback, checking invariants after the callback's action has been
/// evaluated.
pub fn with_orchard_balance_mut<A, E: From<BalanceError>>(
&mut self,
f: impl FnOnce(&mut Balance) -> Result<A, E>,
) -> Result<A, E> {
let result = f(&mut self.orchard_balance)?;
self.check_total()?;
Ok(result)
}
/// Returns the total value of unspent transparent transaction outputs belonging to the wallet.
pub fn unshielded(&self) -> NonNegativeAmount {
self.unshielded
}
/// Adds the specified value to the unshielded total, checking for overflow of
/// the total account balance.
pub fn add_unshielded_value(&mut self, value: NonNegativeAmount) -> Result<(), BalanceError> {
self.unshielded = (self.unshielded + value).ok_or(BalanceError::Overflow)?;
self.check_total()?;
Ok(())
}
/// Returns the total value of funds belonging to the account.
pub fn total(&self) -> NonNegativeAmount {
(self.sapling_balance.total() + self.unshielded)
(self.sapling_balance.total() + self.orchard_balance.total() + self.unshielded)
.expect("Account balance cannot overflow MAX_MONEY")
}
/// Returns the total value of shielded (Sapling and Orchard) funds that may immediately be
/// spent.
pub fn spendable_value(&self) -> NonNegativeAmount {
(self.sapling_balance.spendable_value + self.orchard_balance.spendable_value)
.expect("Account balance cannot overflow MAX_MONEY")
}
/// Returns the total value of change and/or shielding transaction outputs that are awaiting
/// sufficient confirmations for spendability.
pub fn change_pending_confirmation(&self) -> NonNegativeAmount {
(self.sapling_balance.change_pending_confirmation
+ self.orchard_balance.change_pending_confirmation)
.expect("Account balance cannot overflow MAX_MONEY")
}
/// Returns the value of shielded funds that are not yet spendable because additional scanning
/// is required before it will be possible to derive witnesses for the associated notes.
pub fn value_pending_spendability(&self) -> NonNegativeAmount {
(self.sapling_balance.value_pending_spendability
+ self.orchard_balance.value_pending_spendability)
.expect("Account balance cannot overflow MAX_MONEY")
}
}
@ -231,7 +358,7 @@ pub trait SaplingInputSource {
&self,
txid: &TxId,
index: u32,
) -> Result<Option<ReceivedSaplingNote<Self::NoteRef>>, Self::Error>;
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error>;
/// Returns a list of spendable Sapling notes sufficient to cover the specified target value,
/// if possible.
@ -241,7 +368,7 @@ pub trait SaplingInputSource {
target_value: Amount,
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error>;
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error>;
}
/// A trait representing the capability to query a data store for unspent transparent UTXOs
@ -475,18 +602,20 @@ impl BlockMetadata {
/// decrypted and extracted from a [`CompactBlock`].
///
/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock
pub struct ScannedBlock<Nf> {
pub struct ScannedBlock<Nf, S> {
block_height: BlockHeight,
block_hash: BlockHash,
block_time: u32,
sapling_tree_size: u32,
orchard_tree_size: u32,
transactions: Vec<WalletTx<Nf>>,
transactions: Vec<WalletTx<Nf, S>>,
sapling_nullifier_map: Vec<(TxId, u16, Vec<sapling::Nullifier>)>,
sapling_commitments: Vec<(sapling::Node, Retention<BlockHeight>)>,
orchard_nullifier_map: Vec<(TxId, u16, Vec<orchard::note::Nullifier>)>,
orchard_commitments: Vec<(orchard::note::NoteCommitment, Retention<BlockHeight>)>,
}
impl<Nf> ScannedBlock<Nf> {
impl<Nf, S> ScannedBlock<Nf, S> {
/// Constructs a new `ScannedBlock`
#[allow(clippy::too_many_arguments)]
pub fn from_parts(
@ -495,9 +624,11 @@ impl<Nf> ScannedBlock<Nf> {
block_time: u32,
sapling_tree_size: u32,
orchard_tree_size: u32,
transactions: Vec<WalletTx<Nf>>,
transactions: Vec<WalletTx<Nf, S>>,
sapling_nullifier_map: Vec<(TxId, u16, Vec<sapling::Nullifier>)>,
sapling_commitments: Vec<(sapling::Node, Retention<BlockHeight>)>,
orchard_nullifier_map: Vec<(TxId, u16, Vec<orchard::note::Nullifier>)>,
orchard_commitments: Vec<(orchard::note::NoteCommitment, Retention<BlockHeight>)>,
) -> Self {
Self {
block_height,
@ -508,6 +639,8 @@ impl<Nf> ScannedBlock<Nf> {
transactions,
sapling_nullifier_map,
sapling_commitments,
orchard_nullifier_map,
orchard_commitments,
}
}
@ -537,7 +670,7 @@ impl<Nf> ScannedBlock<Nf> {
}
/// Returns the list of transactions from the block that are relevant to the wallet.
pub fn transactions(&self) -> &[WalletTx<Nf>] {
pub fn transactions(&self) -> &[WalletTx<Nf, S>] {
&self.transactions
}
@ -557,10 +690,34 @@ impl<Nf> ScannedBlock<Nf> {
&self.sapling_commitments
}
/// Consumes `self` and returns the list of Sapling note commitments associated with the
/// scanned block as an owned value.
pub fn into_sapling_commitments(self) -> Vec<(sapling::Node, Retention<BlockHeight>)> {
self.sapling_commitments
/// Returns the vector of Orchard nullifiers for each transaction in the block.
///
/// The returned tuple is keyed by both transaction ID and the index of the transaction within
/// the block, so that either the txid or the combination of the block hash available from
/// [`Self::block_hash`] and returned transaction index may be used to uniquely identify the
/// transaction, depending upon the needs of the caller.
pub fn orchard_nullifier_map(&self) -> &[(TxId, u16, Vec<orchard::note::Nullifier>)] {
&self.orchard_nullifier_map
}
/// Returns the ordered list of Orchard note commitments to be added to the note commitment
/// tree.
pub fn orchard_commitments(
&self,
) -> &[(orchard::note::NoteCommitment, Retention<BlockHeight>)] {
&self.orchard_commitments
}
/// Consumes `self` and returns the lists of Sapling and Orchard note commitments associated
/// with the scanned block as an owned value.
#[allow(clippy::type_complexity)]
pub fn into_commitments(
self,
) -> (
Vec<(sapling::Node, Retention<BlockHeight>)>,
Vec<(orchard::note::NoteCommitment, Retention<BlockHeight>)>,
) {
(self.sapling_commitments, self.orchard_commitments)
}
/// Returns the [`BlockMetadata`] corresponding to the scanned block.
@ -599,81 +756,6 @@ pub struct SentTransaction<'a> {
pub utxos_spent: Vec<OutPoint>,
}
/// A shielded transfer protocol supported by the wallet.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ShieldedProtocol {
/// The Sapling protocol
Sapling,
/// The Orchard protocol
Orchard,
}
/// A unique identifier for a shielded transaction output
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NoteId {
txid: TxId,
protocol: ShieldedProtocol,
output_index: u16,
}
impl NoteId {
/// Constructs a new `NoteId` from its parts.
pub fn new(txid: TxId, protocol: ShieldedProtocol, output_index: u16) -> Self {
Self {
txid,
protocol,
output_index,
}
}
/// Returns the ID of the transaction containing this note.
pub fn txid(&self) -> &TxId {
&self.txid
}
/// Returns the shielded protocol used by this note.
pub fn protocol(&self) -> ShieldedProtocol {
self.protocol
}
/// Returns the index of this note within its transaction's corresponding list of
/// shielded outputs.
pub fn output_index(&self) -> u16 {
self.output_index
}
}
/// A value pool to which the wallet supports sending transaction outputs.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum PoolType {
/// The transparent value pool
Transparent,
/// A shielded value pool.
Shielded(ShieldedProtocol),
}
impl fmt::Display for PoolType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PoolType::Transparent => f.write_str("Transparent"),
PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"),
PoolType::Shielded(ShieldedProtocol::Orchard) => f.write_str("Orchard"),
}
}
}
/// A type that represents the recipient of a transaction output; a recipient address (and, for
/// unified addresses, the pool to which the payment is sent) in the case of outgoing output, or an
/// internal account ID and the pool to which funds were sent in the case of a wallet-internal
/// output.
#[derive(Debug, Clone)]
pub enum Recipient {
Transparent(TransparentAddress),
Sapling(sapling::PaymentAddress),
Unified(UnifiedAddress, PoolType),
InternalAccount(AccountId, PoolType),
}
/// A type that represents an output (either Sapling or transparent) that was sent by the wallet.
pub struct SentTransactionOutput {
output_index: usize,
@ -899,7 +981,7 @@ pub trait WalletWrite: WalletRead {
/// `blocks` must be sequential, in order of increasing block height
fn put_blocks(
&mut self,
blocks: Vec<ScannedBlock<sapling::Nullifier>>,
blocks: Vec<ScannedBlock<sapling::Nullifier, Scope>>,
) -> Result<(), Self::Error>;
/// Updates the wallet's view of the blockchain.
@ -986,20 +1068,19 @@ pub mod testing {
memo::Memo,
sapling,
transaction::{components::Amount, Transaction, TxId},
zip32::AccountId,
zip32::{AccountId, Scope},
};
use crate::{
address::{AddressMetadata, UnifiedAddress},
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
wallet::{NoteId, ReceivedNote, WalletTransparentOutput},
};
use super::{
chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata,
DecryptedTransaction, NoteId, NullifierQuery, SaplingInputSource, ScannedBlock,
SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite,
SAPLING_SHARD_HEIGHT,
DecryptedTransaction, NullifierQuery, SaplingInputSource, ScannedBlock, SentTransaction,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
};
pub struct MockWalletDb {
@ -1031,7 +1112,7 @@ pub mod testing {
&self,
_txid: &TxId,
_index: u32,
) -> Result<Option<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error> {
Ok(None)
}
@ -1041,7 +1122,7 @@ pub mod testing {
_target_value: Amount,
_anchor_height: BlockHeight,
_exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error> {
Ok(Vec::new())
}
}
@ -1209,7 +1290,7 @@ pub mod testing {
#[allow(clippy::type_complexity)]
fn put_blocks(
&mut self,
_blocks: Vec<ScannedBlock<sapling::Nullifier>>,
_blocks: Vec<ScannedBlock<sapling::Nullifier, Scope>>,
) -> Result<(), Self::Error> {
Ok(())
}

View File

@ -15,12 +15,12 @@ use zcash_primitives::{
};
use crate::data_api::wallet::input_selection::InputSelectorError;
use crate::data_api::PoolType;
use crate::PoolType;
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::{legacy::TransparentAddress, zip32::DiversifierIndex};
use super::NoteId;
use crate::wallet::NoteId;
/// Errors that can occur as a consequence of wallet operations.
#[derive(Debug)]

View File

@ -1,16 +1,11 @@
use std::num::NonZeroU32;
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
use zcash_primitives::{
consensus::{self, BlockHeight, NetworkUpgrade},
consensus::{self, NetworkUpgrade},
memo::MemoBytes,
sapling::{
self,
note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey},
prover::{OutputProver, SpendProver},
zip32::DiversifiableFullViewingKey,
Node,
},
transaction::{
builder::Builder,
@ -24,15 +19,15 @@ use zcash_primitives::{
use crate::{
address::RecipientAddress,
data_api::{
error::Error, wallet::input_selection::Proposal, DecryptedTransaction, PoolType, Recipient,
SentTransaction, SentTransactionOutput, WalletCommitmentTrees, WalletRead, WalletWrite,
SAPLING_SHARD_HEIGHT,
error::Error, wallet::input_selection::Proposal, DecryptedTransaction, SentTransaction,
SentTransactionOutput, WalletCommitmentTrees, WalletRead, WalletWrite,
},
decrypt_transaction,
fees::{self, ChangeValue, DustOutputPolicy},
keys::UnifiedSpendingKey,
wallet::{OvkPolicy, ReceivedSaplingNote},
wallet::{OvkPolicy, Recipient, WalletNote},
zip321::{self, Payment},
PoolType, ShieldedProtocol,
};
pub mod input_selection;
@ -40,7 +35,7 @@ use input_selection::{
GreedyInputSelector, GreedyInputSelectorError, InputSelector, InputSelectorError,
};
use super::{NoteId, SaplingInputSource, ShieldedProtocol};
use super::SaplingInputSource;
#[cfg(feature = "transparent-inputs")]
use {
@ -590,26 +585,26 @@ where
if let Some(sapling_inputs) = proposal.sapling_inputs() {
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {
for selected in sapling_inputs.notes() {
let (note, scope, merkle_path) = select_key_for_note(
sapling_tree,
selected,
&dfvk,
sapling_inputs.anchor_height(),
)?
.ok_or_else(|| {
Error::NoteMismatch(NoteId {
txid: *selected.txid(),
protocol: ShieldedProtocol::Sapling,
output_index: selected.output_index(),
})
})?;
match selected.note() {
WalletNote::Sapling(note) => {
let key = match selected.spending_key_scope() {
Scope::External => usk.sapling().clone(),
Scope::Internal => usk.sapling().derive_internal(),
};
let key = match scope {
Scope::External => usk.sapling().clone(),
Scope::Internal => usk.sapling().derive_internal(),
};
let merkle_path = sapling_tree.witness_at_checkpoint_id_caching(
selected.note_commitment_tree_position(),
&sapling_inputs.anchor_height(),
)?;
builder.add_sapling_spend(key, note, merkle_path)?;
builder.add_sapling_spend(key, note.clone(), merkle_path)?;
}
WalletNote::Orchard(_) => {
// FIXME: Implement this once `Proposal` has been refactored to
// include Orchard notes.
panic!("Orchard spends are not yet supported");
}
}
}
Ok(())
})?;
@ -880,40 +875,3 @@ where
&proposal,
)
}
#[allow(clippy::type_complexity)]
fn select_key_for_note<N, S: ShardStore<H = Node, CheckpointId = BlockHeight>>(
commitment_tree: &mut ShardTree<
S,
{ sapling::NOTE_COMMITMENT_TREE_DEPTH },
SAPLING_SHARD_HEIGHT,
>,
selected: &ReceivedSaplingNote<N>,
dfvk: &DiversifiableFullViewingKey,
anchor_height: BlockHeight,
) -> Result<Option<(sapling::Note, Scope, sapling::MerklePath)>, ShardTreeError<S::Error>> {
// Attempt to reconstruct the note being spent using both the internal and external dfvks
// corresponding to the unified spending key, checking against the witness we are using
// to spend the note that we've used the correct key.
let external_note = dfvk
.diversified_address(selected.diversifier())
.map(|addr| addr.create_note(selected.value().try_into().unwrap(), selected.rseed()));
let internal_note = dfvk
.diversified_change_address(selected.diversifier())
.map(|addr| addr.create_note(selected.value().try_into().unwrap(), selected.rseed()));
let expected_root = commitment_tree.root_at_checkpoint_id(&anchor_height)?;
let merkle_path = commitment_tree.witness_at_checkpoint_id_caching(
selected.note_commitment_tree_position(),
&anchor_height,
)?;
Ok(external_note
.filter(|n| expected_root == merkle_path.root(Node::from_cmu(&n.cmu())))
.map(|n| (n, Scope::External, merkle_path.clone()))
.or_else(|| {
internal_note
.filter(|n| expected_root == merkle_path.root(Node::from_cmu(&n.cmu())))
.map(|n| (n, Scope::Internal, merkle_path))
}))
}

View File

@ -22,7 +22,7 @@ use crate::{
address::{RecipientAddress, UnifiedAddress},
data_api::SaplingInputSource,
fees::{ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance},
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
wallet::{ReceivedNote, WalletTransparentOutput},
zip321::TransactionRequest,
};
@ -263,15 +263,12 @@ impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
#[derive(Clone, PartialEq, Eq)]
pub struct SaplingInputs<NoteRef> {
anchor_height: BlockHeight,
notes: NonEmpty<ReceivedSaplingNote<NoteRef>>,
notes: NonEmpty<ReceivedNote<NoteRef>>,
}
impl<NoteRef> SaplingInputs<NoteRef> {
/// Constructs a [`SaplingInputs`] from its constituent parts.
pub fn from_parts(
anchor_height: BlockHeight,
notes: NonEmpty<ReceivedSaplingNote<NoteRef>>,
) -> Self {
pub fn from_parts(anchor_height: BlockHeight, notes: NonEmpty<ReceivedNote<NoteRef>>) -> Self {
Self {
anchor_height,
notes,
@ -285,7 +282,7 @@ impl<NoteRef> SaplingInputs<NoteRef> {
}
/// Returns the list of Sapling notes to be used as inputs to the proposed transaction.
pub fn notes(&self) -> &NonEmpty<ReceivedSaplingNote<NoteRef>> {
pub fn notes(&self) -> &NonEmpty<ReceivedNote<NoteRef>> {
&self.notes
}
}
@ -534,7 +531,7 @@ where
}
}
let mut sapling_inputs: Vec<ReceivedSaplingNote<DbT::NoteRef>> = vec![];
let mut sapling_inputs: Vec<ReceivedNote<DbT::NoteRef>> = vec![];
let mut prior_available = NonNegativeAmount::ZERO;
let mut amount_required = NonNegativeAmount::ZERO;
let mut exclude: Vec<DbT::NoteRef> = vec![];
@ -654,7 +651,7 @@ where
target_height,
&transparent_inputs,
&Vec::<TxOut>::new(),
&Vec::<ReceivedSaplingNote<Infallible>>::new(),
&Vec::<ReceivedNote<Infallible>>::new(),
&Vec::<SaplingPayment>::new(),
&self.dust_output_policy,
);
@ -670,7 +667,7 @@ where
target_height,
&transparent_inputs,
&Vec::<TxOut>::new(),
&Vec::<ReceivedSaplingNote<Infallible>>::new(),
&Vec::<ReceivedNote<Infallible>>::new(),
&Vec::<SaplingPayment>::new(),
&self.dust_output_policy,
)?

View File

@ -23,8 +23,38 @@ pub mod zip321;
#[cfg(feature = "unstable-serialization")]
pub mod serialization;
use std::fmt;
pub use decrypt::{decrypt_transaction, DecryptedOutput, TransferType};
#[cfg(test)]
#[macro_use]
extern crate assert_matches;
/// A shielded transfer protocol supported by the wallet.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ShieldedProtocol {
/// The Sapling protocol
Sapling,
/// The Orchard protocol
Orchard,
}
/// A value pool to which the wallet supports sending transaction outputs.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum PoolType {
/// The transparent value pool
Transparent,
/// A shielded value pool.
Shielded(ShieldedProtocol),
}
impl fmt::Display for PoolType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PoolType::Transparent => f.write_str("Transparent"),
PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"),
PoolType::Shielded(ShieldedProtocol::Orchard) => f.write_str("Orchard"),
}
}
}

View File

@ -27,10 +27,11 @@ use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
use crate::{
data_api::{
wallet::input_selection::{Proposal, ProposalError, SaplingInputs},
PoolType, SaplingInputSource, ShieldedProtocol, TransparentInputSource,
SaplingInputSource, TransparentInputSource,
},
fees::{ChangeValue, TransactionBalance},
zip321::{TransactionRequest, Zip321Error},
PoolType, ShieldedProtocol,
};
#[rustfmt::skip]

View File

@ -19,11 +19,12 @@ use zcash_primitives::{
zip32::{AccountId, Scope},
};
use crate::data_api::{BlockMetadata, ScannedBlock, ShieldedProtocol};
use crate::data_api::{BlockMetadata, ScannedBlock};
use crate::{
proto::compact_formats::CompactBlock,
scan::{Batch, BatchRunner, Tasks},
wallet::{WalletSaplingOutput, WalletSaplingSpend, WalletTx},
ShieldedProtocol,
};
/// A key that can be used to perform trial decryption and nullifier
@ -257,7 +258,7 @@ pub fn scan_block<P: consensus::Parameters + Send + 'static, K: ScanningKey>(
vks: &[(&AccountId, &K)],
sapling_nullifiers: &[(AccountId, sapling::Nullifier)],
prior_block_metadata: Option<&BlockMetadata>,
) -> Result<ScannedBlock<K::Nf>, ScanError> {
) -> Result<ScannedBlock<K::Nf, K::Scope>, ScanError> {
scan_block_with_runner::<_, _, ()>(
params,
block,
@ -340,7 +341,7 @@ pub(crate) fn scan_block_with_runner<
nullifiers: &[(AccountId, sapling::Nullifier)],
prior_block_metadata: Option<&BlockMetadata>,
mut batch_runner: Option<&mut TaggedBatchRunner<K::Scope, T>>,
) -> Result<ScannedBlock<K::Nf>, ScanError> {
) -> Result<ScannedBlock<K::Nf, K::Scope>, ScanError> {
if let Some(scan_error) = check_hash_continuity(&block, prior_block_metadata) {
return Err(scan_error);
}
@ -440,7 +441,7 @@ pub(crate) fn scan_block_with_runner<
)?;
let compact_block_tx_count = block.vtx.len();
let mut wtxs: Vec<WalletTx<K::Nf>> = vec![];
let mut wtxs: Vec<WalletTx<K::Nf, K::Scope>> = vec![];
let mut sapling_nullifier_map = Vec::with_capacity(block.vtx.len());
let mut sapling_note_commitments: Vec<(sapling::Node, Retention<BlockHeight>)> = vec![];
for (tx_idx, tx) in block.vtx.into_iter().enumerate() {
@ -493,7 +494,7 @@ pub(crate) fn scan_block_with_runner<
u32::try_from(tx.actions.len()).expect("Orchard action count cannot exceed a u32");
// Check for incoming notes while incrementing tree and witnesses
let mut shielded_outputs: Vec<WalletSaplingOutput<K::Nf>> = vec![];
let mut shielded_outputs: Vec<WalletSaplingOutput<K::Nf, K::Scope>> = vec![];
{
let decoded = &tx
.outputs
@ -526,7 +527,7 @@ pub(crate) fn scan_block_with_runner<
"The batch runner and scan_block must use the same set of IVKs.",
);
(d_note.note, a, (*nk).clone())
(d_note.note, a, d_note.ivk_tag.1, (*nk).clone())
})
})
.collect()
@ -536,13 +537,13 @@ pub(crate) fn scan_block_with_runner<
.flat_map(|(a, k)| {
k.to_sapling_keys()
.into_iter()
.map(move |(_, ivk, nk)| (**a, ivk, nk))
.map(move |(scope, ivk, nk)| (**a, scope, ivk, nk))
})
.collect::<Vec<_>>();
let ivks = vks
.iter()
.map(|(_, ivk, _)| ivk)
.map(|(_, _, ivk, _)| ivk)
.map(PreparedIncomingViewingKey::new)
.collect::<Vec<_>>();
@ -550,8 +551,8 @@ pub(crate) fn scan_block_with_runner<
.into_iter()
.map(|v| {
v.map(|((note, _), ivk_idx)| {
let (account, _, nk) = &vks[ivk_idx];
(note, *account, (*nk).clone())
let (account, scope, _, nk) = &vks[ivk_idx];
(note, *account, scope.clone(), (*nk).clone())
})
})
.collect()
@ -572,7 +573,7 @@ pub(crate) fn scan_block_with_runner<
(false, false) => Retention::Ephemeral,
};
if let Some((note, account, nk)) = dec_output {
if let Some((note, account, scope, nk)) = dec_output {
// A note is marked as "change" if the account that received it
// also spent notes in the same transaction. This will catch,
// for instance:
@ -594,6 +595,7 @@ pub(crate) fn scan_block_with_runner<
is_change,
note_commitment_tree_position,
nf,
scope,
));
}
@ -643,6 +645,8 @@ pub(crate) fn scan_block_with_runner<
wtxs,
sapling_nullifier_map,
sapling_note_commitments,
vec![], // FIXME: collect the Orchard nullifiers
vec![], // FIXME: collect the Orchard note commitments
))
}

View File

@ -15,17 +15,66 @@ use zcash_primitives::{
},
TxId,
},
zip32::AccountId,
zip32::{AccountId, Scope},
};
use crate::{address::UnifiedAddress, PoolType, ShieldedProtocol};
/// A unique identifier for a shielded transaction output
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NoteId {
txid: TxId,
protocol: ShieldedProtocol,
output_index: u16,
}
impl NoteId {
/// Constructs a new `NoteId` from its parts.
pub fn new(txid: TxId, protocol: ShieldedProtocol, output_index: u16) -> Self {
Self {
txid,
protocol,
output_index,
}
}
/// Returns the ID of the transaction containing this note.
pub fn txid(&self) -> &TxId {
&self.txid
}
/// Returns the shielded protocol used by this note.
pub fn protocol(&self) -> ShieldedProtocol {
self.protocol
}
/// Returns the index of this note within its transaction's corresponding list of
/// shielded outputs.
pub fn output_index(&self) -> u16 {
self.output_index
}
}
/// A type that represents the recipient of a transaction output: a recipient address (and, for
/// unified addresses, the pool to which the payment is sent) in the case of an outgoing output, or an
/// internal account ID and the pool to which funds were sent in the case of a wallet-internal
/// output.
#[derive(Debug, Clone)]
pub enum Recipient {
Transparent(TransparentAddress),
Sapling(sapling::PaymentAddress),
Unified(UnifiedAddress, PoolType),
InternalAccount(AccountId, PoolType),
}
/// A subset of a [`Transaction`] relevant to wallets and light clients.
///
/// [`Transaction`]: zcash_primitives::transaction::Transaction
pub struct WalletTx<N> {
pub struct WalletTx<N, S> {
pub txid: TxId,
pub index: usize,
pub sapling_spends: Vec<WalletSaplingSpend>,
pub sapling_outputs: Vec<WalletSaplingOutput<N>>,
pub sapling_outputs: Vec<WalletSaplingOutput<N, S>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -109,8 +158,17 @@ impl WalletSaplingSpend {
/// A subset of an [`OutputDescription`] relevant to wallets and light clients.
///
/// The type parameter `<N>` is used to specify the nullifier type, which may vary between
/// `Sapling` and `Orchard`, and also may vary depending upon the type of key that was used to
/// decrypt this output; incoming viewing keys do not have the capability to derive the nullifier
/// for a note, and the `<N>` will be `()` in these cases.
///
/// The type parameter `<S>` is used to specify the type of the scope of the key used to recover
/// this output; this will usually be [`zcash_primitives::zip32::Scope`] for received notes, and
/// `()` for sent notes.
///
/// [`OutputDescription`]: zcash_primitives::transaction::components::OutputDescription
pub struct WalletSaplingOutput<N> {
pub struct WalletSaplingOutput<N, S> {
index: usize,
cmu: sapling::note::ExtractedNoteCommitment,
ephemeral_key: EphemeralKeyBytes,
@ -119,9 +177,10 @@ pub struct WalletSaplingOutput<N> {
is_change: bool,
note_commitment_tree_position: Position,
nf: N,
recipient_key_scope: S,
}
impl<N> WalletSaplingOutput<N> {
impl<N, S> WalletSaplingOutput<N, S> {
/// Constructs a new `WalletSaplingOutput` value from its constituent parts.
#[allow(clippy::too_many_arguments)]
pub fn from_parts(
@ -133,6 +192,7 @@ impl<N> WalletSaplingOutput<N> {
is_change: bool,
note_commitment_tree_position: Position,
nf: N,
recipient_key_scope: S,
) -> Self {
Self {
index,
@ -143,6 +203,7 @@ impl<N> WalletSaplingOutput<N> {
is_change,
note_commitment_tree_position,
nf,
recipient_key_scope,
}
}
@ -170,38 +231,58 @@ impl<N> WalletSaplingOutput<N> {
pub fn nf(&self) -> &N {
&self.nf
}
pub fn recipient_key_scope(&self) -> &S {
&self.recipient_key_scope
}
}
/// An enumeration of supported shielded note types for use in [`ReceivedNote`]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WalletNote {
Sapling(sapling::Note),
Orchard(orchard::Note),
}
impl WalletNote {
pub fn value(&self) -> NonNegativeAmount {
match self {
WalletNote::Sapling(n) => n.value().try_into().expect(
"Sapling notes must have values in the range of valid non-negative ZEC values.",
),
WalletNote::Orchard(n) => NonNegativeAmount::from_u64(n.value().inner()).expect(
"Orchard notes must have values in the range of valid non-negative ZEC values.",
),
}
}
}
/// Information about a note that is tracked by the wallet that is available for spending,
/// with sufficient information for use in note selection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceivedSaplingNote<NoteRef> {
pub struct ReceivedNote<NoteRef> {
note_id: NoteRef,
txid: TxId,
output_index: u16,
diversifier: sapling::Diversifier,
note_value: NonNegativeAmount,
rseed: sapling::Rseed,
note: WalletNote,
spending_key_scope: Scope,
note_commitment_tree_position: Position,
}
impl<NoteRef> ReceivedSaplingNote<NoteRef> {
impl<NoteRef> ReceivedNote<NoteRef> {
pub fn from_parts(
note_id: NoteRef,
txid: TxId,
output_index: u16,
diversifier: sapling::Diversifier,
note_value: NonNegativeAmount,
rseed: sapling::Rseed,
note: WalletNote,
spending_key_scope: Scope,
note_commitment_tree_position: Position,
) -> Self {
ReceivedSaplingNote {
ReceivedNote {
note_id,
txid,
output_index,
diversifier,
note_value,
rseed,
note,
spending_key_scope,
note_commitment_tree_position,
}
}
@ -209,34 +290,33 @@ impl<NoteRef> ReceivedSaplingNote<NoteRef> {
pub fn internal_note_id(&self) -> &NoteRef {
&self.note_id
}
pub fn txid(&self) -> &TxId {
&self.txid
}
pub fn output_index(&self) -> u16 {
self.output_index
}
pub fn diversifier(&self) -> sapling::Diversifier {
self.diversifier
pub fn note(&self) -> &WalletNote {
&self.note
}
pub fn value(&self) -> NonNegativeAmount {
self.note_value
self.note.value()
}
pub fn rseed(&self) -> sapling::Rseed {
self.rseed
pub fn spending_key_scope(&self) -> Scope {
self.spending_key_scope
}
pub fn note_commitment_tree_position(&self) -> Position {
self.note_commitment_tree_position
}
}
impl<NoteRef> sapling_fees::InputView<NoteRef> for ReceivedSaplingNote<NoteRef> {
impl<NoteRef> sapling_fees::InputView<NoteRef> for ReceivedNote<NoteRef> {
fn note_id(&self) -> &NoteRef {
&self.note_id
}
fn value(&self) -> NonNegativeAmount {
self.note_value
self.note.value()
}
}

View File

@ -8,8 +8,9 @@ and this library adheres to Rust's notion of
## [Unreleased]
### Changed
- `zcash_client_sqlite::error::SqliteClientError` has new error variant:
- `zcash_client_sqlite::error::SqliteClientError` has new error variants:
- `SqliteClientError::UnsupportedPoolType`
- `SqliteClientError::BalanceError`
## [0.8.1] - 2023-10-18

View File

@ -4,9 +4,13 @@ use std::error;
use std::fmt;
use shardtree::error::ShardTreeError;
use zcash_client_backend::data_api::PoolType;
use zcash_client_backend::encoding::{Bech32DecodeError, TransparentCodecError};
use zcash_primitives::{consensus::BlockHeight, zip32::AccountId};
use zcash_client_backend::{
encoding::{Bech32DecodeError, TransparentCodecError},
PoolType,
};
use zcash_primitives::{
consensus::BlockHeight, transaction::components::amount::BalanceError, zip32::AccountId,
};
use crate::wallet::commitment_tree;
use crate::PRUNING_DEPTH;
@ -102,6 +106,9 @@ pub enum SqliteClientError {
/// Unsupported pool type
UnsupportedPoolType(PoolType),
/// An error occurred in computing wallet balance
BalanceError(BalanceError),
}
impl error::Error for SqliteClientError {
@ -111,6 +118,7 @@ impl error::Error for SqliteClientError {
SqliteClientError::Bech32DecodeError(Bech32DecodeError::Bech32Error(e)) => Some(e),
SqliteClientError::DbError(e) => Some(e),
SqliteClientError::Io(e) => Some(e),
SqliteClientError::BalanceError(e) => Some(e),
_ => None,
}
}
@ -149,7 +157,8 @@ impl fmt::Display for SqliteClientError {
SqliteClientError::CommitmentTree(err) => write!(f, "An error occurred accessing or updating note commitment tree data: {}.", err),
SqliteClientError::CacheMiss(height) => write!(f, "Requested height {} does not exist in the block cache.", height),
SqliteClientError::ChainHeightUnknown => write!(f, "Chain height unknown; please call `update_chain_tip`"),
SqliteClientError::UnsupportedPoolType(t) => write!(f, "Pool type is not currently supported: {}", t)
SqliteClientError::UnsupportedPoolType(t) => write!(f, "Pool type is not currently supported: {}", t),
SqliteClientError::BalanceError(e) => write!(f, "Balance error: {}", e),
}
}
}
@ -202,3 +211,9 @@ impl From<ShardTreeError<commitment_tree::Error>> for SqliteClientError {
SqliteClientError::CommitmentTree(e)
}
}
impl From<BalanceError> for SqliteClientError {
fn from(e: BalanceError) -> Self {
SqliteClientError::BalanceError(e)
}
}

View File

@ -55,7 +55,7 @@ use zcash_primitives::{
components::amount::{Amount, NonNegativeAmount},
Transaction, TxId,
},
zip32::{AccountId, DiversifierIndex},
zip32::{AccountId, DiversifierIndex, Scope},
};
use zcash_client_backend::{
@ -64,14 +64,14 @@ use zcash_client_backend::{
self,
chain::{BlockSource, CommitmentTreeRoot},
scanning::{ScanPriority, ScanRange},
AccountBirthday, BlockMetadata, DecryptedTransaction, NoteId, NullifierQuery, PoolType,
Recipient, SaplingInputSource, ScannedBlock, SentTransaction, ShieldedProtocol,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
AccountBirthday, BlockMetadata, DecryptedTransaction, NullifierQuery, SaplingInputSource,
ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary,
WalletWrite, SAPLING_SHARD_HEIGHT,
},
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock,
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
DecryptedOutput, TransferType,
wallet::{NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
};
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
@ -177,8 +177,8 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> SaplingInputSour
&self,
txid: &TxId,
index: u32,
) -> Result<Option<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
wallet::sapling::get_spendable_sapling_note(self.conn.borrow(), txid, index)
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error> {
wallet::sapling::get_spendable_sapling_note(self.conn.borrow(), &self.params, txid, index)
}
fn select_spendable_sapling_notes(
@ -187,9 +187,10 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> SaplingInputSour
target_value: Amount,
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error> {
wallet::sapling::select_spendable_sapling_notes(
self.conn.borrow(),
&self.params,
account,
target_value,
anchor_height,
@ -439,7 +440,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
#[allow(clippy::type_complexity)]
fn put_blocks(
&mut self,
blocks: Vec<ScannedBlock<sapling::Nullifier>>,
blocks: Vec<ScannedBlock<sapling::Nullifier, Scope>>,
) -> Result<(), Self::Error> {
self.transactionally(|wdb| {
let start_positions = blocks.first().map(|block| {
@ -483,7 +484,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
for output in &tx.sapling_outputs {
// Check whether this note was spent in a later block range that
// we previously scanned.
let spent_in = wallet::query_nullifier_map(
let spent_in = wallet::query_nullifier_map::<_, Scope>(
wdb.conn.0,
ShieldedProtocol::Sapling,
output.nf(),
@ -508,7 +509,8 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
}));
last_scanned_height = Some(block.height());
sapling_commitments.extend(block.into_sapling_commitments().into_iter().map(Some));
let (block_sapling_commitments, _) = block.into_commitments();
sapling_commitments.extend(block_sapling_commitments.into_iter().map(Some));
}
// Prune the nullifier map of entries we no longer need.

View File

@ -708,7 +708,7 @@ impl<Cache> TestState<Cache> {
min_confirmations: u32,
) -> NonNegativeAmount {
self.with_account_balance(account, min_confirmations, |balance| {
balance.sapling_balance.spendable_value
balance.sapling_balance().spendable_value()
})
}
@ -718,8 +718,8 @@ impl<Cache> TestState<Cache> {
min_confirmations: u32,
) -> NonNegativeAmount {
self.with_account_balance(account, min_confirmations, |balance| {
balance.sapling_balance.value_pending_spendability
+ balance.sapling_balance.change_pending_confirmation
balance.sapling_balance().value_pending_spendability()
+ balance.sapling_balance().change_pending_confirmation()
})
.unwrap()
}
@ -731,7 +731,7 @@ impl<Cache> TestState<Cache> {
min_confirmations: u32,
) -> NonNegativeAmount {
self.with_account_balance(account, min_confirmations, |balance| {
balance.sapling_balance.change_pending_confirmation
balance.sapling_balance().change_pending_confirmation()
})
}

View File

@ -75,28 +75,27 @@ use std::ops::RangeInclusive;
use tracing::debug;
use zcash_client_backend::data_api::{AccountBalance, Ratio, WalletSummary};
use zcash_primitives::transaction::components::amount::NonNegativeAmount;
use zcash_client_backend::data_api::{
scanning::{ScanPriority, ScanRange},
AccountBirthday, NoteId, ShieldedProtocol, SAPLING_SHARD_HEIGHT,
};
use zcash_primitives::transaction::TransactionData;
use zcash_primitives::zip32::Scope;
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters},
memo::{Memo, MemoBytes},
merkle_tree::read_commitment_tree,
transaction::{components::Amount, Transaction, TxId},
transaction::{components::Amount, Transaction, TransactionData, TxId},
zip32::{AccountId, DiversifierIndex},
};
use zcash_client_backend::{
address::{RecipientAddress, UnifiedAddress},
data_api::{BlockMetadata, PoolType, Recipient, SentTransactionOutput},
data_api::{
scanning::{ScanPriority, ScanRange},
AccountBirthday, BlockMetadata, SentTransactionOutput, SAPLING_SHARD_HEIGHT,
},
encoding::AddressCodec,
keys::UnifiedFullViewingKey,
wallet::WalletTx,
wallet::{NoteId, Recipient, WalletTx},
PoolType, ShieldedProtocol,
};
use crate::wallet::commitment_tree::{get_max_checkpointed_height, SqliteShardStore};
@ -137,6 +136,21 @@ pub(crate) fn pool_code(pool_type: PoolType) -> i64 {
}
}
pub(crate) fn scope_code(scope: Scope) -> i64 {
match scope {
Scope::External => 0i64,
Scope::Internal => 1i64,
}
}
pub(crate) fn parse_scope(code: i64) -> Option<Scope> {
match code {
0i64 => Some(Scope::External),
1i64 => Some(Scope::Internal),
_ => None,
}
}
pub(crate) fn memo_repr(memo: Option<&MemoBytes>) -> Option<&[u8]> {
memo.map(|m| {
if m == &MemoBytes::empty() {
@ -672,17 +686,14 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
}
};
account_balances.entry(account).and_modify(|bal| {
bal.sapling_balance.spendable_value = (bal.sapling_balance.spendable_value
+ spendable_value)
.expect("Spendable value cannot overflow");
bal.sapling_balance.change_pending_confirmation =
(bal.sapling_balance.change_pending_confirmation + change_pending_confirmation)
.expect("Pending change value cannot overflow");
bal.sapling_balance.value_pending_spendability =
(bal.sapling_balance.value_pending_spendability + value_pending_spendability)
.expect("Value pending spendability cannot overflow");
});
if let Some(balances) = account_balances.get_mut(&account) {
balances.with_sapling_balance_mut::<_, SqliteClientError>(|bal| {
bal.add_spendable_value(spendable_value)?;
bal.add_pending_change_value(change_pending_confirmation)?;
bal.add_pending_spendable_value(value_pending_spendability)?;
Ok(())
})?;
}
}
#[cfg(feature = "transparent-inputs")]
@ -712,9 +723,9 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
SqliteClientError::CorruptedData(format!("Negative UTXO value {:?}", raw_value))
})?;
account_balances.entry(account).and_modify(|bal| {
bal.unshielded = (bal.unshielded + value).expect("Unshielded value cannot overflow")
});
if let Some(balances) = account_balances.get_mut(&account) {
balances.add_unshielded_value(value)?;
}
}
}
@ -1513,9 +1524,9 @@ pub(crate) fn put_block(
/// Inserts information about a mined transaction that was observed to
/// contain a note related to this wallet into the database.
pub(crate) fn put_tx_meta<N>(
pub(crate) fn put_tx_meta<N, S>(
conn: &rusqlite::Connection,
tx: &WalletTx<N>,
tx: &WalletTx<N, S>,
height: BlockHeight,
) -> Result<i64, SqliteClientError> {
// It isn't there, so insert our transaction into the database.
@ -1899,7 +1910,7 @@ pub(crate) fn insert_nullifier_map<N: AsRef<[u8]>>(
/// Returns the row of the `transactions` table corresponding to the transaction in which
/// this nullifier is revealed, if any.
pub(crate) fn query_nullifier_map<N: AsRef<[u8]>>(
pub(crate) fn query_nullifier_map<N: AsRef<[u8]>, S>(
conn: &rusqlite::Transaction<'_>,
spend_pool: ShieldedProtocol,
nf: &N,
@ -1937,7 +1948,7 @@ pub(crate) fn query_nullifier_map<N: AsRef<[u8]>>(
// change or explicit in-wallet recipient.
put_tx_meta(
conn,
&WalletTx::<N> {
&WalletTx::<N, S> {
txid,
index,
sapling_spends: vec![],
@ -2151,7 +2162,7 @@ mod tests {
.unwrap()
.unwrap();
let balance = summary.account_balances().get(&account_id).unwrap();
assert_eq!(balance.unshielded, expected);
assert_eq!(balance.unshielded(), expected);
// Check the older APIs for consistency.
let max_height = st.wallet().chain_height().unwrap().unwrap() + 1 - min_confirmations;

View File

@ -248,6 +248,7 @@ mod tests {
memo BLOB,
spent INTEGER,
commitment_tree_position INTEGER,
recipient_key_scope INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
FOREIGN KEY (account) REFERENCES accounts(account),
FOREIGN KEY (spent) REFERENCES transactions(id_tx),

View File

@ -5,6 +5,7 @@ mod addresses_table;
mod initial_setup;
mod nullifier_map;
mod received_notes_nullable_nf;
mod receiving_key_scopes;
mod sapling_memo_consistency;
mod sent_notes_to_internal;
mod shardtree_support;
@ -40,17 +41,17 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
// |
// v_transactions_net
// |
// received_notes_nullable_nf
// / | \
// shardtree_support nullifier_map sapling_memo_consistency
// | |
// add_account_birthdays v_transactions_transparent_history
// | |
// v_sapling_shard_unscanned_ranges v_tx_outputs_use_legacy_false
// | |
// wallet_summaries v_transactions_shielding_balance
// |
// v_transactions_note_uniqueness
// received_notes_nullable_nf
// / | \
// shardtree_support nullifier_map sapling_memo_consistency
// / \ |
// add_account_birthdays receiving_key_scopes v_transactions_transparent_history
// | |
// v_sapling_shard_unscanned_ranges v_tx_outputs_use_legacy_false
// | |
// wallet_summaries v_transactions_shielding_balance
// |
// v_transactions_note_uniqueness
vec![
Box::new(initial_setup::Migration {}),
Box::new(utxos_table::Migration {}),
@ -86,5 +87,8 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
Box::new(v_tx_outputs_use_legacy_false::Migration),
Box::new(v_transactions_shielding_balance::Migration),
Box::new(v_transactions_note_uniqueness::Migration),
Box::new(receiving_key_scopes::Migration {
params: params.clone(),
}),
]
}

View File

@ -0,0 +1,131 @@
//! This migration adds decryption key scope to persisted information about received notes.
use std::collections::HashSet;
use rusqlite::{self, named_params};
use schemer;
use schemer_rusqlite::RusqliteMigration;
use uuid::Uuid;
use zcash_client_backend::keys::UnifiedFullViewingKey;
use zcash_primitives::{
consensus::{self, sapling_zip212_enforcement, BlockHeight, BranchId},
sapling::note_encryption::{
try_sapling_note_decryption, PreparedIncomingViewingKey, Zip212Enforcement,
},
transaction::Transaction,
zip32::Scope,
};
use crate::wallet::{
init::{migrations::shardtree_support, WalletMigrationError},
scan_queue_extrema, scope_code,
};
pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xee89ed2b_c1c2_421e_9e98_c1e3e54a7fc2);
pub(super) struct Migration<P> {
pub(super) params: P,
}
impl<P> schemer::Migration for Migration<P> {
fn id(&self) -> Uuid {
MIGRATION_ID
}
fn dependencies(&self) -> HashSet<Uuid> {
[shardtree_support::MIGRATION_ID].into_iter().collect()
}
fn description(&self) -> &'static str {
"Add decryption key scope to persisted information about received notes."
}
}
impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
type Error = WalletMigrationError;
fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
transaction.execute_batch(
&format!(
"ALTER TABLE sapling_received_notes ADD COLUMN recipient_key_scope INTEGER NOT NULL DEFAULT {};",
scope_code(Scope::External)
)
)?;
// For all notes we have to determine whether they were actually sent to the internal key
// or the external key for the account, so we trial-decrypt the original output with the
// internal IVK and update the persisted scope value if necessary. We check all notes,
// rather than just change notes, because shielding notes may not have been considered
// change.
let mut stmt_select_notes = transaction.prepare(
"SELECT id_note, output_index, transactions.raw, transactions.block, transactions.expiry_height, accounts.ufvk
FROM sapling_received_notes
INNER JOIN accounts on accounts.account = sapling_received_notes.account
INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx"
)?;
let mut rows = stmt_select_notes.query([])?;
while let Some(row) = rows.next()? {
let note_id: i64 = row.get(0)?;
let output_index: usize = row.get(1)?;
let tx_data: Vec<u8> = row.get(2)?;
let tx = Transaction::read(&tx_data[..], BranchId::Canopy)
.expect("Transaction must be valid");
let output = tx
.sapling_bundle()
.and_then(|b| b.shielded_outputs().get(output_index))
.unwrap_or_else(|| panic!("A Sapling output must exist at index {}", output_index));
let tx_height = row.get::<_, Option<u32>>(3)?.map(BlockHeight::from);
let tx_expiry = row.get::<_, u32>(4)?;
let zip212_height = tx_height.map_or_else(
|| {
if tx_expiry == 0 {
scan_queue_extrema(transaction).map(|extrema| extrema.map(|r| *r.end()))
} else {
Ok(Some(BlockHeight::from(tx_expiry)))
}
},
|h| Ok(Some(h)),
)?;
let zip212_enforcement = zip212_height.map_or_else(
|| {
// If the transaction has not been mined and the expiry height is set to 0 (no
// expiry) an no chain tip information is available, then we assume it can only
// be mined under ZIP 212 enforcement rules, so we default to `On`
Zip212Enforcement::On
},
|h| sapling_zip212_enforcement(&self.params, h),
);
let ufvk_str: String = row.get(5)?;
let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str)
.expect("Stored UFVKs must be valid");
let dfvk = ufvk
.sapling()
.expect("UFVK must have a Sapling component to have received Sapling notes");
// We previously set the default to external scope, so we now verify whether the output
// is decryptable using the intenally-scoped IVK and, if so, mark it as such.
let pivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal));
if try_sapling_note_decryption(&pivk, output, zip212_enforcement).is_some() {
transaction.execute(
"UPDATE sapling_received_notes SET recipient_key_scope = :scope
WHERE id_note = :note_id",
named_params! {":scope": scope_code(Scope::Internal), ":note_id": note_id},
)?;
}
}
Ok(())
}
fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> {
// TODO: something better than just panic?
panic!("Cannot revert this migration.");
}
}

View File

@ -8,9 +8,7 @@ use secrecy::{ExposeSecret, SecretVec};
use uuid::Uuid;
use zcash_client_backend::{
address::RecipientAddress,
data_api::{PoolType, ShieldedProtocol},
keys::UnifiedSpendingKey,
address::RecipientAddress, keys::UnifiedSpendingKey, PoolType, ShieldedProtocol,
};
use zcash_primitives::{consensus, zip32::AccountId};

View File

@ -6,7 +6,7 @@ use rusqlite::{self, named_params};
use schemer;
use schemer_rusqlite::RusqliteMigration;
use uuid::Uuid;
use zcash_client_backend::data_api::{PoolType, ShieldedProtocol};
use zcash_client_backend::{PoolType, ShieldedProtocol};
use super::add_transaction_views;
use crate::wallet::{init::WalletMigrationError, pool_code};

View File

@ -6,24 +6,25 @@ use rusqlite::{named_params, params, types::Value, Connection, Row};
use std::rc::Rc;
use zcash_primitives::{
consensus::BlockHeight,
consensus::{self, BlockHeight},
memo::MemoBytes,
sapling::{self, Diversifier, Note, Nullifier, Rseed},
transaction::{
components::{amount::NonNegativeAmount, Amount},
TxId,
},
zip32::AccountId,
zip32::{AccountId, Scope},
};
use zcash_client_backend::{
wallet::{ReceivedSaplingNote, WalletSaplingOutput},
keys::UnifiedFullViewingKey,
wallet::{ReceivedNote, WalletNote, WalletSaplingOutput},
DecryptedOutput, TransferType,
};
use crate::{error::SqliteClientError, ReceivedNoteId};
use super::{memo_repr, wallet_birthday};
use super::{memo_repr, parse_scope, scope_code, wallet_birthday};
/// This trait provides a generalization over shielded output representations.
pub(crate) trait ReceivedSaplingOutput {
@ -34,9 +35,10 @@ pub(crate) trait ReceivedSaplingOutput {
fn is_change(&self) -> bool;
fn nullifier(&self) -> Option<&sapling::Nullifier>;
fn note_commitment_tree_position(&self) -> Option<Position>;
fn recipient_key_scope(&self) -> Scope;
}
impl ReceivedSaplingOutput for WalletSaplingOutput<sapling::Nullifier> {
impl ReceivedSaplingOutput for WalletSaplingOutput<sapling::Nullifier, Scope> {
fn index(&self) -> usize {
self.index()
}
@ -58,6 +60,10 @@ impl ReceivedSaplingOutput for WalletSaplingOutput<sapling::Nullifier> {
fn note_commitment_tree_position(&self) -> Option<Position> {
Some(WalletSaplingOutput::note_commitment_tree_position(self))
}
fn recipient_key_scope(&self) -> Scope {
*self.recipient_key_scope()
}
}
impl ReceivedSaplingOutput for DecryptedOutput<Note> {
@ -82,9 +88,19 @@ impl ReceivedSaplingOutput for DecryptedOutput<Note> {
fn note_commitment_tree_position(&self) -> Option<Position> {
None
}
fn recipient_key_scope(&self) -> Scope {
if self.transfer_type == TransferType::WalletInternal {
Scope::Internal
} else {
Scope::External
}
}
}
fn to_spendable_note(row: &Row) -> Result<ReceivedSaplingNote<ReceivedNoteId>, SqliteClientError> {
fn to_spendable_note<P: consensus::Parameters>(
params: &P,
row: &Row,
) -> Result<ReceivedNote<ReceivedNoteId>, SqliteClientError> {
let note_id = ReceivedNoteId(row.get(0)?);
let txid = row.get::<_, [u8; 32]>(1).map(TxId::from_bytes)?;
let output_index = row.get(2)?;
@ -124,13 +140,35 @@ fn to_spendable_note(row: &Row) -> Result<ReceivedSaplingNote<ReceivedNoteId>, S
SqliteClientError::CorruptedData("Note commitment tree position invalid.".to_string())
})?);
Ok(ReceivedSaplingNote::from_parts(
let ufvk_str: String = row.get(7)?;
let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str)
.map_err(SqliteClientError::CorruptedData)?;
let scope_code: i64 = row.get(8)?;
let spending_key_scope = parse_scope(scope_code).ok_or_else(|| {
SqliteClientError::CorruptedData(format!("Invalid key scope code {}", scope_code))
})?;
let recipient = match spending_key_scope {
Scope::Internal => ufvk
.sapling()
.and_then(|dfvk| dfvk.diversified_change_address(diversifier)),
Scope::External => ufvk
.sapling()
.and_then(|dfvk| dfvk.diversified_address(diversifier)),
}
.ok_or_else(|| SqliteClientError::CorruptedData("Diversifier invalid.".to_owned()))?;
Ok(ReceivedNote::from_parts(
note_id,
txid,
output_index,
diversifier,
note_value,
rseed,
WalletNote::Sapling(sapling::Note::from_parts(
recipient,
note_value.into(),
rseed,
)),
spending_key_scope,
note_commitment_tree_position,
))
}
@ -139,14 +177,17 @@ fn to_spendable_note(row: &Row) -> Result<ReceivedSaplingNote<ReceivedNoteId>, S
// (https://github.com/rust-lang/rust-clippy/issues/11308) means it fails to identify that the `result` temporary
// is required in order to resolve the borrows involved in the `query_and_then` call.
#[allow(clippy::let_and_return)]
pub(crate) fn get_spendable_sapling_note(
pub(crate) fn get_spendable_sapling_note<P: consensus::Parameters>(
conn: &Connection,
params: &P,
txid: &TxId,
index: u32,
) -> Result<Option<ReceivedSaplingNote<ReceivedNoteId>>, SqliteClientError> {
) -> Result<Option<ReceivedNote<ReceivedNoteId>>, SqliteClientError> {
let mut stmt_select_note = conn.prepare_cached(
"SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position
"SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position,
accounts.ufvk, recipient_key_scope
FROM sapling_received_notes
INNER JOIN accounts on accounts.account = sapling_received_notes.account
INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx
WHERE txid = :txid
AND output_index = :output_index
@ -159,7 +200,7 @@ pub(crate) fn get_spendable_sapling_note(
":txid": txid.as_ref(),
":output_index": index,
],
to_spendable_note,
|r| to_spendable_note(params, r),
)?
.next()
.transpose();
@ -182,8 +223,8 @@ fn unscanned_tip_exists(
"SELECT EXISTS (
SELECT 1 FROM v_sapling_shard_unscanned_ranges range
WHERE range.block_range_start <= :anchor_height
AND :anchor_height BETWEEN
range.subtree_start_height
AND :anchor_height BETWEEN
range.subtree_start_height
AND IFNULL(range.subtree_end_height, :anchor_height)
)",
named_params![":anchor_height": u32::from(anchor_height),],
@ -191,13 +232,14 @@ fn unscanned_tip_exists(
)
}
pub(crate) fn select_spendable_sapling_notes(
pub(crate) fn select_spendable_sapling_notes<P: consensus::Parameters>(
conn: &Connection,
params: &P,
account: AccountId,
target_value: Amount,
anchor_height: BlockHeight,
exclude: &[ReceivedNoteId],
) -> Result<Vec<ReceivedSaplingNote<ReceivedNoteId>>, SqliteClientError> {
) -> Result<Vec<ReceivedNote<ReceivedNoteId>>, SqliteClientError> {
let birthday_height = match wallet_birthday(conn)? {
Some(birthday) => birthday,
None => {
@ -229,13 +271,16 @@ pub(crate) fn select_spendable_sapling_notes(
// 4) Match the selected notes against the witnesses at the desired height.
let mut stmt_select_notes = conn.prepare_cached(
"WITH eligible AS (
SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position,
SELECT
id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position,
SUM(value)
OVER (PARTITION BY account, spent ORDER BY id_note) AS so_far
OVER (PARTITION BY sapling_received_notes.account, spent ORDER BY id_note) AS so_far,
accounts.ufvk as ufvk, recipient_key_scope
FROM sapling_received_notes
INNER JOIN accounts on accounts.account = sapling_received_notes.account
INNER JOIN transactions
ON transactions.id_tx = sapling_received_notes.tx
WHERE account = :account
WHERE sapling_received_notes.account = :account
AND commitment_tree_position IS NOT NULL
AND spent IS NULL
AND transactions.block <= :anchor_height
@ -251,10 +296,10 @@ pub(crate) fn select_spendable_sapling_notes(
AND unscanned.block_range_end > :wallet_birthday
)
)
SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position
SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position, ufvk, recipient_key_scope
FROM eligible WHERE so_far < :target_value
UNION
SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position
SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position, ufvk, recipient_key_scope
FROM (SELECT * from eligible WHERE so_far >= :target_value LIMIT 1)",
)?;
@ -269,7 +314,7 @@ pub(crate) fn select_spendable_sapling_notes(
":exclude": &excluded_ptr,
":wallet_birthday": u32::from(birthday_height)
],
to_spendable_note,
|r| to_spendable_note(params, r),
)?;
notes.collect::<Result<_, _>>()
@ -360,7 +405,9 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
) -> Result<(), SqliteClientError> {
let mut stmt_upsert_received_note = conn.prepare_cached(
"INSERT INTO sapling_received_notes
(tx, output_index, account, diversifier, value, rcm, memo, nf, is_change, spent, commitment_tree_position)
(tx, output_index, account, diversifier, value, rcm, memo, nf,
is_change, spent, commitment_tree_position,
recipient_key_scope)
VALUES (
:tx,
:output_index,
@ -372,7 +419,8 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
:nf,
:is_change,
:spent,
:commitment_tree_position
:commitment_tree_position,
:recipient_key_scope
)
ON CONFLICT (tx, output_index) DO UPDATE
SET account = :account,
@ -383,7 +431,8 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
memo = IFNULL(:memo, memo),
is_change = IFNULL(:is_change, is_change),
spent = IFNULL(:spent, spent),
commitment_tree_position = IFNULL(:commitment_tree_position, commitment_tree_position)",
commitment_tree_position = IFNULL(:commitment_tree_position, commitment_tree_position),
recipient_key_scope = :recipient_key_scope",
)?;
let rcm = output.note().rcm().to_repr();
@ -402,6 +451,7 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
":is_change": output.is_change(),
":spent": spent_in,
":commitment_tree_position": output.note_commitment_tree_position().map(u64::from),
":recipient_key_scope": scope_code(output.recipient_key_scope()),
];
stmt_upsert_received_note
@ -416,6 +466,7 @@ pub(crate) mod tests {
use std::{convert::Infallible, num::NonZeroU32};
use incrementalmerkletree::Hashable;
use rusqlite::params;
use secrecy::Secret;
use zcash_proofs::prover::LocalTxProver;
@ -447,22 +498,22 @@ pub(crate) mod tests {
chain::CommitmentTreeRoot,
error::Error,
wallet::input_selection::{GreedyInputSelector, GreedyInputSelectorError},
AccountBirthday, Ratio, ShieldedProtocol, WalletCommitmentTrees, WalletRead,
WalletWrite,
AccountBirthday, Ratio, WalletCommitmentTrees, WalletRead, WalletWrite,
},
decrypt_transaction,
fees::{fixed, standard, DustOutputPolicy},
keys::UnifiedSpendingKey,
wallet::OvkPolicy,
zip321::{self, Payment, TransactionRequest},
ShieldedProtocol,
};
use crate::{
error::SqliteClientError,
testing::{input_selector, AddressType, BlockCache, TestBuilder, TestState},
wallet::{
block_max_scanned, commitment_tree, sapling::select_spendable_sapling_notes,
scanning::tests::test_with_canopy_birthday,
block_max_scanned, commitment_tree, parse_scope,
sapling::select_spendable_sapling_notes, scanning::tests::test_with_canopy_birthday,
},
AccountId, NoteId, ReceivedNoteId,
};
@ -1149,6 +1200,15 @@ pub(crate) mod tests {
NonNegativeAmount::ZERO
);
let change_note_scope = st.wallet().conn.query_row(
"SELECT recipient_key_scope
FROM sapling_received_notes
WHERE value = ?",
params![u64::from(value)],
|row| Ok(parse_scope(row.get(0)?)),
);
assert_matches!(change_note_scope, Ok(Some(Scope::Internal)));
// TODO: This test was originally written to use the pre-zip-313 fee rule
// and has not yet been updated.
#[allow(deprecated)]
@ -1518,6 +1578,7 @@ pub(crate) mod tests {
// Verify that the received note is not considered spendable
let spendable = select_spendable_sapling_notes(
&st.wallet().conn,
&st.wallet().params,
AccountId::ZERO,
Amount::const_from_i64(300000),
received_tx_height + 10,
@ -1533,6 +1594,7 @@ pub(crate) mod tests {
// Verify that the received note is now considered spendable
let spendable = select_spendable_sapling_notes(
&st.wallet().conn,
&st.wallet().params,
AccountId::ZERO,
Amount::const_from_i64(300000),
received_tx_height + 10,
@ -1586,6 +1648,7 @@ pub(crate) mod tests {
// Verify that our note is considered spendable
let spendable = select_spendable_sapling_notes(
&st.wallet().conn,
&st.wallet().params,
account,
Amount::const_from_i64(300000),
birthday.height() + 5,

View File

@ -95,6 +95,7 @@ and this library adheres to Rust's notion of
- `impl From<NonNegativeAmount> for zcash_primitives::sapling::value::NoteValue`
- `impl Sum<NonNegativeAmount> for Option<NonNegativeAmount>`
- `impl<'a> Sum<&'a NonNegativeAmount> for Option<NonNegativeAmount>`
- `impl TryFrom<sapling::value::NoteValue> for NonNegativeAmount`
- `impl {Clone, PartialEq, Eq} for zcash_primitives::memo::Error`
- `impl {PartialEq, Eq} for zcash_primitives::sapling::note::Rseed`
- `impl From<TxId> for [u8; 32]`

View File

@ -1,4 +1,5 @@
use std::convert::TryFrom;
use std::error;
use std::iter::Sum;
use std::ops::{Add, AddAssign, Mul, Neg, Sub, SubAssign};
@ -322,6 +323,14 @@ impl From<NonNegativeAmount> for sapling::value::NoteValue {
}
}
impl TryFrom<sapling::value::NoteValue> for NonNegativeAmount {
type Error = ();
fn try_from(value: sapling::value::NoteValue) -> Result<Self, Self::Error> {
Self::from_u64(value.inner())
}
}
impl TryFrom<Amount> for NonNegativeAmount {
type Error = ();
@ -394,6 +403,8 @@ pub enum BalanceError {
Underflow,
}
impl error::Error for BalanceError {}
impl std::fmt::Display for BalanceError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self {