zcash_client_backend: Factor out input source traits from `WalletRead`

Prior to this change, it's necessary to implement the entirety of the
`WalletRead` trait in order to be able to use the input selection
functionality provided by `zcash_client_backend::data_api::input_selection`.
This change factors out the minimal operations required for transaction
proposal construction to better reflect the principle of least authority
and make the input selection code reusable in more contexts.

In order to minimize the operations of the newly-created `InputSource`
and `ShieldingSource` traits, this change also removes the
`min_confirmations` field from transaction proposals, in favor of
storing explicit target and anchor heights. This has the effect of
limiting the lifetime of transaction proposals to `PRUNING_DEPTH -
min_confirmations` blocks.
This commit is contained in:
Kris Nuttycombe 2023-10-02 18:02:06 -06:00
parent 7d1c0a29ca
commit aa063ae3fd
15 changed files with 461 additions and 435 deletions

View File

@ -162,7 +162,7 @@ jobs:
- run: cargo fetch
# Requires #![deny(rustdoc::broken_intra_doc_links)] in crates.
- name: Check intra-doc links
run: cargo doc --all --document-private-items
run: cargo doc --all --all-features --document-private-items
fmt:
name: Rustfmt

5
Cargo.lock generated
View File

@ -2183,9 +2183,9 @@ dependencies = [
[[package]]
name = "shardtree"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19f96dde3a8693874f7e7c53d95616569b4009379a903789efbd448f4ea9cc7"
checksum = "dbf20c7a2747d9083092e3a3eeb9a7ed75577ae364896bebbc5e0bdcd4e97735"
dependencies = [
"assert_matches",
"bitflags 2.4.0",
@ -2963,6 +2963,7 @@ dependencies = [
"jubjub",
"memuse",
"nom",
"nonempty",
"orchard",
"percent-encoding",
"proptest",

View File

@ -39,7 +39,7 @@ zcash_proofs = { version = "0.13", path = "zcash_proofs", default-features = fal
ff = "0.13"
group = "0.13"
incrementalmerkletree = "0.5"
shardtree = "0.1"
shardtree = "0.2"
# Payment protocols
# - Sapling

View File

@ -8,10 +8,16 @@ and this library adheres to Rust's notion of
## [Unreleased]
### Added
- `zcash_client_backend::data_api::wallet::propose_standard_transfer_to_address`
- `zcash_client_backend::data_api`:
- `TransparentInputSource`
- `SaplingInputSource`
- `wallet::propose_standard_transfer_to_address`
- `wallet::input_selection::SaplingInputs`
- `wallet::input_selection::ShieldingSelector` has been
factored out from the `InputSelector` trait to separate out transparent
functionality and move it behind the `transparent-inputs` feature flag.
- `zcash_client_backend::fees::standard`
- `zcash_client_backend::wallet`:
- `input_selection::Proposal::min_confirmations`
- `ReceivedSaplingNote::from_parts`
- `ReceivedSaplingNote::{txid, output_index, diversifier, rseed, note_commitment_tree_position}`
@ -39,7 +45,7 @@ and this library adheres to Rust's notion of
- `wallet::create_proposed_transaction` now takes its `proposal` argument
by reference instead of as an owned value.
- `wallet::create_proposed_transaction` no longer takes a `min_confirmations`
argument. Instead, `min_confirmations` is stored in the `Proposal`
argument. Instead, it uses the anchor height from its `proposal` argument.
- `wallet::create_spend_to_address` now takes an additional
`change_memo` argument.
- The error type of `wallet::create_spend_to_address` has been changed to use
@ -51,6 +57,39 @@ and this library adheres to Rust's notion of
- `wallet::create_spend_to_address`
- `wallet::shield_transparent_funds`
- `wallet::spend`
- In order to support better reusability for input selection code, two new
traits have been factored out from `WalletRead`:
- `zcash_client_backend::data_api::TransparentInputSource`
- `zcash_client_backend::data_api::SaplingInputSource`
- `wallet::input_selection::InputSelector::propose_shielding`,
has been moved out to the newly-created `ShieldingSelector` trait.
- `ShieldingSelector::propose_shielding` has been altered such that it takes
an explicit `target_height` in order to minimize the capabilities that the
`data_api::TransparentInputSource` trait must expose. Also, it now takes its
`min_confirmations` argument as `u32` instead of `NonZeroU32`.
- The `wallet::input_selection::InputSelector::DataSource`
associated type has been renamed to `InputSource`.
- The signature of `wallet:input_selection::InputSelector::propose_transaction`
has been altered such that it longer takes `min_confirmations` as an
argument, instead taking explicit `target_height` and `anchor_height`
arguments. This helps to minimize the set of capabilities that the
`data_api::SaplingInputSource` must expose.
- `WalletRead::get_checkpoint_depth` has been removed without replacement. This
is no longer needed given the change to use the stored anchor height for transaction
proposal execution.
- `wallet::{propose_shielding, shield_transparent_funds}` now takes their
`min_confirmations` arguments as `u32` rather than a `NonZeroU32` to permit
implmentations to enable zero-conf shielding.
- `wallet::create_proposed_transaction` now forces implementations to ignore
the database identifiers for its contained notes by universally quantifying
the `NoteRef` type parameter.
- `wallet::input_selection::Proposal::sapling_inputs` now returns type
`Option<&SaplingInputs>`.
- `wallet::input_selection::Proposal::min_anchor_height` has been removed in
favor of storing this value in `SaplingInputs`.
- `wallet::input_selection::GreedyInputSelector` now has relaxed requirements
for its `InputSource` associated type.
- `zcash_client_backend::fees`:
- `ChangeValue::Sapling` is now a structured variant. In addition to the
existing change value, it now also carries an optional memo to be associated
@ -92,8 +131,12 @@ and this library adheres to Rust's notion of
### Removed
- `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
removed; it was unused in the ECC mobile wallet SDKs and has been superseded by
`get_account_for_ufvk`.
- `zcash_client_backend::data_api::WalletRead::get_spendable_sapling_notes` has been
removed without replacement as it was unused, and its functionality will be
fully reproduced by `SaplingInputSource::select_spendable_sapling_notes` in a future
change.
## [0.10.0] - 2023-09-25

View File

@ -30,6 +30,7 @@ zcash_primitives.workspace = true
# (Breaking upgrades to these require a breaking upgrade to this crate.)
# - Data Access API
time = "0.3.22"
nonempty.workspace = true
# - Encodings
base64.workspace = true
@ -87,6 +88,7 @@ gumdrop = "0.8"
jubjub.workspace = true
proptest.workspace = true
rand_core.workspace = true
shardtree = { workspace = true, features = ["test-dependencies"] }
zcash_proofs.workspace = true
zcash_address = { workspace = true, features = ["test-dependencies"] }

View File

@ -17,10 +17,7 @@ use zcash_primitives::{
memo::{Memo, MemoBytes},
sapling::{self, Node, NOTE_COMMITMENT_TREE_DEPTH},
transaction::{
components::{
amount::{Amount, NonNegativeAmount},
OutPoint,
},
components::amount::{Amount, NonNegativeAmount},
Transaction, TxId,
},
zip32::AccountId,
@ -34,6 +31,9 @@ use crate::{
wallet::{ReceivedSaplingNote, WalletTransparentOutput, WalletTx},
};
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::transaction::components::OutPoint;
use self::chain::CommitmentTreeRoot;
use self::scanning::ScanRange;
@ -210,12 +210,9 @@ impl WalletSummary {
}
}
/// Read-only operations required for light wallet functions.
///
/// This trait defines the read-only portion of the storage interface atop which
/// higher-level wallet operations are implemented. It serves to allow wallet functions to
/// be abstracted away from any particular data storage substrate.
pub trait WalletRead {
/// A trait representing the capability to query a data store for unspent Sapling notes belonging
/// to a wallet.
pub trait SaplingInputSource {
/// The type of errors produced by a wallet backend.
type Error;
@ -225,6 +222,43 @@ pub trait WalletRead {
/// or a UUID.
type NoteRef: Copy + Debug + Eq + Ord;
/// Returns a list of spendable Sapling notes sufficient to cover the specified target value,
/// if possible.
fn select_spendable_sapling_notes(
&self,
account: AccountId,
target_value: Amount,
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error>;
}
/// A trait representing the capability to query a data store for unspent transparent UTXOs
/// belonging to a wallet.
#[cfg(feature = "transparent-inputs")]
pub trait TransparentInputSource {
/// The type of errors produced by a wallet backend.
type Error;
/// Returns a list of unspent transparent UTXOs that appear in the chain at heights up to and
/// including `max_height`.
fn get_unspent_transparent_outputs(
&self,
address: &TransparentAddress,
max_height: BlockHeight,
exclude: &[OutPoint],
) -> Result<Vec<WalletTransparentOutput>, Self::Error>;
}
/// Read-only operations required for light wallet functions.
///
/// This trait defines the read-only portion of the storage interface atop which
/// higher-level wallet operations are implemented. It serves to allow wallet functions to
/// be abstracted away from any particular data storage substrate.
pub trait WalletRead {
/// The type of errors that may be generated when querying a wallet data store.
type Error;
/// Returns the height of the chain as known to the wallet as of the most recent call to
/// [`WalletWrite::update_chain_tip`].
///
@ -351,24 +385,6 @@ pub trait WalletRead {
query: NullifierQuery,
) -> Result<Vec<(AccountId, sapling::Nullifier)>, Self::Error>;
/// Return all unspent Sapling notes, excluding the specified note IDs.
fn get_spendable_sapling_notes(
&self,
account: AccountId,
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error>;
/// Returns a list of spendable Sapling notes sufficient to cover the specified target value,
/// if possible.
fn select_spendable_sapling_notes(
&self,
account: AccountId,
target_value: Amount,
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error>;
/// Returns the set of all transparent receivers associated with the given account.
///
/// The set contains all transparent receivers that are known to have been derived
@ -379,15 +395,6 @@ pub trait WalletRead {
account: AccountId,
) -> Result<HashMap<TransparentAddress, AddressMetadata>, Self::Error>;
/// Returns a list of unspent transparent UTXOs that appear in the chain at heights up to and
/// including `max_height`.
fn get_unspent_transparent_outputs(
&self,
address: &TransparentAddress,
max_height: BlockHeight,
exclude: &[OutPoint],
) -> Result<Vec<WalletTransparentOutput>, Self::Error>;
/// Returns a mapping from transparent receiver to not-yet-shielded UTXO balance,
/// for each address associated with a nonzero balance.
fn get_transparent_balances(
@ -897,16 +904,7 @@ pub trait WalletCommitmentTrees {
Error = Self::Error,
>;
/// Returns the depth of the checkpoint in the tree that can be used to create a witness at the
/// anchor having the given number of confirmations.
///
/// This assumes that at any time a note is added to the tree, a checkpoint is created for the
/// end of the block in which that note was discovered.
fn get_checkpoint_depth(
&self,
min_confirmations: NonZeroU32,
) -> Result<usize, ShardTreeError<Self::Error>>;
fn with_sapling_tree_mut<F, A, E>(&mut self, callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
@ -939,10 +937,7 @@ pub mod testing {
legacy::TransparentAddress,
memo::Memo,
sapling,
transaction::{
components::{Amount, OutPoint},
Transaction, TxId,
},
transaction::{components::Amount, Transaction, TxId},
zip32::AccountId,
};
@ -954,8 +949,9 @@ pub mod testing {
use super::{
chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata,
DecryptedTransaction, NoteId, NullifierQuery, ScannedBlock, SentTransaction,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
DecryptedTransaction, NoteId, NullifierQuery, SaplingInputSource, ScannedBlock,
SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite,
SAPLING_SHARD_HEIGHT,
};
pub struct MockWalletDb {
@ -967,6 +963,9 @@ pub mod testing {
>,
}
#[cfg(feature = "transparent-inputs")]
use {super::TransparentInputSource, zcash_primitives::transaction::components::OutPoint};
impl MockWalletDb {
pub fn new(network: Network) -> Self {
Self {
@ -976,10 +975,38 @@ pub mod testing {
}
}
impl WalletRead for MockWalletDb {
impl SaplingInputSource for MockWalletDb {
type Error = ();
type NoteRef = u32;
fn select_spendable_sapling_notes(
&self,
_account: AccountId,
_target_value: Amount,
_anchor_height: BlockHeight,
_exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
Ok(Vec::new())
}
}
#[cfg(feature = "transparent-inputs")]
impl TransparentInputSource for MockWalletDb {
type Error = ();
fn get_unspent_transparent_outputs(
&self,
_address: &TransparentAddress,
_anchor_height: BlockHeight,
_exclude: &[OutPoint],
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
Ok(Vec::new())
}
}
impl WalletRead for MockWalletDb {
type Error = ();
fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
@ -1079,25 +1106,6 @@ pub mod testing {
Ok(Vec::new())
}
fn get_spendable_sapling_notes(
&self,
_account: AccountId,
_anchor_height: BlockHeight,
_exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
Ok(Vec::new())
}
fn select_spendable_sapling_notes(
&self,
_account: AccountId,
_target_value: Amount,
_anchor_height: BlockHeight,
_exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
Ok(Vec::new())
}
fn get_transparent_receivers(
&self,
_account: AccountId,
@ -1105,15 +1113,6 @@ pub mod testing {
Ok(HashMap::new())
}
fn get_unspent_transparent_outputs(
&self,
_address: &TransparentAddress,
_anchor_height: BlockHeight,
_exclude: &[OutPoint],
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
Ok(Vec::new())
}
fn get_transparent_balances(
&self,
_account: AccountId,
@ -1214,12 +1213,5 @@ pub mod testing {
Ok(())
}
fn get_checkpoint_depth(
&self,
min_confirmations: NonZeroU32,
) -> Result<usize, ShardTreeError<Self::Error>> {
Ok(usize::try_from(u32::from(min_confirmations)).unwrap())
}
}
}

View File

@ -35,13 +35,18 @@ use crate::{
};
pub mod input_selection;
use input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector};
use input_selection::{
GreedyInputSelector, GreedyInputSelectorError, InputSelector, InputSelectorError,
};
use super::{NoteId, ShieldedProtocol};
use super::{NoteId, SaplingInputSource, ShieldedProtocol};
#[cfg(feature = "transparent-inputs")]
use {
super::TransparentInputSource,
crate::wallet::WalletTransparentOutput,
input_selection::ShieldingSelector,
std::convert::Infallible,
zcash_primitives::{legacy::TransparentAddress, sapling::keys::OutgoingViewingKey},
};
@ -216,7 +221,9 @@ pub fn create_spend_to_address<DbT, ParamsT>(
>
where
ParamsT: consensus::Parameters + Clone,
DbT: WalletWrite + WalletCommitmentTrees,
DbT: WalletWrite
+ WalletCommitmentTrees
+ SaplingInputSource<Error = <DbT as WalletRead>::Error>,
DbT::NoteRef: Copy + Eq + Ord,
{
let account = wallet_db
@ -324,10 +331,12 @@ pub fn spend<DbT, ParamsT, InputsT>(
>,
>
where
DbT: WalletWrite + WalletCommitmentTrees,
DbT: WalletWrite
+ WalletCommitmentTrees
+ SaplingInputSource<Error = <DbT as WalletRead>::Error>,
DbT::NoteRef: Copy + Eq + Ord,
ParamsT: consensus::Parameters + Clone,
InputsT: InputSelector<DataSource = DbT>,
InputsT: InputSelector<InputSource = DbT>,
{
let account = wallet_db
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
@ -368,21 +377,32 @@ pub fn propose_transfer<DbT, ParamsT, InputsT, CommitmentTreeErrT>(
min_confirmations: NonZeroU32,
) -> Result<
Proposal<InputsT::FeeRule, DbT::NoteRef>,
Error<DbT::Error, CommitmentTreeErrT, InputsT::Error, <InputsT::FeeRule as FeeRule>::Error>,
Error<
<DbT as WalletRead>::Error,
CommitmentTreeErrT,
InputsT::Error,
<InputsT::FeeRule as FeeRule>::Error,
>,
>
where
DbT: WalletWrite,
DbT: WalletRead + SaplingInputSource<Error = <DbT as WalletRead>::Error>,
DbT::NoteRef: Copy + Eq + Ord,
ParamsT: consensus::Parameters + Clone,
InputsT: InputSelector<DataSource = DbT>,
InputsT: InputSelector<InputSource = DbT>,
{
let (target_height, anchor_height) = wallet_db
.get_target_and_anchor_heights(min_confirmations)
.map_err(|e| Error::from(InputSelectorError::DataSource(e)))?
.ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?;
input_selector
.propose_transaction(
params,
wallet_db,
target_height,
anchor_height,
spend_from_account,
request,
min_confirmations,
)
.map_err(Error::from)
}
@ -421,7 +441,7 @@ pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
) -> Result<
Proposal<StandardFeeRule, DbT::NoteRef>,
Error<
DbT::Error,
<DbT as WalletRead>::Error,
CommitmentTreeErrT,
GreedyInputSelectorError<Zip317FeeError, DbT::NoteRef>,
Zip317FeeError,
@ -429,7 +449,7 @@ pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
>
where
ParamsT: consensus::Parameters + Clone,
DbT: WalletWrite,
DbT: WalletRead + SaplingInputSource<Error = <DbT as WalletRead>::Error>,
DbT::NoteRef: Copy + Eq + Ord,
{
let request = zip321::TransactionRequest::new(vec![Payment {
@ -467,23 +487,33 @@ pub fn propose_shielding<DbT, ParamsT, InputsT, CommitmentTreeErrT>(
input_selector: &InputsT,
shielding_threshold: NonNegativeAmount,
from_addrs: &[TransparentAddress],
min_confirmations: NonZeroU32,
min_confirmations: u32,
) -> Result<
Proposal<InputsT::FeeRule, DbT::NoteRef>,
Error<DbT::Error, CommitmentTreeErrT, InputsT::Error, <InputsT::FeeRule as FeeRule>::Error>,
Proposal<InputsT::FeeRule, Infallible>,
Error<
<DbT as WalletRead>::Error,
CommitmentTreeErrT,
InputsT::Error,
<InputsT::FeeRule as FeeRule>::Error,
>,
>
where
ParamsT: consensus::Parameters,
DbT: WalletWrite,
DbT::NoteRef: Copy + Eq + Ord,
InputsT: InputSelector<DataSource = DbT>,
DbT: WalletRead + TransparentInputSource<Error = <DbT as WalletRead>::Error>,
InputsT: ShieldingSelector<InputSource = DbT>,
{
let chain_tip_height = wallet_db
.chain_height()
.map_err(|e| Error::from(InputSelectorError::DataSource(e)))?
.ok_or_else(|| Error::from(InputSelectorError::SyncRequired))?;
input_selector
.propose_shielding(
params,
wallet_db,
shielding_threshold,
from_addrs,
chain_tip_height + 1,
min_confirmations,
)
.map_err(Error::from)
@ -499,14 +529,14 @@ where
/// to fall back to the transparent receiver until full Orchard support is implemented.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT>(
pub fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
usk: &UnifiedSpendingKey,
ovk_policy: OvkPolicy,
proposal: &Proposal<FeeRuleT, DbT::NoteRef>,
proposal: &Proposal<FeeRuleT, N>,
) -> Result<
TxId,
Error<
@ -518,7 +548,6 @@ pub fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT>(
>
where
DbT: WalletWrite + WalletCommitmentTrees,
DbT::NoteRef: Copy + Eq + Ord,
ParamsT: consensus::Parameters + Clone,
FeeRuleT: FeeRule,
{
@ -557,28 +586,29 @@ where
// are no possible transparent inputs, so we ignore those
let mut builder = Builder::new(params.clone(), proposal.min_target_height(), None);
let checkpoint_depth = wallet_db.get_checkpoint_depth(proposal.min_confirmations())?;
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {
for selected in proposal.sapling_inputs() {
let (note, key, merkle_path) = select_key_for_note(
sapling_tree,
selected,
usk.sapling(),
&dfvk,
checkpoint_depth,
)?
.ok_or_else(|| {
Error::NoteMismatch(NoteId {
txid: *selected.txid(),
protocol: ShieldedProtocol::Sapling,
output_index: selected.output_index(),
})
})?;
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, key, merkle_path) = select_key_for_note(
sapling_tree,
selected,
usk.sapling(),
&dfvk,
sapling_inputs.anchor_height(),
)?
.ok_or_else(|| {
Error::NoteMismatch(NoteId {
txid: *selected.txid(),
protocol: ShieldedProtocol::Sapling,
output_index: selected.output_index(),
})
})?;
builder.add_sapling_spend(key, selected.diversifier(), note, merkle_path)?;
}
Ok(())
})?;
builder.add_sapling_spend(key, selected.diversifier(), note, merkle_path)?;
}
Ok(())
})?;
}
#[cfg(feature = "transparent-inputs")]
let utxos = {
@ -807,7 +837,7 @@ pub fn shield_transparent_funds<DbT, ParamsT, InputsT>(
shielding_threshold: NonNegativeAmount,
usk: &UnifiedSpendingKey,
from_addrs: &[TransparentAddress],
min_confirmations: NonZeroU32,
min_confirmations: u32,
) -> Result<
TxId,
Error<
@ -819,9 +849,10 @@ pub fn shield_transparent_funds<DbT, ParamsT, InputsT>(
>
where
ParamsT: consensus::Parameters,
DbT: WalletWrite + WalletCommitmentTrees,
DbT::NoteRef: Copy + Eq + Ord,
InputsT: InputSelector<DataSource = DbT>,
DbT: WalletWrite
+ WalletCommitmentTrees
+ TransparentInputSource<Error = <DbT as WalletRead>::Error>,
InputsT: ShieldingSelector<InputSource = DbT>,
{
let proposal = propose_shielding(
wallet_db,
@ -853,7 +884,7 @@ fn select_key_for_note<N, S: ShardStore<H = Node, CheckpointId = BlockHeight>>(
selected: &ReceivedSaplingNote<N>,
extsk: &ExtendedSpendingKey,
dfvk: &DiversifiableFullViewingKey,
checkpoint_depth: usize,
anchor_height: BlockHeight,
) -> Result<
Option<(sapling::Note, ExtendedSpendingKey, sapling::MerklePath)>,
ShardTreeError<S::Error>,
@ -868,9 +899,11 @@ fn select_key_for_note<N, S: ShardStore<H = Node, CheckpointId = BlockHeight>>(
.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(checkpoint_depth)?;
let merkle_path = commitment_tree
.witness_caching(selected.note_commitment_tree_position(), checkpoint_depth)?;
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())))

View File

@ -1,10 +1,9 @@
//! Types related to the process of selecting inputs to be spent given a transaction request.
use core::marker::PhantomData;
use std::fmt;
use std::num::NonZeroU32;
use std::{collections::BTreeSet, fmt::Debug};
use std::fmt::{self, Debug};
use nonempty::NonEmpty;
use zcash_primitives::{
consensus::{self, BlockHeight},
legacy::TransparentAddress,
@ -12,7 +11,7 @@ use zcash_primitives::{
components::{
amount::{BalanceError, NonNegativeAmount},
sapling::fees as sapling,
OutPoint, TxOut,
TxOut,
},
fees::FeeRule,
},
@ -21,12 +20,18 @@ use zcash_primitives::{
use crate::{
address::{RecipientAddress, UnifiedAddress},
data_api::WalletRead,
data_api::SaplingInputSource,
fees::{ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance},
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
zip321::TransactionRequest,
};
#[cfg(feature = "transparent-inputs")]
use {
crate::data_api::TransparentInputSource, std::collections::BTreeSet, std::convert::Infallible,
zcash_primitives::transaction::components::OutPoint,
};
/// The type of errors that may be produced in input selection.
pub enum InputSelectorError<DbErrT, SelectorErrT> {
/// An error occurred accessing the underlying data store.
@ -73,17 +78,14 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
}
}
/// A data structure that describes the inputs to be consumed and outputs to
/// be produced in a proposed transaction.
/// The inputs to be consumed and outputs to be produced in a proposed transaction.
pub struct Proposal<FeeRuleT, NoteRef> {
transaction_request: TransactionRequest,
transparent_inputs: Vec<WalletTransparentOutput>,
sapling_inputs: Vec<ReceivedSaplingNote<NoteRef>>,
sapling_inputs: Option<SaplingInputs<NoteRef>>,
balance: TransactionBalance,
fee_rule: FeeRuleT,
min_confirmations: NonZeroU32,
min_target_height: BlockHeight,
min_anchor_height: BlockHeight,
is_shielding: bool,
}
@ -97,8 +99,8 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
&self.transparent_inputs
}
/// Returns the Sapling inputs that have been selected to fund the transaction.
pub fn sapling_inputs(&self) -> &[ReceivedSaplingNote<NoteRef>] {
&self.sapling_inputs
pub fn sapling_inputs(&self) -> Option<&SaplingInputs<NoteRef>> {
self.sapling_inputs.as_ref()
}
/// Returns the change outputs to be added to the transaction and the fee to be paid.
pub fn balance(&self) -> &TransactionBalance {
@ -108,10 +110,6 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
pub fn fee_rule(&self) -> &FeeRuleT {
&self.fee_rule
}
/// Returns the value of `min_confirmations` used in input selection.
pub fn min_confirmations(&self) -> NonZeroU32 {
self.min_confirmations
}
/// Returns the target height for which the proposal was prepared.
///
/// The chain must contain at least this many blocks in order for the proposal to
@ -119,14 +117,6 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
pub fn min_target_height(&self) -> BlockHeight {
self.min_target_height
}
/// Returns the anchor height used in preparing the proposal.
///
/// If, at the time that the proposal is executed, the anchor height required to satisfy
/// the minimum confirmation depth is less than this height, the proposal execution
/// API should return an error.
pub fn min_anchor_height(&self) -> BlockHeight {
self.min_anchor_height
}
/// Returns a flag indicating whether or not the proposed transaction
/// is exclusively wallet-internal (if it does not involve any external
/// recipients).
@ -140,16 +130,41 @@ impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
f.debug_struct("Proposal")
.field("transaction_request", &self.transaction_request)
.field("transparent_inputs", &self.transparent_inputs)
.field("sapling_inputs", &self.sapling_inputs.len())
.field(
"sapling_inputs",
&self.sapling_inputs().map(|i| i.notes.len()),
)
.field(
"sapling_anchor_height",
&self.sapling_inputs().map(|i| i.anchor_height),
)
.field("balance", &self.balance)
//.field("fee_rule", &self.fee_rule)
.field("min_target_height", &self.min_target_height)
.field("min_anchor_height", &self.min_anchor_height)
.field("is_shielding", &self.is_shielding)
.finish_non_exhaustive()
}
}
/// The Sapling inputs to a proposed transaction.
pub struct SaplingInputs<NoteRef> {
anchor_height: BlockHeight,
notes: NonEmpty<ReceivedSaplingNote<NoteRef>>,
}
impl<NoteRef> SaplingInputs<NoteRef> {
/// Returns the anchor height for Sapling inputs that should be used when constructing the
/// proposed transaction.
pub fn anchor_height(&self) -> BlockHeight {
self.anchor_height
}
/// Returns the list of Sapling notes to be used as inputs to the proposed transaction.
pub fn notes(&self) -> &NonEmpty<ReceivedSaplingNote<NoteRef>> {
&self.notes
}
}
/// A strategy for selecting transaction inputs and proposing transaction outputs.
///
/// Proposals should include only economically useful inputs, as determined by `Self::FeeRule`;
@ -158,12 +173,12 @@ impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
pub trait InputSelector {
/// The type of errors that may be generated in input selection
type Error;
/// The type of data source that the input selector expects to access to obtain input notes and
/// UTXOs. This associated type permits input selectors that may use specialized knowledge of
/// the internals of a particular backing data store, if the generic API of `WalletRead` does
/// not provide sufficiently fine-grained operations for a particular backing store to
/// optimally perform input selection.
type DataSource: WalletRead;
/// The type of data source that the input selector expects to access to obtain input Sapling
/// notes. This associated type permits input selectors that may use specialized knowledge of
/// the internals of a particular backing data store, if the generic API of
/// `SaplingInputSource` does not provide sufficiently fine-grained operations for a particular
/// backing store to optimally perform input selection.
type InputSource: SaplingInputSource;
/// The type of the fee rule that this input selector uses when computing fees.
type FeeRule: FeeRule;
@ -181,21 +196,38 @@ pub trait InputSelector {
///
/// If insufficient funds are available to satisfy the required outputs for the shielding
/// request, this operation must fail and return [`InputSelectorError::InsufficientFunds`].
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn propose_transaction<ParamsT>(
&self,
params: &ParamsT,
wallet_db: &Self::DataSource,
wallet_db: &Self::InputSource,
target_height: BlockHeight,
anchor_height: BlockHeight,
account: AccountId,
transaction_request: TransactionRequest,
min_confirmations: NonZeroU32,
) -> Result<
Proposal<Self::FeeRule, <<Self as InputSelector>::DataSource as WalletRead>::NoteRef>,
InputSelectorError<<<Self as InputSelector>::DataSource as WalletRead>::Error, Self::Error>,
Proposal<Self::FeeRule, <Self::InputSource as SaplingInputSource>::NoteRef>,
InputSelectorError<<Self::InputSource as SaplingInputSource>::Error, Self::Error>,
>
where
ParamsT: consensus::Parameters;
}
/// A strategy for selecting transaction inputs and proposing transaction outputs
/// for shielding-only transactions (transactions which spend transparent UTXOs and
/// send all transaction outputs to the wallet's shielded internal address(es)).
#[cfg(feature = "transparent-inputs")]
pub trait ShieldingSelector {
/// The type of errors that may be generated in input selection
type Error;
/// The type of data source that the input selector expects to access to obtain input
/// transparent UTXOs. This associated type permits input selectors that may use specialized
/// knowledge of the internals of a particular backing data store, if the generic API of
/// `TransparentInputSource` does not provide sufficiently fine-grained operations for a
/// particular backing store to optimally perform input selection.
type InputSource: TransparentInputSource;
/// The type of the fee rule that this input selector uses when computing fees.
type FeeRule: FeeRule;
/// Performs input selection and returns a proposal for the construction of a shielding
/// transaction.
@ -205,18 +237,18 @@ pub trait InputSelector {
/// specified source addresses. If insufficient funds are available to satisfy the required
/// outputs for the shielding request, this operation must fail and return
/// [`InputSelectorError::InsufficientFunds`].
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn propose_shielding<ParamsT>(
&self,
params: &ParamsT,
wallet_db: &Self::DataSource,
wallet_db: &Self::InputSource,
shielding_threshold: NonNegativeAmount,
source_addrs: &[TransparentAddress],
min_confirmations: NonZeroU32,
target_height: BlockHeight,
min_confirmations: u32,
) -> Result<
Proposal<Self::FeeRule, <<Self as InputSelector>::DataSource as WalletRead>::NoteRef>,
InputSelectorError<<<Self as InputSelector>::DataSource as WalletRead>::Error, Self::Error>,
Proposal<Self::FeeRule, Infallible>,
InputSelectorError<<Self::InputSource as TransparentInputSource>::Error, Self::Error>,
>
where
ParamsT: consensus::Parameters;
@ -296,8 +328,8 @@ impl sapling::OutputView for SaplingPayment {
/// An [`InputSelector`] implementation that uses a greedy strategy to select between available
/// notes.
///
/// This implementation performs input selection using methods available via the [`WalletRead`]
/// interface.
/// This implementation performs input selection using methods available via the
/// [`SaplingInputSource`] and [`TransparentInputSource`] interfaces.
pub struct GreedyInputSelector<DbT, ChangeT> {
change_strategy: ChangeT,
dust_output_policy: DustOutputPolicy,
@ -318,32 +350,31 @@ impl<DbT, ChangeT: ChangeStrategy> GreedyInputSelector<DbT, ChangeT> {
impl<DbT, ChangeT> InputSelector for GreedyInputSelector<DbT, ChangeT>
where
DbT: WalletRead,
DbT: SaplingInputSource,
ChangeT: ChangeStrategy,
ChangeT::FeeRule: Clone,
{
type Error = GreedyInputSelectorError<ChangeT::Error, DbT::NoteRef>;
type DataSource = DbT;
type InputSource = DbT;
type FeeRule = ChangeT::FeeRule;
#[allow(clippy::type_complexity)]
fn propose_transaction<ParamsT>(
&self,
params: &ParamsT,
wallet_db: &Self::DataSource,
wallet_db: &Self::InputSource,
target_height: BlockHeight,
anchor_height: BlockHeight,
account: AccountId,
transaction_request: TransactionRequest,
min_confirmations: NonZeroU32,
) -> Result<Proposal<Self::FeeRule, DbT::NoteRef>, InputSelectorError<DbT::Error, Self::Error>>
) -> Result<
Proposal<Self::FeeRule, DbT::NoteRef>,
InputSelectorError<<DbT as SaplingInputSource>::Error, Self::Error>,
>
where
ParamsT: consensus::Parameters,
Self::InputSource: SaplingInputSource,
{
// Target the next block, assuming we are up-to-date.
let (target_height, anchor_height) = wallet_db
.get_target_and_anchor_heights(min_confirmations)
.map_err(InputSelectorError::DataSource)
.and_then(|x| x.ok_or(InputSelectorError::SyncRequired))?;
let mut transparent_outputs = vec![];
let mut sapling_outputs = vec![];
for payment in transaction_request.payments() {
@ -401,12 +432,15 @@ where
return Ok(Proposal {
transaction_request,
transparent_inputs: vec![],
sapling_inputs,
sapling_inputs: NonEmpty::from_vec(sapling_inputs).map(|notes| {
SaplingInputs {
anchor_height,
notes,
}
}),
balance,
fee_rule: (*self.change_strategy.fee_rule()).clone(),
min_confirmations,
min_target_height: target_height,
min_anchor_height: anchor_height,
is_shielding: false,
});
}
@ -446,27 +480,44 @@ where
}
}
}
}
#[cfg(feature = "transparent-inputs")]
impl<DbT, ChangeT> ShieldingSelector for GreedyInputSelector<DbT, ChangeT>
where
DbT: TransparentInputSource,
ChangeT: ChangeStrategy,
ChangeT::FeeRule: Clone,
{
type Error = GreedyInputSelectorError<ChangeT::Error, Infallible>;
type InputSource = DbT;
type FeeRule = ChangeT::FeeRule;
#[allow(clippy::type_complexity)]
fn propose_shielding<ParamsT>(
&self,
params: &ParamsT,
wallet_db: &Self::DataSource,
wallet_db: &Self::InputSource,
shielding_threshold: NonNegativeAmount,
source_addrs: &[TransparentAddress],
min_confirmations: NonZeroU32,
) -> Result<Proposal<Self::FeeRule, DbT::NoteRef>, InputSelectorError<DbT::Error, Self::Error>>
target_height: BlockHeight,
min_confirmations: u32,
) -> Result<
Proposal<Self::FeeRule, Infallible>,
InputSelectorError<<DbT as TransparentInputSource>::Error, Self::Error>,
>
where
ParamsT: consensus::Parameters,
{
let (target_height, latest_anchor) = wallet_db
.get_target_and_anchor_heights(min_confirmations)
.map_err(InputSelectorError::DataSource)
.and_then(|x| x.ok_or(InputSelectorError::SyncRequired))?;
let mut transparent_inputs: Vec<WalletTransparentOutput> = source_addrs
.iter()
.map(|taddr| wallet_db.get_unspent_transparent_outputs(taddr, latest_anchor, &[]))
.map(|taddr| {
wallet_db.get_unspent_transparent_outputs(
taddr,
target_height - min_confirmations,
&[],
)
})
.collect::<Result<Vec<Vec<_>>, _>>()
.map_err(InputSelectorError::DataSource)?
.into_iter()
@ -478,7 +529,7 @@ where
target_height,
&transparent_inputs,
&Vec::<TxOut>::new(),
&Vec::<ReceivedSaplingNote<DbT::NoteRef>>::new(),
&Vec::<ReceivedSaplingNote<Infallible>>::new(),
&Vec::<SaplingPayment>::new(),
&self.dust_output_policy,
);
@ -494,7 +545,7 @@ where
target_height,
&transparent_inputs,
&Vec::<TxOut>::new(),
&Vec::<ReceivedSaplingNote<DbT::NoteRef>>::new(),
&Vec::<ReceivedSaplingNote<Infallible>>::new(),
&Vec::<SaplingPayment>::new(),
&self.dust_output_policy,
)?
@ -508,12 +559,10 @@ where
Ok(Proposal {
transaction_request: TransactionRequest::empty(),
transparent_inputs,
sapling_inputs: vec![],
sapling_inputs: None,
balance,
fee_rule: (*self.change_strategy.fee_rule()).clone(),
min_confirmations,
min_target_height: target_height,
min_anchor_height: latest_anchor,
is_shielding: true,
})
} else {

View File

@ -112,7 +112,7 @@ pub enum ChangeError<E, NoteRefT> {
DustInputs {
/// The outpoints corresponding to transparent inputs having no current economic value.
transparent: Vec<OutPoint>,
/// The identifiers for Sapling inputs having not current economic value
/// The identifiers for Sapling inputs having no current economic value
sapling: Vec<NoteRefT>,
},
/// An error occurred that was specific to the change selection strategy in use.

View File

@ -44,10 +44,7 @@ use std::{
};
use incrementalmerkletree::Position;
use shardtree::{
error::{QueryError, ShardTreeError},
ShardTree,
};
use shardtree::{error::ShardTreeError, ShardTree};
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
@ -55,10 +52,7 @@ use zcash_primitives::{
memo::{Memo, MemoBytes},
sapling,
transaction::{
components::{
amount::{Amount, NonNegativeAmount},
OutPoint,
},
components::amount::{Amount, NonNegativeAmount},
Transaction, TxId,
},
zip32::{AccountId, DiversifierIndex},
@ -71,8 +65,8 @@ use zcash_client_backend::{
chain::{BlockSource, CommitmentTreeRoot},
scanning::{ScanPriority, ScanRange},
AccountBirthday, BlockMetadata, DecryptedTransaction, NoteId, NullifierQuery, PoolType,
Recipient, ScannedBlock, SentTransaction, ShieldedProtocol, WalletCommitmentTrees,
WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
Recipient, SaplingInputSource, ScannedBlock, SentTransaction, ShieldedProtocol,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
},
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock,
@ -82,6 +76,12 @@ use zcash_client_backend::{
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
#[cfg(feature = "transparent-inputs")]
use {
zcash_client_backend::data_api::TransparentInputSource,
zcash_primitives::transaction::components::OutPoint,
};
#[cfg(feature = "unstable")]
use {
crate::chain::{fsblockdb_with_blocks, BlockMeta},
@ -94,7 +94,7 @@ pub mod error;
pub mod wallet;
use wallet::{
commitment_tree::{self, get_checkpoint_depth, put_shard_roots},
commitment_tree::{self, put_shard_roots},
SubtreeScanProgress,
};
@ -167,10 +167,54 @@ impl<P: consensus::Parameters + Clone> WalletDb<Connection, P> {
}
}
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for WalletDb<C, P> {
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> SaplingInputSource
for WalletDb<C, P>
{
type Error = SqliteClientError;
type NoteRef = ReceivedNoteId;
fn select_spendable_sapling_notes(
&self,
account: AccountId,
target_value: Amount,
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
wallet::sapling::select_spendable_sapling_notes(
self.conn.borrow(),
account,
target_value,
anchor_height,
exclude,
)
}
}
#[cfg(feature = "transparent-inputs")]
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> TransparentInputSource
for WalletDb<C, P>
{
type Error = SqliteClientError;
fn get_unspent_transparent_outputs(
&self,
address: &TransparentAddress,
max_height: BlockHeight,
exclude: &[OutPoint],
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
wallet::get_unspent_transparent_outputs(
self.conn.borrow(),
&self.params,
address,
max_height,
exclude,
)
}
}
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for WalletDb<C, P> {
type Error = SqliteClientError;
fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
wallet::scan_queue_extrema(self.conn.borrow())
.map(|h| h.map(|range| *range.end()))
@ -277,36 +321,6 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
}
}
fn get_spendable_sapling_notes(
&self,
account: AccountId,
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
wallet::sapling::get_spendable_sapling_notes(
self.conn.borrow(),
account,
anchor_height,
exclude,
)
}
fn select_spendable_sapling_notes(
&self,
account: AccountId,
target_value: Amount,
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
wallet::sapling::select_spendable_sapling_notes(
self.conn.borrow(),
account,
target_value,
anchor_height,
exclude,
)
}
fn get_transparent_receivers(
&self,
_account: AccountId,
@ -320,27 +334,6 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
);
}
fn get_unspent_transparent_outputs(
&self,
_address: &TransparentAddress,
_max_height: BlockHeight,
_exclude: &[OutPoint],
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
#[cfg(feature = "transparent-inputs")]
return wallet::get_unspent_transparent_outputs(
self.conn.borrow(),
&self.params,
_address,
_max_height,
_exclude,
);
#[cfg(not(feature = "transparent-inputs"))]
panic!(
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
}
fn get_transparent_balances(
&self,
_account: AccountId,
@ -534,7 +527,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
// Update the Sapling note commitment tree with all newly read note commitments
let mut subtrees = subtrees.into_iter();
wdb.with_sapling_tree_mut::<_, _, SqliteClientError>(move |sapling_tree| {
wdb.with_sapling_tree_mut::<_, _, Self::Error>(move |sapling_tree| {
for (tree, checkpoints) in &mut subtrees {
sapling_tree.insert_tree(tree, checkpoints)?;
}
@ -792,18 +785,6 @@ impl<P: consensus::Parameters> WalletCommitmentTrees for WalletDb<rusqlite::Conn
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
Ok(())
}
fn get_checkpoint_depth(
&self,
min_confirmations: NonZeroU32,
) -> Result<usize, ShardTreeError<Self::Error>> {
get_checkpoint_depth(&self.conn, SAPLING_TABLES_PREFIX, min_confirmations)
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?
// `CheckpointPruned` is perhaps a little misleading; in this case it's that
// the chain tip is unknown, but if that were the case we should never have been
// calling this anyway.
.ok_or(ShardTreeError::Query(QueryError::CheckpointPruned))
}
}
impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTransaction<'conn>, P> {
@ -844,18 +825,6 @@ impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTran
roots,
)
}
fn get_checkpoint_depth(
&self,
min_confirmations: NonZeroU32,
) -> Result<usize, ShardTreeError<Self::Error>> {
get_checkpoint_depth(self.conn.0, SAPLING_TABLES_PREFIX, min_confirmations)
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?
// `CheckpointPruned` is perhaps a little misleading; in this case it's that
// the chain tip is unknown, but if that were the case we should never have been
// calling this anyway.
.ok_or(ShardTreeError::Query(QueryError::CheckpointPruned))
}
}
/// A handle for the SQLite block source.

View File

@ -70,7 +70,9 @@ use super::BlockDb;
#[cfg(feature = "transparent-inputs")]
use {
zcash_client_backend::data_api::wallet::{propose_shielding, shield_transparent_funds},
zcash_client_backend::data_api::wallet::{
input_selection::ShieldingSelector, propose_shielding, shield_transparent_funds,
},
zcash_primitives::legacy::TransparentAddress,
};
@ -483,7 +485,7 @@ impl<Cache> TestState<Cache> {
>,
>
where
InputsT: InputSelector<DataSource = WalletDb<Connection, Network>>,
InputsT: InputSelector<InputSource = WalletDb<Connection, Network>>,
{
let params = self.network();
let prover = test_prover();
@ -518,7 +520,7 @@ impl<Cache> TestState<Cache> {
>,
>
where
InputsT: InputSelector<DataSource = WalletDb<Connection, Network>>,
InputsT: InputSelector<InputSource = WalletDb<Connection, Network>>,
{
let params = self.network();
propose_transfer::<_, _, _, Infallible>(
@ -575,9 +577,9 @@ impl<Cache> TestState<Cache> {
input_selector: &InputsT,
shielding_threshold: NonNegativeAmount,
from_addrs: &[TransparentAddress],
min_confirmations: NonZeroU32,
min_confirmations: u32,
) -> Result<
Proposal<InputsT::FeeRule, ReceivedNoteId>,
Proposal<InputsT::FeeRule, Infallible>,
data_api::error::Error<
SqliteClientError,
Infallible,
@ -586,7 +588,7 @@ impl<Cache> TestState<Cache> {
>,
>
where
InputsT: InputSelector<DataSource = WalletDb<Connection, Network>>,
InputsT: ShieldingSelector<InputSource = WalletDb<Connection, Network>>,
{
let params = self.network();
propose_shielding::<_, _, _, Infallible>(
@ -639,7 +641,7 @@ impl<Cache> TestState<Cache> {
shielding_threshold: NonNegativeAmount,
usk: &UnifiedSpendingKey,
from_addrs: &[TransparentAddress],
min_confirmations: NonZeroU32,
min_confirmations: u32,
) -> Result<
TxId,
data_api::error::Error<
@ -650,7 +652,7 @@ impl<Cache> TestState<Cache> {
>,
>
where
InputsT: InputSelector<DataSource = WalletDb<Connection, Network>>,
InputsT: ShieldingSelector<InputSource = WalletDb<Connection, Network>>,
{
let params = self.network();
let prover = test_prover();

View File

@ -67,7 +67,6 @@
use incrementalmerkletree::Retention;
use rusqlite::{self, named_params, OptionalExtension};
use shardtree::ShardTree;
use std::cmp;
use std::collections::{BTreeMap, HashMap};
use std::convert::TryFrom;
use std::io::{self, Cursor};
@ -100,7 +99,7 @@ use zcash_client_backend::{
wallet::WalletTx,
};
use crate::wallet::commitment_tree::SqliteShardStore;
use crate::wallet::commitment_tree::{get_max_checkpointed_height, SqliteShardStore};
use crate::{
error::SqliteClientError, SqlTransaction, WalletCommitmentTrees, WalletDb, PRUNING_DEPTH,
SAPLING_TABLES_PREFIX,
@ -932,19 +931,19 @@ pub(crate) fn get_target_and_anchor_heights(
conn: &rusqlite::Connection,
min_confirmations: NonZeroU32,
) -> Result<Option<(BlockHeight, BlockHeight)>, rusqlite::Error> {
scan_queue_extrema(conn).map(|heights| {
heights.map(|range| {
let target_height = *range.end() + 1;
// Select an anchor min_confirmations back from the target block,
// unless that would be before the earliest block we have.
let anchor_height = BlockHeight::from(cmp::max(
u32::from(target_height).saturating_sub(min_confirmations.into()),
u32::from(*range.start()),
));
match scan_queue_extrema(conn)?.map(|range| *range.end()) {
Some(chain_tip_height) => {
let sapling_anchor_height = get_max_checkpointed_height(
conn,
SAPLING_TABLES_PREFIX,
chain_tip_height,
min_confirmations,
)?;
(target_height, anchor_height)
})
})
Ok(sapling_anchor_height.map(|h| (chain_tip_height + 1, h)))
}
None => Ok(None),
}
}
fn parse_block_metadata(
@ -1915,7 +1914,9 @@ mod tests {
PRUNING_DEPTH,
},
zcash_client_backend::{
data_api::{wallet::input_selection::GreedyInputSelector, WalletWrite},
data_api::{
wallet::input_selection::GreedyInputSelector, TransparentInputSource, WalletWrite,
},
encoding::AddressCodec,
fees::{fixed, DustOutputPolicy},
wallet::WalletTransparentOutput,
@ -2141,13 +2142,7 @@ mod tests {
DustOutputPolicy::default(),
);
let txid = st
.shield_transparent_funds(
&input_selector,
value,
&usk,
&[*taddr],
NonZeroU32::new(1).unwrap(),
)
.shield_transparent_funds(&input_selector, value, &usk, &[*taddr], 1)
.unwrap();
// The wallet should have zero transparent balance, because the shielding

View File

@ -21,8 +21,6 @@ use zcash_primitives::{consensus::BlockHeight, merkle_tree::HashSer};
use zcash_client_backend::serialization::shardtree::{read_shard, write_shard};
use super::scan_queue_extrema;
/// Errors that can appear in SQLite-back [`ShardStore`] implementation operations.
#[derive(Debug)]
pub enum Error {
@ -173,6 +171,7 @@ impl<'conn, 'a: 'conn, H: HashSer, const SHARD_HEIGHT: u8> ShardStore
checkpoint_depth: usize,
) -> Result<Option<(Self::CheckpointId, Checkpoint)>, Self::Error> {
get_checkpoint_at_depth(self.conn, self.table_prefix, checkpoint_depth)
.map_err(Error::Query)
}
fn get_checkpoint(
@ -280,6 +279,7 @@ impl<H: HashSer, const SHARD_HEIGHT: u8> ShardStore
checkpoint_depth: usize,
) -> Result<Option<(Self::CheckpointId, Checkpoint)>, Self::Error> {
get_checkpoint_at_depth(&self.conn, self.table_prefix, checkpoint_depth)
.map_err(Error::Query)
}
fn get_checkpoint(
@ -750,38 +750,37 @@ pub(crate) fn get_checkpoint(
.transpose()
}
pub(crate) fn get_checkpoint_depth(
pub(crate) fn get_max_checkpointed_height(
conn: &rusqlite::Connection,
table_prefix: &'static str,
chain_tip_height: BlockHeight,
min_confirmations: NonZeroU32,
) -> Result<Option<usize>, rusqlite::Error> {
scan_queue_extrema(conn)?
.map(|range| *range.end())
.map(|chain_tip| {
let max_checkpoint_height =
u32::from(chain_tip).saturating_sub(u32::from(min_confirmations) - 1);
) -> Result<Option<BlockHeight>, rusqlite::Error> {
let max_checkpoint_height =
u32::from(chain_tip_height).saturating_sub(u32::from(min_confirmations) - 1);
// We exclude from consideration all checkpoints having heights greater than the maximum
// checkpoint height. The checkpoint depth is the number of excluded checkpoints + 1.
conn.query_row(
&format!(
"SELECT COUNT(*)
FROM {}_tree_checkpoints
WHERE checkpoint_id > :max_checkpoint_height",
table_prefix
),
named_params![":max_checkpoint_height": max_checkpoint_height],
|row| row.get::<_, usize>(0).map(|s| s + 1),
)
})
.transpose()
// We exclude from consideration all checkpoints having heights greater than the maximum
// checkpoint height. The checkpoint depth is the number of excluded checkpoints + 1.
conn.query_row(
&format!(
"SELECT checkpoint_id
FROM {}_tree_checkpoints
WHERE checkpoint_id <= :max_checkpoint_height
ORDER BY checkpoint_id DESC
LIMIT 1",
table_prefix
),
named_params![":max_checkpoint_height": max_checkpoint_height],
|row| row.get::<_, u32>(0).map(BlockHeight::from),
)
.optional()
}
pub(crate) fn get_checkpoint_at_depth(
conn: &rusqlite::Connection,
table_prefix: &'static str,
checkpoint_depth: usize,
) -> Result<Option<(BlockHeight, Checkpoint)>, Error> {
) -> Result<Option<(BlockHeight, Checkpoint)>, rusqlite::Error> {
if checkpoint_depth == 0 {
return Ok(None);
}
@ -806,27 +805,21 @@ pub(crate) fn get_checkpoint_at_depth(
))
},
)
.optional()
.map_err(Error::Query)?;
.optional()?;
checkpoint_parts
.map(|(checkpoint_id, pos_opt)| {
let mut stmt = conn
.prepare_cached(&format!(
"SELECT mark_removed_position
let mut stmt = conn.prepare_cached(&format!(
"SELECT mark_removed_position
FROM {}_tree_checkpoint_marks_removed
WHERE checkpoint_id = ?",
table_prefix
))
.map_err(Error::Query)?;
let mark_removed_rows = stmt
.query([u32::from(checkpoint_id)])
.map_err(Error::Query)?;
table_prefix
))?;
let mark_removed_rows = stmt.query([u32::from(checkpoint_id)])?;
let marks_removed = mark_removed_rows
.mapped(|row| row.get::<_, u64>(0).map(Position::from))
.collect::<Result<BTreeSet<_>, _>>()
.map_err(Error::Query)?;
.collect::<Result<BTreeSet<_>, _>>()?;
Ok((
checkpoint_id,
@ -1168,6 +1161,7 @@ mod tests {
// simulate discovery of a note
let mut tree = ShardTree::<_, 6, 3>::new(store, 10);
let checkpoint_height = BlockHeight::from(3);
tree.batch_insert(
Position::from(24),
('a'..='h').into_iter().map(|c| {
@ -1176,7 +1170,7 @@ mod tests {
match c {
'c' => Retention::Marked,
'h' => Retention::Checkpoint {
id: BlockHeight::from(3),
id: checkpoint_height,
is_marked: false,
},
_ => Retention::Ephemeral,
@ -1187,7 +1181,9 @@ mod tests {
.unwrap();
// construct a witness for the note
let witness = tree.witness(Position::from(26), 0).unwrap();
let witness = tree
.witness_at_checkpoint_id(Position::from(26), &checkpoint_height)
.unwrap();
assert_eq!(
witness.path_elems(),
&[

View File

@ -159,62 +159,6 @@ fn unscanned_tip_exists(
)
}
pub(crate) fn get_spendable_sapling_notes(
conn: &Connection,
account: AccountId,
anchor_height: BlockHeight,
exclude: &[ReceivedNoteId],
) -> Result<Vec<ReceivedSaplingNote<ReceivedNoteId>>, SqliteClientError> {
let birthday_height = match wallet_birthday(conn)? {
Some(birthday) => birthday,
None => {
// the wallet birthday can only be unknown if there are no accounts in the wallet; in
// such a case, the wallet has no notes to spend.
return Ok(vec![]);
}
};
if unscanned_tip_exists(conn, anchor_height)? {
return Ok(vec![]);
}
let mut stmt_select_notes = conn.prepare_cached(
"SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position
FROM sapling_received_notes
INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx
WHERE account = :account
AND commitment_tree_position IS NOT NULL
AND spent IS NULL
AND transactions.block <= :anchor_height
AND id_note NOT IN rarray(:exclude)
AND NOT EXISTS (
SELECT 1 FROM v_sapling_shard_unscanned_ranges unscanned
-- select all the unscanned ranges involving the shard containing this note
WHERE sapling_received_notes.commitment_tree_position >= unscanned.start_position
AND sapling_received_notes.commitment_tree_position < unscanned.end_position_exclusive
-- exclude unscanned ranges that start above the anchor height (they don't affect spendability)
AND unscanned.block_range_start <= :anchor_height
-- exclude unscanned ranges that end below the wallet birthday
AND unscanned.block_range_end > :wallet_birthday
)",
)?;
let excluded: Vec<Value> = exclude.iter().map(|n| Value::from(n.0)).collect();
let excluded_ptr = Rc::new(excluded);
let notes = stmt_select_notes.query_and_then(
named_params![
":account": u32::from(account),
":anchor_height": u32::from(anchor_height),
":exclude": &excluded_ptr,
":wallet_birthday": u32::from(birthday_height)
],
to_spendable_note,
)?;
notes.collect::<Result<_, _>>()
}
pub(crate) fn select_spendable_sapling_notes(
conn: &Connection,
account: AccountId,
@ -769,7 +713,7 @@ pub(crate) mod tests {
st.propose_standard_transfer::<Infallible>(
account,
StandardFeeRule::Zip317,
NonZeroU32::new(10).unwrap(),
NonZeroU32::new(2).unwrap(),
&to,
NonNegativeAmount::const_from_u64(70000),
None,
@ -1468,7 +1412,7 @@ pub(crate) mod tests {
NonNegativeAmount::from_u64(10000).unwrap(),
&usk,
&[*taddr],
NonZeroU32::new(1).unwrap()
1
),
Ok(_)
);

View File

@ -5,7 +5,7 @@ use super::{
components::{
amount::NonNegativeAmount,
sapling::{self, GrothProofBytes},
transparent, Amount,
transparent,
},
sighash_v4::v4_signature_hash,
sighash_v5::v5_signature_hash,
@ -13,7 +13,7 @@ use super::{
};
#[cfg(feature = "zfuture")]
use crate::extensions::transparent::Precondition;
use {super::components::Amount, crate::extensions::transparent::Precondition};
pub const SIGHASH_ALL: u8 = 0x01;
pub const SIGHASH_NONE: u8 = 0x02;