Merge pull request #1182 from nuttycom/sqlite_wallet/orchard_support
`zcash_client_sqlite`: Add Orchard wallet support
This commit is contained in:
commit
bb466de379
|
@ -76,6 +76,8 @@ and this library adheres to Rust's notion of
|
||||||
constraint on its `<AccountId>` parameter has been strengthened to `Copy`.
|
constraint on its `<AccountId>` parameter has been strengthened to `Copy`.
|
||||||
- `zcash_client_backend::fees`:
|
- `zcash_client_backend::fees`:
|
||||||
- Arguments to `ChangeStrategy::compute_balance` have changed.
|
- Arguments to `ChangeStrategy::compute_balance` have changed.
|
||||||
|
- `ChangeError::DustInputs` now has an `orchard` field behind the `orchard`
|
||||||
|
feature flag.
|
||||||
- `zcash_client_backend::proto`:
|
- `zcash_client_backend::proto`:
|
||||||
- `ProposalDecodingError` has a new variant `TransparentMemo`.
|
- `ProposalDecodingError` has a new variant `TransparentMemo`.
|
||||||
- `zcash_client_backend::zip321::render::amount_str` now takes a
|
- `zcash_client_backend::zip321::render::amount_str` now takes a
|
||||||
|
|
|
@ -445,8 +445,15 @@ where
|
||||||
)
|
)
|
||||||
.map_err(InputSelectorError::Proposal);
|
.map_err(InputSelectorError::Proposal);
|
||||||
}
|
}
|
||||||
Err(ChangeError::DustInputs { mut sapling, .. }) => {
|
Err(ChangeError::DustInputs {
|
||||||
|
mut sapling,
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
mut orchard,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
exclude.append(&mut sapling);
|
exclude.append(&mut sapling);
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
exclude.append(&mut orchard);
|
||||||
}
|
}
|
||||||
Err(ChangeError::InsufficientFunds { required, .. }) => {
|
Err(ChangeError::InsufficientFunds { required, .. }) => {
|
||||||
amount_required = required;
|
amount_required = required;
|
||||||
|
|
|
@ -149,6 +149,9 @@ pub enum ChangeError<E, NoteRefT> {
|
||||||
transparent: Vec<OutPoint>,
|
transparent: Vec<OutPoint>,
|
||||||
/// The identifiers for Sapling inputs having no current economic value
|
/// The identifiers for Sapling inputs having no current economic value
|
||||||
sapling: Vec<NoteRefT>,
|
sapling: Vec<NoteRefT>,
|
||||||
|
/// The identifiers for Orchard inputs having no current economic value
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
orchard: Vec<NoteRefT>,
|
||||||
},
|
},
|
||||||
/// An error occurred that was specific to the change selection strategy in use.
|
/// An error occurred that was specific to the change selection strategy in use.
|
||||||
StrategyError(E),
|
StrategyError(E),
|
||||||
|
@ -169,9 +172,13 @@ impl<E, NoteRefT> ChangeError<E, NoteRefT> {
|
||||||
ChangeError::DustInputs {
|
ChangeError::DustInputs {
|
||||||
transparent,
|
transparent,
|
||||||
sapling,
|
sapling,
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
orchard,
|
||||||
} => ChangeError::DustInputs {
|
} => ChangeError::DustInputs {
|
||||||
transparent,
|
transparent,
|
||||||
sapling,
|
sapling,
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
orchard,
|
||||||
},
|
},
|
||||||
ChangeError::StrategyError(e) => ChangeError::StrategyError(f(e)),
|
ChangeError::StrategyError(e) => ChangeError::StrategyError(f(e)),
|
||||||
ChangeError::BundleError(e) => ChangeError::BundleError(e),
|
ChangeError::BundleError(e) => ChangeError::BundleError(e),
|
||||||
|
@ -194,10 +201,21 @@ impl<CE: fmt::Display, N: fmt::Display> fmt::Display for ChangeError<CE, N> {
|
||||||
ChangeError::DustInputs {
|
ChangeError::DustInputs {
|
||||||
transparent,
|
transparent,
|
||||||
sapling,
|
sapling,
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
orchard,
|
||||||
} => {
|
} => {
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
let orchard_len = orchard.len();
|
||||||
|
#[cfg(not(feature = "orchard"))]
|
||||||
|
let orchard_len = 0;
|
||||||
|
|
||||||
// we can't encode the UA to its string representation because we
|
// we can't encode the UA to its string representation because we
|
||||||
// don't have network parameters here
|
// don't have network parameters here
|
||||||
write!(f, "Insufficient funds: {} dust inputs were present, but would cost more to spend than they are worth.", transparent.len() + sapling.len())
|
write!(
|
||||||
|
f,
|
||||||
|
"Insufficient funds: {} dust inputs were present, but would cost more to spend than they are worth.",
|
||||||
|
transparent.len() + sapling.len() + orchard_len,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
ChangeError::StrategyError(err) => {
|
ChangeError::StrategyError(err) => {
|
||||||
write!(f, "{}", err)
|
write!(f, "{}", err)
|
||||||
|
|
|
@ -17,30 +17,26 @@ use super::{
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
use super::orchard as orchard_fees;
|
use super::orchard as orchard_fees;
|
||||||
|
|
||||||
|
pub(crate) struct NetFlows {
|
||||||
|
t_in: NonNegativeAmount,
|
||||||
|
t_out: NonNegativeAmount,
|
||||||
|
sapling_in: NonNegativeAmount,
|
||||||
|
sapling_out: NonNegativeAmount,
|
||||||
|
orchard_in: NonNegativeAmount,
|
||||||
|
orchard_out: NonNegativeAmount,
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn single_change_output_balance<
|
pub(crate) fn calculate_net_flows<NoteRefT: Clone, F: FeeRule, E>(
|
||||||
P: consensus::Parameters,
|
|
||||||
NoteRefT: Clone,
|
|
||||||
F: FeeRule,
|
|
||||||
E,
|
|
||||||
>(
|
|
||||||
params: &P,
|
|
||||||
fee_rule: &F,
|
|
||||||
target_height: BlockHeight,
|
|
||||||
transparent_inputs: &[impl transparent::InputView],
|
transparent_inputs: &[impl transparent::InputView],
|
||||||
transparent_outputs: &[impl transparent::OutputView],
|
transparent_outputs: &[impl transparent::OutputView],
|
||||||
sapling: &impl sapling_fees::BundleView<NoteRefT>,
|
sapling: &impl sapling_fees::BundleView<NoteRefT>,
|
||||||
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
|
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
|
||||||
dust_output_policy: &DustOutputPolicy,
|
) -> Result<NetFlows, ChangeError<E, NoteRefT>>
|
||||||
default_dust_threshold: NonNegativeAmount,
|
|
||||||
change_memo: Option<MemoBytes>,
|
|
||||||
_fallback_change_pool: ShieldedProtocol,
|
|
||||||
) -> Result<TransactionBalance, ChangeError<E, NoteRefT>>
|
|
||||||
where
|
where
|
||||||
E: From<F::Error> + From<BalanceError>,
|
E: From<F::Error> + From<BalanceError>,
|
||||||
{
|
{
|
||||||
let overflow = || ChangeError::StrategyError(E::from(BalanceError::Overflow));
|
let overflow = || ChangeError::StrategyError(E::from(BalanceError::Overflow));
|
||||||
let underflow = || ChangeError::StrategyError(E::from(BalanceError::Underflow));
|
|
||||||
|
|
||||||
let t_in = transparent_inputs
|
let t_in = transparent_inputs
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -85,13 +81,30 @@ where
|
||||||
#[cfg(not(feature = "orchard"))]
|
#[cfg(not(feature = "orchard"))]
|
||||||
let orchard_out = NonNegativeAmount::ZERO;
|
let orchard_out = NonNegativeAmount::ZERO;
|
||||||
|
|
||||||
|
Ok(NetFlows {
|
||||||
|
t_in,
|
||||||
|
t_out,
|
||||||
|
sapling_in,
|
||||||
|
sapling_out,
|
||||||
|
orchard_in,
|
||||||
|
orchard_out,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn single_change_output_policy<NoteRefT: Clone, F: FeeRule, E>(
|
||||||
|
_net_flows: &NetFlows,
|
||||||
|
_fallback_change_pool: ShieldedProtocol,
|
||||||
|
) -> Result<(ShieldedProtocol, usize, usize), ChangeError<E, NoteRefT>>
|
||||||
|
where
|
||||||
|
E: From<F::Error> + From<BalanceError>,
|
||||||
|
{
|
||||||
// TODO: implement a less naive strategy for selecting the pool to which change will be sent.
|
// TODO: implement a less naive strategy for selecting the pool to which change will be sent.
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
let (change_pool, sapling_change, orchard_change) =
|
let (change_pool, sapling_change, orchard_change) =
|
||||||
if orchard_in.is_positive() || orchard_out.is_positive() {
|
if _net_flows.orchard_in.is_positive() || _net_flows.orchard_out.is_positive() {
|
||||||
// Send change to Orchard if we're spending any Orchard inputs or creating any Orchard outputs
|
// Send change to Orchard if we're spending any Orchard inputs or creating any Orchard outputs
|
||||||
(ShieldedProtocol::Orchard, 0, 1)
|
(ShieldedProtocol::Orchard, 0, 1)
|
||||||
} else if sapling_in.is_positive() || sapling_out.is_positive() {
|
} else if _net_flows.sapling_in.is_positive() || _net_flows.sapling_out.is_positive() {
|
||||||
// Otherwise, send change to Sapling if we're spending any Sapling inputs or creating any
|
// Otherwise, send change to Sapling if we're spending any Sapling inputs or creating any
|
||||||
// Sapling outputs, so that we avoid pool-crossing.
|
// Sapling outputs, so that we avoid pool-crossing.
|
||||||
(ShieldedProtocol::Sapling, 1, 0)
|
(ShieldedProtocol::Sapling, 1, 0)
|
||||||
|
@ -104,18 +117,68 @@ where
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
#[cfg(not(feature = "orchard"))]
|
#[cfg(not(feature = "orchard"))]
|
||||||
let (change_pool, sapling_change) = (ShieldedProtocol::Sapling, 1);
|
let (change_pool, sapling_change, orchard_change) = (ShieldedProtocol::Sapling, 1, 0);
|
||||||
|
|
||||||
|
Ok((change_pool, sapling_change, orchard_change))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) fn single_change_output_balance<
|
||||||
|
P: consensus::Parameters,
|
||||||
|
NoteRefT: Clone,
|
||||||
|
F: FeeRule,
|
||||||
|
E,
|
||||||
|
>(
|
||||||
|
params: &P,
|
||||||
|
fee_rule: &F,
|
||||||
|
target_height: BlockHeight,
|
||||||
|
transparent_inputs: &[impl transparent::InputView],
|
||||||
|
transparent_outputs: &[impl transparent::OutputView],
|
||||||
|
sapling: &impl sapling_fees::BundleView<NoteRefT>,
|
||||||
|
#[cfg(feature = "orchard")] orchard: &impl orchard_fees::BundleView<NoteRefT>,
|
||||||
|
dust_output_policy: &DustOutputPolicy,
|
||||||
|
default_dust_threshold: NonNegativeAmount,
|
||||||
|
change_memo: Option<MemoBytes>,
|
||||||
|
_fallback_change_pool: ShieldedProtocol,
|
||||||
|
) -> Result<TransactionBalance, ChangeError<E, NoteRefT>>
|
||||||
|
where
|
||||||
|
E: From<F::Error> + From<BalanceError>,
|
||||||
|
{
|
||||||
|
let overflow = || ChangeError::StrategyError(E::from(BalanceError::Overflow));
|
||||||
|
let underflow = || ChangeError::StrategyError(E::from(BalanceError::Underflow));
|
||||||
|
|
||||||
|
let net_flows = calculate_net_flows::<NoteRefT, F, E>(
|
||||||
|
transparent_inputs,
|
||||||
|
transparent_outputs,
|
||||||
|
sapling,
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
orchard,
|
||||||
|
)?;
|
||||||
|
let (change_pool, sapling_change, _orchard_change) =
|
||||||
|
single_change_output_policy::<NoteRefT, F, E>(&net_flows, _fallback_change_pool)?;
|
||||||
|
|
||||||
|
let sapling_input_count = sapling
|
||||||
|
.bundle_type()
|
||||||
|
.num_spends(sapling.inputs().len())
|
||||||
|
.map_err(ChangeError::BundleError)?;
|
||||||
|
let sapling_output_count = sapling
|
||||||
|
.bundle_type()
|
||||||
|
.num_outputs(
|
||||||
|
sapling.inputs().len(),
|
||||||
|
sapling.outputs().len() + sapling_change,
|
||||||
|
)
|
||||||
|
.map_err(ChangeError::BundleError)?;
|
||||||
|
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
let orchard_num_actions = orchard
|
let orchard_action_count = orchard
|
||||||
.bundle_type()
|
.bundle_type()
|
||||||
.num_actions(
|
.num_actions(
|
||||||
orchard.inputs().len(),
|
orchard.inputs().len(),
|
||||||
orchard.outputs().len() + orchard_change,
|
orchard.outputs().len() + _orchard_change,
|
||||||
)
|
)
|
||||||
.map_err(ChangeError::BundleError)?;
|
.map_err(ChangeError::BundleError)?;
|
||||||
#[cfg(not(feature = "orchard"))]
|
#[cfg(not(feature = "orchard"))]
|
||||||
let orchard_num_actions = 0;
|
let orchard_action_count = 0;
|
||||||
|
|
||||||
let fee_amount = fee_rule
|
let fee_amount = fee_rule
|
||||||
.fee_required(
|
.fee_required(
|
||||||
|
@ -123,23 +186,16 @@ where
|
||||||
target_height,
|
target_height,
|
||||||
transparent_inputs,
|
transparent_inputs,
|
||||||
transparent_outputs,
|
transparent_outputs,
|
||||||
sapling
|
sapling_input_count,
|
||||||
.bundle_type()
|
sapling_output_count,
|
||||||
.num_spends(sapling.inputs().len())
|
orchard_action_count,
|
||||||
.map_err(ChangeError::BundleError)?,
|
|
||||||
sapling
|
|
||||||
.bundle_type()
|
|
||||||
.num_outputs(
|
|
||||||
sapling.inputs().len(),
|
|
||||||
sapling.outputs().len() + sapling_change,
|
|
||||||
)
|
|
||||||
.map_err(ChangeError::BundleError)?,
|
|
||||||
orchard_num_actions,
|
|
||||||
)
|
)
|
||||||
.map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?;
|
.map_err(|fee_error| ChangeError::StrategyError(E::from(fee_error)))?;
|
||||||
|
|
||||||
let total_in = (t_in + sapling_in + orchard_in).ok_or_else(overflow)?;
|
let total_in =
|
||||||
let total_out = (t_out + sapling_out + orchard_out + fee_amount).ok_or_else(overflow)?;
|
(net_flows.t_in + net_flows.sapling_in + net_flows.orchard_in).ok_or_else(overflow)?;
|
||||||
|
let total_out = (net_flows.t_out + net_flows.sapling_out + net_flows.orchard_out + fee_amount)
|
||||||
|
.ok_or_else(overflow)?;
|
||||||
|
|
||||||
let proposed_change = (total_in - total_out).ok_or(ChangeError::InsufficientFunds {
|
let proposed_change = (total_in - total_out).ok_or(ChangeError::InsufficientFunds {
|
||||||
available: total_in,
|
available: total_in,
|
||||||
|
|
|
@ -16,8 +16,8 @@ use zcash_primitives::{
|
||||||
use crate::ShieldedProtocol;
|
use crate::ShieldedProtocol;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
|
common::{calculate_net_flows, single_change_output_balance, single_change_output_policy},
|
||||||
DustOutputPolicy, TransactionBalance,
|
sapling as sapling_fees, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
|
@ -93,32 +93,73 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
let mut orchard_dust: Vec<NoteRefT> = orchard
|
||||||
|
.inputs()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|i| {
|
||||||
|
if orchard_fees::InputView::<NoteRefT>::value(i) < self.fee_rule.marginal_fee() {
|
||||||
|
Some(orchard_fees::InputView::<NoteRefT>::note_id(i).clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
#[cfg(not(feature = "orchard"))]
|
||||||
|
let mut orchard_dust: Vec<NoteRefT> = vec![];
|
||||||
|
|
||||||
// Depending on the shape of the transaction, we may be able to spend up to
|
// Depending on the shape of the transaction, we may be able to spend up to
|
||||||
// `grace_actions - 1` dust inputs. If we don't have any dust inputs though,
|
// `grace_actions - 1` dust inputs. If we don't have any dust inputs though,
|
||||||
// we don't need to worry about any of that.
|
// we don't need to worry about any of that.
|
||||||
if !(transparent_dust.is_empty() && sapling_dust.is_empty()) {
|
if !(transparent_dust.is_empty() && sapling_dust.is_empty() && orchard_dust.is_empty()) {
|
||||||
let t_non_dust = transparent_inputs.len() - transparent_dust.len();
|
let t_non_dust = transparent_inputs.len() - transparent_dust.len();
|
||||||
let t_allowed_dust = transparent_outputs.len().saturating_sub(t_non_dust);
|
let t_allowed_dust = transparent_outputs.len().saturating_sub(t_non_dust);
|
||||||
|
|
||||||
// We add one to the sapling outputs for the (single) change output Note that this
|
// We add one to either the Sapling or Orchard outputs for the (single)
|
||||||
// means that wallet-internal shielding transactions are an opportunity to spend a dust
|
// change output. Note that this means that wallet-internal shielding
|
||||||
// note.
|
// transactions are an opportunity to spend a dust note.
|
||||||
|
let net_flows = calculate_net_flows::<NoteRefT, Self::FeeRule, Self::Error>(
|
||||||
|
transparent_inputs,
|
||||||
|
transparent_outputs,
|
||||||
|
sapling,
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
orchard,
|
||||||
|
)?;
|
||||||
|
let (_, sapling_change, orchard_change) =
|
||||||
|
single_change_output_policy::<NoteRefT, Self::FeeRule, Self::Error>(
|
||||||
|
&net_flows,
|
||||||
|
self.fallback_change_pool,
|
||||||
|
)?;
|
||||||
|
|
||||||
let s_non_dust = sapling.inputs().len() - sapling_dust.len();
|
let s_non_dust = sapling.inputs().len() - sapling_dust.len();
|
||||||
let s_allowed_dust = (sapling.outputs().len() + 1).saturating_sub(s_non_dust);
|
let s_allowed_dust =
|
||||||
|
(sapling.outputs().len() + sapling_change).saturating_sub(s_non_dust);
|
||||||
|
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
let (orchard_inputs_len, orchard_outputs_len) =
|
||||||
|
(orchard.inputs().len(), orchard.outputs().len());
|
||||||
|
#[cfg(not(feature = "orchard"))]
|
||||||
|
let (orchard_inputs_len, orchard_outputs_len) = (0, 0);
|
||||||
|
|
||||||
|
let o_non_dust = orchard_inputs_len - orchard_dust.len();
|
||||||
|
let o_allowed_dust = (orchard_outputs_len + orchard_change).saturating_sub(o_non_dust);
|
||||||
|
|
||||||
let available_grace_inputs = self
|
let available_grace_inputs = self
|
||||||
.fee_rule
|
.fee_rule
|
||||||
.grace_actions()
|
.grace_actions()
|
||||||
.saturating_sub(t_non_dust)
|
.saturating_sub(t_non_dust)
|
||||||
.saturating_sub(s_non_dust);
|
.saturating_sub(s_non_dust)
|
||||||
|
.saturating_sub(o_non_dust);
|
||||||
|
|
||||||
let mut t_disallowed_dust = transparent_dust.len().saturating_sub(t_allowed_dust);
|
let mut t_disallowed_dust = transparent_dust.len().saturating_sub(t_allowed_dust);
|
||||||
let mut s_disallowed_dust = sapling_dust.len().saturating_sub(s_allowed_dust);
|
let mut s_disallowed_dust = sapling_dust.len().saturating_sub(s_allowed_dust);
|
||||||
|
let mut o_disallowed_dust = orchard_dust.len().saturating_sub(o_allowed_dust);
|
||||||
|
|
||||||
if available_grace_inputs > 0 {
|
if available_grace_inputs > 0 {
|
||||||
// If we have available grace inputs, allocate them first to transparent dust
|
// If we have available grace inputs, allocate them first to transparent dust
|
||||||
// and then to Sapling dust. The caller has provided inputs that it is willing
|
// and then to Sapling dust followed by Orchard dust. The caller has provided
|
||||||
// to spend, so we don't need to consider privacy effects at this layer.
|
// inputs that it is willing to spend, so we don't need to consider privacy
|
||||||
|
// effects at this layer.
|
||||||
let t_grace_dust = available_grace_inputs.saturating_sub(t_disallowed_dust);
|
let t_grace_dust = available_grace_inputs.saturating_sub(t_disallowed_dust);
|
||||||
t_disallowed_dust = t_disallowed_dust.saturating_sub(t_grace_dust);
|
t_disallowed_dust = t_disallowed_dust.saturating_sub(t_grace_dust);
|
||||||
|
|
||||||
|
@ -126,6 +167,12 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
||||||
.saturating_sub(t_grace_dust)
|
.saturating_sub(t_grace_dust)
|
||||||
.saturating_sub(s_disallowed_dust);
|
.saturating_sub(s_disallowed_dust);
|
||||||
s_disallowed_dust = s_disallowed_dust.saturating_sub(s_grace_dust);
|
s_disallowed_dust = s_disallowed_dust.saturating_sub(s_grace_dust);
|
||||||
|
|
||||||
|
let o_grace_dust = available_grace_inputs
|
||||||
|
.saturating_sub(t_grace_dust)
|
||||||
|
.saturating_sub(s_grace_dust)
|
||||||
|
.saturating_sub(o_disallowed_dust);
|
||||||
|
o_disallowed_dust = o_disallowed_dust.saturating_sub(o_grace_dust);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate the lists of inputs to be disregarded in input selection to just the
|
// Truncate the lists of inputs to be disregarded in input selection to just the
|
||||||
|
@ -135,11 +182,16 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
||||||
transparent_dust.truncate(t_disallowed_dust);
|
transparent_dust.truncate(t_disallowed_dust);
|
||||||
sapling_dust.reverse();
|
sapling_dust.reverse();
|
||||||
sapling_dust.truncate(s_disallowed_dust);
|
sapling_dust.truncate(s_disallowed_dust);
|
||||||
|
orchard_dust.reverse();
|
||||||
|
orchard_dust.truncate(o_disallowed_dust);
|
||||||
|
|
||||||
if !(transparent_dust.is_empty() && sapling_dust.is_empty()) {
|
if !(transparent_dust.is_empty() && sapling_dust.is_empty() && orchard_dust.is_empty())
|
||||||
|
{
|
||||||
return Err(ChangeError::DustInputs {
|
return Err(ChangeError::DustInputs {
|
||||||
transparent: transparent_dust,
|
transparent: transparent_dust,
|
||||||
sapling: sapling_dust,
|
sapling: sapling_dust,
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
orchard: orchard_dust,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,13 +69,13 @@ use zcash_client_backend::{
|
||||||
},
|
},
|
||||||
proto::compact_formats::CompactBlock,
|
proto::compact_formats::CompactBlock,
|
||||||
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
|
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
|
||||||
DecryptedOutput, ShieldedProtocol, TransferType,
|
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
|
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
|
||||||
|
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
use zcash_client_backend::{data_api::ORCHARD_SHARD_HEIGHT, PoolType};
|
use zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT;
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
use {
|
use {
|
||||||
|
@ -111,6 +111,7 @@ pub(crate) const PRUNING_DEPTH: u32 = 100;
|
||||||
pub(crate) const VERIFY_LOOKAHEAD: u32 = 10;
|
pub(crate) const VERIFY_LOOKAHEAD: u32 = 10;
|
||||||
|
|
||||||
pub(crate) const SAPLING_TABLES_PREFIX: &str = "sapling";
|
pub(crate) const SAPLING_TABLES_PREFIX: &str = "sapling";
|
||||||
|
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
pub(crate) const ORCHARD_TABLES_PREFIX: &str = "orchard";
|
pub(crate) const ORCHARD_TABLES_PREFIX: &str = "orchard";
|
||||||
|
|
||||||
|
@ -208,7 +209,20 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
|
||||||
txid,
|
txid,
|
||||||
index,
|
index,
|
||||||
),
|
),
|
||||||
ShieldedProtocol::Orchard => Ok(None),
|
ShieldedProtocol::Orchard => {
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
return wallet::orchard::get_spendable_orchard_note(
|
||||||
|
self.conn.borrow(),
|
||||||
|
&self.params,
|
||||||
|
txid,
|
||||||
|
index,
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(not(feature = "orchard"))]
|
||||||
|
return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded(
|
||||||
|
ShieldedProtocol::Orchard,
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,14 +234,25 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
|
||||||
anchor_height: BlockHeight,
|
anchor_height: BlockHeight,
|
||||||
exclude: &[Self::NoteRef],
|
exclude: &[Self::NoteRef],
|
||||||
) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
|
) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
|
||||||
wallet::sapling::select_spendable_sapling_notes(
|
let received_iter = std::iter::empty();
|
||||||
|
let received_iter = received_iter.chain(wallet::sapling::select_spendable_sapling_notes(
|
||||||
self.conn.borrow(),
|
self.conn.borrow(),
|
||||||
&self.params,
|
&self.params,
|
||||||
account,
|
account,
|
||||||
target_value,
|
target_value,
|
||||||
anchor_height,
|
anchor_height,
|
||||||
exclude,
|
exclude,
|
||||||
)
|
)?);
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
let received_iter = received_iter.chain(wallet::orchard::select_spendable_orchard_notes(
|
||||||
|
self.conn.borrow(),
|
||||||
|
&self.params,
|
||||||
|
account,
|
||||||
|
target_value,
|
||||||
|
anchor_height,
|
||||||
|
exclude,
|
||||||
|
)?);
|
||||||
|
Ok(received_iter.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
|
@ -719,7 +744,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
.map(|res| (res.subtree, res.checkpoints))
|
.map(|res| (res.subtree, res.checkpoints))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Update the Sapling note commitment tree with all newly read note commitments
|
// Update the Orchard note commitment tree with all newly read note commitments
|
||||||
let mut orchard_subtrees = orchard_subtrees.into_iter();
|
let mut orchard_subtrees = orchard_subtrees.into_iter();
|
||||||
wdb.with_orchard_tree_mut::<_, _, Self::Error>(move |orchard_tree| {
|
wdb.with_orchard_tree_mut::<_, _, Self::Error>(move |orchard_tree| {
|
||||||
for (tree, checkpoints) in &mut orchard_subtrees {
|
for (tree, checkpoints) in &mut orchard_subtrees {
|
||||||
|
@ -935,6 +960,19 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(_bundle) = sent_tx.tx().orchard_bundle() {
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
for action in _bundle.actions() {
|
||||||
|
wallet::orchard::mark_orchard_note_spent(
|
||||||
|
wdb.conn.0,
|
||||||
|
tx_ref,
|
||||||
|
action.nullifier(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "orchard"))]
|
||||||
|
panic!("Sent a transaction with Orchard Actions without `orchard` enabled?");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
for utxo_outpoint in sent_tx.utxos_spent() {
|
for utxo_outpoint in sent_tx.utxos_spent() {
|
||||||
|
|
|
@ -129,6 +129,7 @@ use {
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod commitment_tree;
|
pub mod commitment_tree;
|
||||||
|
pub(crate) mod common;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
pub(crate) mod orchard;
|
pub(crate) mod orchard;
|
||||||
|
@ -723,6 +724,15 @@ pub(crate) trait ScanProgress {
|
||||||
fully_scanned_height: BlockHeight,
|
fully_scanned_height: BlockHeight,
|
||||||
chain_tip_height: BlockHeight,
|
chain_tip_height: BlockHeight,
|
||||||
) -> Result<Option<Ratio<u64>>, SqliteClientError>;
|
) -> Result<Option<Ratio<u64>>, SqliteClientError>;
|
||||||
|
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
fn orchard_scan_progress(
|
||||||
|
&self,
|
||||||
|
conn: &rusqlite::Connection,
|
||||||
|
birthday_height: BlockHeight,
|
||||||
|
fully_scanned_height: BlockHeight,
|
||||||
|
chain_tip_height: BlockHeight,
|
||||||
|
) -> Result<Option<Ratio<u64>>, SqliteClientError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -804,6 +814,83 @@ impl ScanProgress for SubtreeScanProgress {
|
||||||
.flatten())
|
.flatten())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
#[tracing::instrument(skip(conn))]
|
||||||
|
fn orchard_scan_progress(
|
||||||
|
&self,
|
||||||
|
conn: &rusqlite::Connection,
|
||||||
|
birthday_height: BlockHeight,
|
||||||
|
fully_scanned_height: BlockHeight,
|
||||||
|
chain_tip_height: BlockHeight,
|
||||||
|
) -> Result<Option<Ratio<u64>>, SqliteClientError> {
|
||||||
|
if fully_scanned_height == chain_tip_height {
|
||||||
|
// Compute the total blocks scanned since the wallet birthday
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT SUM(orchard_action_count)
|
||||||
|
FROM blocks
|
||||||
|
WHERE height >= :birthday_height",
|
||||||
|
named_params![":birthday_height": u32::from(birthday_height)],
|
||||||
|
|row| {
|
||||||
|
let scanned = row.get::<_, Option<u64>>(0)?;
|
||||||
|
Ok(scanned.map(|n| Ratio::new(n, n)))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(SqliteClientError::from)
|
||||||
|
} else {
|
||||||
|
let start_height = birthday_height;
|
||||||
|
// Compute the starting number of notes directly from the blocks table
|
||||||
|
let start_size = conn.query_row(
|
||||||
|
"SELECT MAX(orchard_commitment_tree_size)
|
||||||
|
FROM blocks
|
||||||
|
WHERE height <= :start_height",
|
||||||
|
named_params![":start_height": u32::from(start_height)],
|
||||||
|
|row| row.get::<_, Option<u64>>(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Compute the total blocks scanned so far above the starting height
|
||||||
|
let scanned_count = conn.query_row(
|
||||||
|
"SELECT SUM(orchard_action_count)
|
||||||
|
FROM blocks
|
||||||
|
WHERE height > :start_height",
|
||||||
|
named_params![":start_height": u32::from(start_height)],
|
||||||
|
|row| row.get::<_, Option<u64>>(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// We don't have complete information on how many actions will exist in the shard at
|
||||||
|
// the chain tip without having scanned the chain tip block, so we overestimate by
|
||||||
|
// computing the maximum possible number of notes directly from the shard indices.
|
||||||
|
//
|
||||||
|
// TODO: it would be nice to be able to reliably have the size of the commitment tree
|
||||||
|
// at the chain tip without having to have scanned that block.
|
||||||
|
Ok(conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT MIN(shard_index), MAX(shard_index)
|
||||||
|
FROM orchard_tree_shards
|
||||||
|
WHERE subtree_end_height > :start_height
|
||||||
|
OR subtree_end_height IS NULL",
|
||||||
|
named_params![":start_height": u32::from(start_height)],
|
||||||
|
|row| {
|
||||||
|
let min_tree_size = row
|
||||||
|
.get::<_, Option<u64>>(0)?
|
||||||
|
.map(|min| min << ORCHARD_SHARD_HEIGHT);
|
||||||
|
let max_idx = row.get::<_, Option<u64>>(1)?;
|
||||||
|
Ok(start_size
|
||||||
|
.or(min_tree_size)
|
||||||
|
.zip(max_idx)
|
||||||
|
.map(|(min_tree_size, max)| {
|
||||||
|
let max_tree_size = (max + 1) << ORCHARD_SHARD_HEIGHT;
|
||||||
|
Ratio::new(
|
||||||
|
scanned_count.unwrap_or(0),
|
||||||
|
max_tree_size - min_tree_size,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.optional()?
|
||||||
|
.flatten())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the spendable balance for the account at the specified height.
|
/// Returns the spendable balance for the account at the specified height.
|
||||||
|
@ -841,27 +928,27 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
||||||
chain_tip_height,
|
chain_tip_height,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// If the shard containing the summary height contains any unscanned ranges that start below or
|
#[cfg(feature = "orchard")]
|
||||||
// including that height, none of our balance is currently spendable.
|
let orchard_scan_progress = progress.orchard_scan_progress(
|
||||||
#[tracing::instrument(skip_all)]
|
tx,
|
||||||
fn is_any_spendable(
|
birthday_height,
|
||||||
conn: &rusqlite::Connection,
|
fully_scanned_height,
|
||||||
summary_height: BlockHeight,
|
chain_tip_height,
|
||||||
) -> Result<bool, SqliteClientError> {
|
)?;
|
||||||
conn.query_row(
|
#[cfg(not(feature = "orchard"))]
|
||||||
"SELECT NOT EXISTS(
|
let orchard_scan_progress: Option<Ratio<u64>> = None;
|
||||||
SELECT 1 FROM v_sapling_shard_unscanned_ranges
|
|
||||||
WHERE :summary_height
|
// Treat Sapling and Orchard outputs as having the same cost to scan.
|
||||||
BETWEEN subtree_start_height
|
let scan_progress = sapling_scan_progress
|
||||||
AND IFNULL(subtree_end_height, :summary_height)
|
.zip(orchard_scan_progress)
|
||||||
AND block_range_start <= :summary_height
|
.map(|(s, o)| {
|
||||||
)",
|
Ratio::new(
|
||||||
named_params![":summary_height": u32::from(summary_height)],
|
s.numerator() + o.numerator(),
|
||||||
|row| row.get::<_, bool>(0),
|
s.denominator() + o.denominator(),
|
||||||
)
|
)
|
||||||
.map_err(|e| e.into())
|
})
|
||||||
}
|
.or(sapling_scan_progress)
|
||||||
let any_spendable = is_any_spendable(tx, summary_height)?;
|
.or(orchard_scan_progress);
|
||||||
|
|
||||||
let mut stmt_accounts = tx.prepare_cached("SELECT id FROM accounts")?;
|
let mut stmt_accounts = tx.prepare_cached("SELECT id FROM accounts")?;
|
||||||
let mut account_balances = stmt_accounts
|
let mut account_balances = stmt_accounts
|
||||||
|
@ -871,12 +958,51 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
||||||
})
|
})
|
||||||
.collect::<Result<HashMap<AccountId, AccountBalance>, _>>()?;
|
.collect::<Result<HashMap<AccountId, AccountBalance>, _>>()?;
|
||||||
|
|
||||||
let sapling_trace = tracing::info_span!("stmt_select_notes").entered();
|
fn count_notes<F>(
|
||||||
let mut stmt_select_notes = tx.prepare_cached(
|
tx: &rusqlite::Transaction,
|
||||||
|
summary_height: BlockHeight,
|
||||||
|
account_balances: &mut HashMap<AccountId, AccountBalance>,
|
||||||
|
table_prefix: &'static str,
|
||||||
|
with_pool_balance: F,
|
||||||
|
) -> Result<(), SqliteClientError>
|
||||||
|
where
|
||||||
|
F: Fn(
|
||||||
|
&mut AccountBalance,
|
||||||
|
NonNegativeAmount,
|
||||||
|
NonNegativeAmount,
|
||||||
|
NonNegativeAmount,
|
||||||
|
) -> Result<(), SqliteClientError>,
|
||||||
|
{
|
||||||
|
// If the shard containing the summary height contains any unscanned ranges that start below or
|
||||||
|
// including that height, none of our balance is currently spendable.
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
fn is_any_spendable(
|
||||||
|
conn: &rusqlite::Connection,
|
||||||
|
summary_height: BlockHeight,
|
||||||
|
table_prefix: &'static str,
|
||||||
|
) -> Result<bool, SqliteClientError> {
|
||||||
|
conn.query_row(
|
||||||
|
&format!(
|
||||||
|
"SELECT NOT EXISTS(
|
||||||
|
SELECT 1 FROM v_{table_prefix}_shard_unscanned_ranges
|
||||||
|
WHERE :summary_height
|
||||||
|
BETWEEN subtree_start_height
|
||||||
|
AND IFNULL(subtree_end_height, :summary_height)
|
||||||
|
AND block_range_start <= :summary_height
|
||||||
|
)"
|
||||||
|
),
|
||||||
|
named_params![":summary_height": u32::from(summary_height)],
|
||||||
|
|row| row.get::<_, bool>(0),
|
||||||
|
)
|
||||||
|
.map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
let any_spendable = is_any_spendable(tx, summary_height, table_prefix)?;
|
||||||
|
let mut stmt_select_notes = tx.prepare_cached(&format!(
|
||||||
"SELECT n.account_id, n.value, n.is_change, scan_state.max_priority, t.block
|
"SELECT n.account_id, n.value, n.is_change, scan_state.max_priority, t.block
|
||||||
FROM sapling_received_notes n
|
FROM {table_prefix}_received_notes n
|
||||||
JOIN transactions t ON t.id_tx = n.tx
|
JOIN transactions t ON t.id_tx = n.tx
|
||||||
LEFT OUTER JOIN v_sapling_shards_scan_state scan_state
|
LEFT OUTER JOIN v_{table_prefix}_shards_scan_state scan_state
|
||||||
ON n.commitment_tree_position >= scan_state.start_position
|
ON n.commitment_tree_position >= scan_state.start_position
|
||||||
AND n.commitment_tree_position < scan_state.end_position_exclusive
|
AND n.commitment_tree_position < scan_state.end_position_exclusive
|
||||||
WHERE n.spent IS NULL
|
WHERE n.spent IS NULL
|
||||||
|
@ -885,7 +1011,7 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
||||||
OR t.block IS NOT NULL
|
OR t.block IS NOT NULL
|
||||||
OR t.expiry_height >= :summary_height
|
OR t.expiry_height >= :summary_height
|
||||||
)",
|
)",
|
||||||
)?;
|
))?;
|
||||||
|
|
||||||
let mut rows =
|
let mut rows =
|
||||||
stmt_select_notes.query(named_params![":summary_height": u32::from(summary_height)])?;
|
stmt_select_notes.query(named_params![":summary_height": u32::from(summary_height)])?;
|
||||||
|
@ -894,7 +1020,10 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
||||||
|
|
||||||
let value_raw = row.get::<_, i64>(1)?;
|
let value_raw = row.get::<_, i64>(1)?;
|
||||||
let value = NonNegativeAmount::from_nonnegative_i64(value_raw).map_err(|_| {
|
let value = NonNegativeAmount::from_nonnegative_i64(value_raw).map_err(|_| {
|
||||||
SqliteClientError::CorruptedData(format!("Negative received note value: {}", value_raw))
|
SqliteClientError::CorruptedData(format!(
|
||||||
|
"Negative received note value: {}",
|
||||||
|
value_raw
|
||||||
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let is_change = row.get::<_, bool>(2)?;
|
let is_change = row.get::<_, bool>(2)?;
|
||||||
|
@ -921,7 +1050,8 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
||||||
&& received_height.iter().any(|h| h <= &summary_height)
|
&& received_height.iter().any(|h| h <= &summary_height)
|
||||||
&& max_priority <= ScanPriority::Scanned;
|
&& max_priority <= ScanPriority::Scanned;
|
||||||
|
|
||||||
let is_pending_change = is_change && received_height.iter().all(|h| h > &summary_height);
|
let is_pending_change =
|
||||||
|
is_change && received_height.iter().all(|h| h > &summary_height);
|
||||||
|
|
||||||
let (spendable_value, change_pending_confirmation, value_pending_spendability) = {
|
let (spendable_value, change_pending_confirmation, value_pending_spendability) = {
|
||||||
let zero = NonNegativeAmount::ZERO;
|
let zero = NonNegativeAmount::ZERO;
|
||||||
|
@ -935,14 +1065,52 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(balances) = account_balances.get_mut(&account) {
|
if let Some(balances) = account_balances.get_mut(&account) {
|
||||||
|
with_pool_balance(
|
||||||
|
balances,
|
||||||
|
spendable_value,
|
||||||
|
change_pending_confirmation,
|
||||||
|
value_pending_spendability,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
{
|
||||||
|
let orchard_trace = tracing::info_span!("orchard_balances").entered();
|
||||||
|
count_notes(
|
||||||
|
tx,
|
||||||
|
summary_height,
|
||||||
|
&mut account_balances,
|
||||||
|
ORCHARD_TABLES_PREFIX,
|
||||||
|
|balances, spendable_value, change_pending_confirmation, value_pending_spendability| {
|
||||||
|
balances.with_orchard_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(())
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
drop(orchard_trace);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sapling_trace = tracing::info_span!("sapling_balances").entered();
|
||||||
|
count_notes(
|
||||||
|
tx,
|
||||||
|
summary_height,
|
||||||
|
&mut account_balances,
|
||||||
|
SAPLING_TABLES_PREFIX,
|
||||||
|
|balances, spendable_value, change_pending_confirmation, value_pending_spendability| {
|
||||||
balances.with_sapling_balance_mut::<_, SqliteClientError>(|bal| {
|
balances.with_sapling_balance_mut::<_, SqliteClientError>(|bal| {
|
||||||
bal.add_spendable_value(spendable_value)?;
|
bal.add_spendable_value(spendable_value)?;
|
||||||
bal.add_pending_change_value(change_pending_confirmation)?;
|
bal.add_pending_change_value(change_pending_confirmation)?;
|
||||||
bal.add_pending_spendable_value(value_pending_spendability)?;
|
bal.add_pending_spendable_value(value_pending_spendability)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})
|
||||||
}
|
},
|
||||||
}
|
)?;
|
||||||
drop(sapling_trace);
|
drop(sapling_trace);
|
||||||
|
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
|
@ -1025,7 +1193,7 @@ pub(crate) fn get_wallet_summary<P: consensus::Parameters>(
|
||||||
account_balances,
|
account_balances,
|
||||||
chain_tip_height,
|
chain_tip_height,
|
||||||
fully_scanned_height,
|
fully_scanned_height,
|
||||||
sapling_scan_progress,
|
scan_progress,
|
||||||
next_sapling_subtree_index,
|
next_sapling_subtree_index,
|
||||||
#[cfg(feature = "orchard")]
|
#[cfg(feature = "orchard")]
|
||||||
next_orchard_subtree_index,
|
next_orchard_subtree_index,
|
||||||
|
@ -1039,24 +1207,31 @@ pub(crate) fn get_received_memo(
|
||||||
conn: &rusqlite::Connection,
|
conn: &rusqlite::Connection,
|
||||||
note_id: NoteId,
|
note_id: NoteId,
|
||||||
) -> Result<Option<Memo>, SqliteClientError> {
|
) -> Result<Option<Memo>, SqliteClientError> {
|
||||||
let memo_bytes: Option<Vec<_>> = match note_id.protocol() {
|
let fetch_memo = |table_prefix: &'static str, output_col: &'static str| {
|
||||||
ShieldedProtocol::Sapling => conn
|
conn.query_row(
|
||||||
.query_row(
|
&format!(
|
||||||
"SELECT memo FROM sapling_received_notes
|
"SELECT memo FROM {table_prefix}_received_notes
|
||||||
JOIN transactions ON sapling_received_notes.tx = transactions.id_tx
|
JOIN transactions ON {table_prefix}_received_notes.tx = transactions.id_tx
|
||||||
WHERE transactions.txid = :txid
|
WHERE transactions.txid = :txid
|
||||||
AND sapling_received_notes.output_index = :output_index",
|
AND {table_prefix}_received_notes.{output_col} = :output_index"
|
||||||
|
),
|
||||||
named_params![
|
named_params![
|
||||||
":txid": note_id.txid().as_ref(),
|
":txid": note_id.txid().as_ref(),
|
||||||
":output_index": note_id.output_index()
|
":output_index": note_id.output_index()
|
||||||
],
|
],
|
||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)
|
)
|
||||||
.optional()?
|
.optional()
|
||||||
.flatten(),
|
};
|
||||||
_ => {
|
|
||||||
|
let memo_bytes: Option<Vec<_>> = match note_id.protocol() {
|
||||||
|
ShieldedProtocol::Sapling => fetch_memo(SAPLING_TABLES_PREFIX, "output_index")?.flatten(),
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
ShieldedProtocol::Orchard => fetch_memo(ORCHARD_TABLES_PREFIX, "action_index")?.flatten(),
|
||||||
|
#[cfg(not(feature = "orchard"))]
|
||||||
|
ShieldedProtocol::Orchard => {
|
||||||
return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded(
|
return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded(
|
||||||
note_id.protocol(),
|
ShieldedProtocol::Orchard,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1321,7 +1496,24 @@ pub(crate) fn get_target_and_anchor_heights(
|
||||||
min_confirmations,
|
min_confirmations,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(sapling_anchor_height.map(|h| (chain_tip_height + 1, h)))
|
#[cfg(feature = "orchard")]
|
||||||
|
let orchard_anchor_height = get_max_checkpointed_height(
|
||||||
|
conn,
|
||||||
|
ORCHARD_TABLES_PREFIX,
|
||||||
|
chain_tip_height,
|
||||||
|
min_confirmations,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "orchard"))]
|
||||||
|
let orchard_anchor_height: Option<BlockHeight> = None;
|
||||||
|
|
||||||
|
let anchor_height = sapling_anchor_height
|
||||||
|
.zip(orchard_anchor_height)
|
||||||
|
.map(|(s, o)| std::cmp::min(s, o))
|
||||||
|
.or(sapling_anchor_height)
|
||||||
|
.or(orchard_anchor_height);
|
||||||
|
|
||||||
|
Ok(anchor_height.map(|h| (chain_tip_height + 1, h)))
|
||||||
}
|
}
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
|
@ -1541,7 +1733,7 @@ pub(crate) fn get_max_height_hash(
|
||||||
pub(crate) fn get_min_unspent_height(
|
pub(crate) fn get_min_unspent_height(
|
||||||
conn: &rusqlite::Connection,
|
conn: &rusqlite::Connection,
|
||||||
) -> Result<Option<BlockHeight>, SqliteClientError> {
|
) -> Result<Option<BlockHeight>, SqliteClientError> {
|
||||||
conn.query_row(
|
let min_sapling: Option<BlockHeight> = conn.query_row(
|
||||||
"SELECT MIN(tx.block)
|
"SELECT MIN(tx.block)
|
||||||
FROM sapling_received_notes n
|
FROM sapling_received_notes n
|
||||||
JOIN transactions tx ON tx.id_tx = n.tx
|
JOIN transactions tx ON tx.id_tx = n.tx
|
||||||
|
@ -1551,8 +1743,27 @@ pub(crate) fn get_min_unspent_height(
|
||||||
row.get(0)
|
row.get(0)
|
||||||
.map(|maybe_height: Option<u32>| maybe_height.map(|height| height.into()))
|
.map(|maybe_height: Option<u32>| maybe_height.map(|height| height.into()))
|
||||||
},
|
},
|
||||||
)
|
)?;
|
||||||
.map_err(SqliteClientError::from)
|
#[cfg(feature = "orchard")]
|
||||||
|
let min_orchard: Option<BlockHeight> = conn.query_row(
|
||||||
|
"SELECT MIN(tx.block)
|
||||||
|
FROM orchard_received_notes n
|
||||||
|
JOIN transactions tx ON tx.id_tx = n.tx
|
||||||
|
WHERE n.spent IS NULL",
|
||||||
|
[],
|
||||||
|
|row| {
|
||||||
|
row.get(0)
|
||||||
|
.map(|maybe_height: Option<u32>| maybe_height.map(|height| height.into()))
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
#[cfg(not(feature = "orchard"))]
|
||||||
|
let min_orchard = None;
|
||||||
|
|
||||||
|
Ok(min_sapling
|
||||||
|
.zip(min_orchard)
|
||||||
|
.map(|(s, o)| s.min(o))
|
||||||
|
.or(min_sapling)
|
||||||
|
.or(min_orchard))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Truncates the database to the given height.
|
/// Truncates the database to the given height.
|
||||||
|
@ -1594,6 +1805,10 @@ pub(crate) fn truncate_to_height<P: consensus::Parameters>(
|
||||||
wdb.with_sapling_tree_mut(|tree| {
|
wdb.with_sapling_tree_mut(|tree| {
|
||||||
tree.truncate_removing_checkpoint(&block_height).map(|_| ())
|
tree.truncate_removing_checkpoint(&block_height).map(|_| ())
|
||||||
})?;
|
})?;
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
wdb.with_orchard_tree_mut(|tree| {
|
||||||
|
tree.truncate_removing_checkpoint(&block_height).map(|_| ())
|
||||||
|
})?;
|
||||||
|
|
||||||
// Rewind received notes
|
// Rewind received notes
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
@ -1607,6 +1822,18 @@ pub(crate) fn truncate_to_height<P: consensus::Parameters>(
|
||||||
);",
|
);",
|
||||||
[u32::from(block_height)],
|
[u32::from(block_height)],
|
||||||
)?;
|
)?;
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM orchard_received_notes
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT rn.id
|
||||||
|
FROM orchard_received_notes rn
|
||||||
|
LEFT OUTER JOIN transactions tx
|
||||||
|
ON tx.id_tx = rn.tx
|
||||||
|
WHERE tx.block IS NOT NULL AND tx.block > ?
|
||||||
|
);",
|
||||||
|
[u32::from(block_height)],
|
||||||
|
)?;
|
||||||
|
|
||||||
// Do not delete sent notes; this can contain data that is not recoverable
|
// Do not delete sent notes; this can contain data that is not recoverable
|
||||||
// from the chain. Wallets must continue to operate correctly in the
|
// from the chain. Wallets must continue to operate correctly in the
|
||||||
|
|
|
@ -0,0 +1,216 @@
|
||||||
|
//! Functions common to Sapling and Orchard support in the wallet.
|
||||||
|
|
||||||
|
use rusqlite::{named_params, types::Value, Connection, Row};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use zcash_client_backend::{
|
||||||
|
wallet::{Note, ReceivedNote},
|
||||||
|
ShieldedProtocol,
|
||||||
|
};
|
||||||
|
use zcash_primitives::transaction::{components::amount::NonNegativeAmount, TxId};
|
||||||
|
use zcash_protocol::consensus::{self, BlockHeight};
|
||||||
|
|
||||||
|
use super::wallet_birthday;
|
||||||
|
use crate::{error::SqliteClientError, AccountId, ReceivedNoteId, SAPLING_TABLES_PREFIX};
|
||||||
|
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
use crate::ORCHARD_TABLES_PREFIX;
|
||||||
|
|
||||||
|
fn per_protocol_names(protocol: ShieldedProtocol) -> (&'static str, &'static str, &'static str) {
|
||||||
|
match protocol {
|
||||||
|
ShieldedProtocol::Sapling => (SAPLING_TABLES_PREFIX, "output_index", "rcm"),
|
||||||
|
#[cfg(feature = "orchard")]
|
||||||
|
ShieldedProtocol::Orchard => (ORCHARD_TABLES_PREFIX, "action_index", "rho, rseed"),
|
||||||
|
#[cfg(not(feature = "orchard"))]
|
||||||
|
ShieldedProtocol::Orchard => {
|
||||||
|
unreachable!("Should never be called unless the `orchard` feature is enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unscanned_tip_exists(
|
||||||
|
conn: &Connection,
|
||||||
|
anchor_height: BlockHeight,
|
||||||
|
table_prefix: &'static str,
|
||||||
|
) -> Result<bool, rusqlite::Error> {
|
||||||
|
// v_sapling_shard_unscanned_ranges only returns ranges ending on or after wallet birthday, so
|
||||||
|
// we don't need to refer to the birthday in this query.
|
||||||
|
conn.query_row(
|
||||||
|
&format!(
|
||||||
|
"SELECT EXISTS (
|
||||||
|
SELECT 1 FROM v_{table_prefix}_shard_unscanned_ranges range
|
||||||
|
WHERE range.block_range_start <= :anchor_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),],
|
||||||
|
|row| row.get::<_, bool>(0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The `clippy::let_and_return` lint is explicitly allowed here because a bug in Clippy
|
||||||
|
// (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_note<P: consensus::Parameters, F>(
|
||||||
|
conn: &Connection,
|
||||||
|
params: &P,
|
||||||
|
txid: &TxId,
|
||||||
|
index: u32,
|
||||||
|
protocol: ShieldedProtocol,
|
||||||
|
to_spendable_note: F,
|
||||||
|
) -> Result<Option<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError>
|
||||||
|
where
|
||||||
|
F: Fn(&P, &Row) -> Result<Option<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError>,
|
||||||
|
{
|
||||||
|
let (table_prefix, index_col, note_reconstruction_cols) = per_protocol_names(protocol);
|
||||||
|
let result = conn.query_row_and_then(
|
||||||
|
&format!(
|
||||||
|
"SELECT {table_prefix}_received_notes.id, txid, {index_col},
|
||||||
|
diversifier, value, {note_reconstruction_cols}, commitment_tree_position,
|
||||||
|
accounts.ufvk, recipient_key_scope
|
||||||
|
FROM {table_prefix}_received_notes
|
||||||
|
INNER JOIN accounts ON accounts.id = {table_prefix}_received_notes.account_id
|
||||||
|
INNER JOIN transactions ON transactions.id_tx = {table_prefix}_received_notes.tx
|
||||||
|
WHERE txid = :txid
|
||||||
|
AND {index_col} = :output_index
|
||||||
|
AND accounts.ufvk IS NOT NULL
|
||||||
|
AND recipient_key_scope IS NOT NULL
|
||||||
|
AND nf IS NOT NULL
|
||||||
|
AND commitment_tree_position IS NOT NULL
|
||||||
|
AND spent IS NULL"
|
||||||
|
),
|
||||||
|
named_params![
|
||||||
|
":txid": txid.as_ref(),
|
||||||
|
":output_index": index,
|
||||||
|
],
|
||||||
|
|row| to_spendable_note(params, row),
|
||||||
|
);
|
||||||
|
|
||||||
|
// `OptionalExtension` doesn't work here because the error type of `Result` is already
|
||||||
|
// `SqliteClientError`
|
||||||
|
match result {
|
||||||
|
Ok(r) => Ok(r),
|
||||||
|
Err(SqliteClientError::DbError(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) fn select_spendable_notes<P: consensus::Parameters, F>(
|
||||||
|
conn: &Connection,
|
||||||
|
params: &P,
|
||||||
|
account: AccountId,
|
||||||
|
target_value: NonNegativeAmount,
|
||||||
|
anchor_height: BlockHeight,
|
||||||
|
exclude: &[ReceivedNoteId],
|
||||||
|
protocol: ShieldedProtocol,
|
||||||
|
to_spendable_note: F,
|
||||||
|
) -> Result<Vec<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError>
|
||||||
|
where
|
||||||
|
F: Fn(&P, &Row) -> Result<Option<ReceivedNote<ReceivedNoteId, Note>>, 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![]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (table_prefix, index_col, note_reconstruction_cols) = per_protocol_names(protocol);
|
||||||
|
if unscanned_tip_exists(conn, anchor_height, table_prefix)? {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The goal of this SQL statement is to select the oldest notes until the required
|
||||||
|
// value has been reached.
|
||||||
|
// 1) Use a window function to create a view of all notes, ordered from oldest to
|
||||||
|
// newest, with an additional column containing a running sum:
|
||||||
|
// - Unspent notes accumulate the values of all unspent notes in that note's
|
||||||
|
// account, up to itself.
|
||||||
|
// - Spent notes accumulate the values of all notes in the transaction they were
|
||||||
|
// spent in, up to itself.
|
||||||
|
//
|
||||||
|
// 2) Select all unspent notes in the desired account, along with their running sum.
|
||||||
|
//
|
||||||
|
// 3) Select all notes for which the running sum was less than the required value, as
|
||||||
|
// well as a single note for which the sum was greater than or equal to the
|
||||||
|
// required value, bringing the sum of all selected notes across the threshold.
|
||||||
|
let mut stmt_select_notes = conn.prepare_cached(
|
||||||
|
&format!(
|
||||||
|
"WITH eligible AS (
|
||||||
|
SELECT
|
||||||
|
{table_prefix}_received_notes.id AS id, txid, {index_col},
|
||||||
|
diversifier, value, {note_reconstruction_cols}, commitment_tree_position,
|
||||||
|
SUM(value) OVER (
|
||||||
|
PARTITION BY {table_prefix}_received_notes.account_id, spent
|
||||||
|
ORDER BY {table_prefix}_received_notes.id
|
||||||
|
) AS so_far,
|
||||||
|
accounts.ufvk as ufvk, recipient_key_scope
|
||||||
|
FROM {table_prefix}_received_notes
|
||||||
|
INNER JOIN accounts
|
||||||
|
ON accounts.id = {table_prefix}_received_notes.account_id
|
||||||
|
INNER JOIN transactions
|
||||||
|
ON transactions.id_tx = {table_prefix}_received_notes.tx
|
||||||
|
WHERE {table_prefix}_received_notes.account_id = :account
|
||||||
|
AND accounts.ufvk IS NOT NULL
|
||||||
|
AND recipient_key_scope IS NOT NULL
|
||||||
|
AND nf IS NOT NULL
|
||||||
|
AND commitment_tree_position IS NOT NULL
|
||||||
|
AND spent IS NULL
|
||||||
|
AND transactions.block <= :anchor_height
|
||||||
|
AND {table_prefix}_received_notes.id NOT IN rarray(:exclude)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM v_{table_prefix}_shard_unscanned_ranges unscanned
|
||||||
|
-- select all the unscanned ranges involving the shard containing this note
|
||||||
|
WHERE {table_prefix}_received_notes.commitment_tree_position >= unscanned.start_position
|
||||||
|
AND {table_prefix}_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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SELECT id, txid, {index_col},
|
||||||
|
diversifier, value, {note_reconstruction_cols}, commitment_tree_position,
|
||||||
|
ufvk, recipient_key_scope
|
||||||
|
FROM eligible WHERE so_far < :target_value
|
||||||
|
UNION
|
||||||
|
SELECT id, txid, {index_col},
|
||||||
|
diversifier, value, {note_reconstruction_cols}, commitment_tree_position,
|
||||||
|
ufvk, recipient_key_scope
|
||||||
|
FROM (SELECT * from eligible WHERE so_far >= :target_value LIMIT 1)",
|
||||||
|
)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let excluded: Vec<Value> = exclude
|
||||||
|
.iter()
|
||||||
|
.filter_map(|ReceivedNoteId(p, n)| {
|
||||||
|
if *p == protocol {
|
||||||
|
Some(Value::from(*n))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let excluded_ptr = Rc::new(excluded);
|
||||||
|
|
||||||
|
let notes = stmt_select_notes.query_and_then(
|
||||||
|
named_params![
|
||||||
|
":account": account.0,
|
||||||
|
":anchor_height": &u32::from(anchor_height),
|
||||||
|
":target_value": &u64::from(target_value),
|
||||||
|
":exclude": &excluded_ptr,
|
||||||
|
":wallet_birthday": u32::from(birthday_height)
|
||||||
|
],
|
||||||
|
|r| to_spendable_note(params, r),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
notes
|
||||||
|
.filter_map(|r| r.transpose())
|
||||||
|
.collect::<Result<_, _>>()
|
||||||
|
}
|
|
@ -1,24 +1,36 @@
|
||||||
use incrementalmerkletree::Position;
|
use incrementalmerkletree::Position;
|
||||||
use rusqlite::{named_params, params, Connection};
|
use orchard::{
|
||||||
|
keys::Diversifier,
|
||||||
|
note::{Note, Nullifier, RandomSeed},
|
||||||
|
};
|
||||||
|
use rusqlite::{named_params, params, Connection, Row};
|
||||||
|
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
data_api::NullifierQuery, wallet::WalletOrchardOutput, DecryptedOutput, TransferType,
|
data_api::NullifierQuery,
|
||||||
|
wallet::{ReceivedNote, WalletOrchardOutput},
|
||||||
|
DecryptedOutput, ShieldedProtocol, TransferType,
|
||||||
|
};
|
||||||
|
use zcash_keys::keys::UnifiedFullViewingKey;
|
||||||
|
use zcash_primitives::transaction::TxId;
|
||||||
|
use zcash_protocol::{
|
||||||
|
consensus::{self, BlockHeight},
|
||||||
|
memo::MemoBytes,
|
||||||
|
value::Zatoshis,
|
||||||
};
|
};
|
||||||
use zcash_protocol::memo::MemoBytes;
|
|
||||||
use zip32::Scope;
|
use zip32::Scope;
|
||||||
|
|
||||||
use crate::{error::SqliteClientError, AccountId};
|
use crate::{error::SqliteClientError, AccountId, ReceivedNoteId};
|
||||||
|
|
||||||
use super::{memo_repr, scope_code};
|
use super::{memo_repr, parse_scope, scope_code};
|
||||||
|
|
||||||
/// This trait provides a generalization over shielded output representations.
|
/// This trait provides a generalization over shielded output representations.
|
||||||
pub(crate) trait ReceivedOrchardOutput {
|
pub(crate) trait ReceivedOrchardOutput {
|
||||||
fn index(&self) -> usize;
|
fn index(&self) -> usize;
|
||||||
fn account_id(&self) -> AccountId;
|
fn account_id(&self) -> AccountId;
|
||||||
fn note(&self) -> &orchard::note::Note;
|
fn note(&self) -> &Note;
|
||||||
fn memo(&self) -> Option<&MemoBytes>;
|
fn memo(&self) -> Option<&MemoBytes>;
|
||||||
fn is_change(&self) -> bool;
|
fn is_change(&self) -> bool;
|
||||||
fn nullifier(&self) -> Option<&orchard::note::Nullifier>;
|
fn nullifier(&self) -> Option<&Nullifier>;
|
||||||
fn note_commitment_tree_position(&self) -> Option<Position>;
|
fn note_commitment_tree_position(&self) -> Option<Position>;
|
||||||
fn recipient_key_scope(&self) -> Option<Scope>;
|
fn recipient_key_scope(&self) -> Option<Scope>;
|
||||||
}
|
}
|
||||||
|
@ -30,7 +42,7 @@ impl ReceivedOrchardOutput for WalletOrchardOutput<AccountId> {
|
||||||
fn account_id(&self) -> AccountId {
|
fn account_id(&self) -> AccountId {
|
||||||
*WalletOrchardOutput::account_id(self)
|
*WalletOrchardOutput::account_id(self)
|
||||||
}
|
}
|
||||||
fn note(&self) -> &orchard::note::Note {
|
fn note(&self) -> &Note {
|
||||||
WalletOrchardOutput::note(self)
|
WalletOrchardOutput::note(self)
|
||||||
}
|
}
|
||||||
fn memo(&self) -> Option<&MemoBytes> {
|
fn memo(&self) -> Option<&MemoBytes> {
|
||||||
|
@ -39,7 +51,7 @@ impl ReceivedOrchardOutput for WalletOrchardOutput<AccountId> {
|
||||||
fn is_change(&self) -> bool {
|
fn is_change(&self) -> bool {
|
||||||
WalletOrchardOutput::is_change(self)
|
WalletOrchardOutput::is_change(self)
|
||||||
}
|
}
|
||||||
fn nullifier(&self) -> Option<&orchard::note::Nullifier> {
|
fn nullifier(&self) -> Option<&Nullifier> {
|
||||||
self.nf()
|
self.nf()
|
||||||
}
|
}
|
||||||
fn note_commitment_tree_position(&self) -> Option<Position> {
|
fn note_commitment_tree_position(&self) -> Option<Position> {
|
||||||
|
@ -50,7 +62,7 @@ impl ReceivedOrchardOutput for WalletOrchardOutput<AccountId> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReceivedOrchardOutput for DecryptedOutput<orchard::note::Note, AccountId> {
|
impl ReceivedOrchardOutput for DecryptedOutput<Note, AccountId> {
|
||||||
fn index(&self) -> usize {
|
fn index(&self) -> usize {
|
||||||
self.index()
|
self.index()
|
||||||
}
|
}
|
||||||
|
@ -66,7 +78,7 @@ impl ReceivedOrchardOutput for DecryptedOutput<orchard::note::Note, AccountId> {
|
||||||
fn is_change(&self) -> bool {
|
fn is_change(&self) -> bool {
|
||||||
self.transfer_type() == TransferType::WalletInternal
|
self.transfer_type() == TransferType::WalletInternal
|
||||||
}
|
}
|
||||||
fn nullifier(&self) -> Option<&orchard::note::Nullifier> {
|
fn nullifier(&self) -> Option<&Nullifier> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
fn note_commitment_tree_position(&self) -> Option<Position> {
|
fn note_commitment_tree_position(&self) -> Option<Position> {
|
||||||
|
@ -81,6 +93,136 @@ impl ReceivedOrchardOutput for DecryptedOutput<orchard::note::Note, AccountId> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_spendable_note<P: consensus::Parameters>(
|
||||||
|
params: &P,
|
||||||
|
row: &Row,
|
||||||
|
) -> Result<
|
||||||
|
Option<ReceivedNote<ReceivedNoteId, zcash_client_backend::wallet::Note>>,
|
||||||
|
SqliteClientError,
|
||||||
|
> {
|
||||||
|
let note_id = ReceivedNoteId(ShieldedProtocol::Orchard, row.get(0)?);
|
||||||
|
let txid = row.get::<_, [u8; 32]>(1).map(TxId::from_bytes)?;
|
||||||
|
let action_index = row.get(2)?;
|
||||||
|
let diversifier = {
|
||||||
|
let d: Vec<_> = row.get(3)?;
|
||||||
|
if d.len() != 11 {
|
||||||
|
return Err(SqliteClientError::CorruptedData(
|
||||||
|
"Invalid diversifier length".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut tmp = [0; 11];
|
||||||
|
tmp.copy_from_slice(&d);
|
||||||
|
Diversifier::from_bytes(tmp)
|
||||||
|
};
|
||||||
|
|
||||||
|
let note_value: u64 = row.get::<_, i64>(4)?.try_into().map_err(|_e| {
|
||||||
|
SqliteClientError::CorruptedData("Note values must be nonnegative".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let rho = {
|
||||||
|
let rho_bytes: [u8; 32] = row.get(5)?;
|
||||||
|
Option::from(Nullifier::from_bytes(&rho_bytes))
|
||||||
|
.ok_or_else(|| SqliteClientError::CorruptedData("Invalid rho.".to_string()))
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let rseed = {
|
||||||
|
let rseed_bytes: [u8; 32] = row.get(6)?;
|
||||||
|
Option::from(RandomSeed::from_bytes(rseed_bytes, &rho)).ok_or_else(|| {
|
||||||
|
SqliteClientError::CorruptedData("Invalid Orchard random seed.".to_string())
|
||||||
|
})
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let note_commitment_tree_position =
|
||||||
|
Position::from(u64::try_from(row.get::<_, i64>(7)?).map_err(|_| {
|
||||||
|
SqliteClientError::CorruptedData("Note commitment tree position invalid.".to_string())
|
||||||
|
})?);
|
||||||
|
|
||||||
|
let ufvk_str: Option<String> = row.get(8)?;
|
||||||
|
let scope_code: Option<i64> = row.get(9)?;
|
||||||
|
|
||||||
|
// If we don't have information about the recipient key scope or the ufvk we can't determine
|
||||||
|
// which spending key to use. This may be because the received note was associated with an
|
||||||
|
// imported viewing key, so we treat such notes as not spendable. Although this method is
|
||||||
|
// presently only called using the results of queries where both the ufvk and
|
||||||
|
// recipient_key_scope columns are checked to be non-null, this is method is written
|
||||||
|
// defensively to account for the fact that both of these are nullable columns in case it
|
||||||
|
// is used elsewhere in the future.
|
||||||
|
ufvk_str
|
||||||
|
.zip(scope_code)
|
||||||
|
.map(|(ufvk_str, scope_code)| {
|
||||||
|
let ufvk = UnifiedFullViewingKey::decode(params, &ufvk_str)
|
||||||
|
.map_err(SqliteClientError::CorruptedData)?;
|
||||||
|
|
||||||
|
let spending_key_scope = parse_scope(scope_code).ok_or_else(|| {
|
||||||
|
SqliteClientError::CorruptedData(format!("Invalid key scope code {}", scope_code))
|
||||||
|
})?;
|
||||||
|
let recipient = ufvk
|
||||||
|
.orchard()
|
||||||
|
.map(|fvk| fvk.to_ivk(spending_key_scope).address(diversifier))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
SqliteClientError::CorruptedData("Diversifier invalid.".to_owned())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let note = Option::from(Note::from_parts(
|
||||||
|
recipient,
|
||||||
|
orchard::value::NoteValue::from_raw(note_value),
|
||||||
|
rho,
|
||||||
|
rseed,
|
||||||
|
))
|
||||||
|
.ok_or_else(|| SqliteClientError::CorruptedData("Invalid Orchard note.".to_string()))?;
|
||||||
|
|
||||||
|
Ok(ReceivedNote::from_parts(
|
||||||
|
note_id,
|
||||||
|
txid,
|
||||||
|
action_index,
|
||||||
|
zcash_client_backend::wallet::Note::Orchard(note),
|
||||||
|
spending_key_scope,
|
||||||
|
note_commitment_tree_position,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_spendable_orchard_note<P: consensus::Parameters>(
|
||||||
|
conn: &Connection,
|
||||||
|
params: &P,
|
||||||
|
txid: &TxId,
|
||||||
|
index: u32,
|
||||||
|
) -> Result<
|
||||||
|
Option<ReceivedNote<ReceivedNoteId, zcash_client_backend::wallet::Note>>,
|
||||||
|
SqliteClientError,
|
||||||
|
> {
|
||||||
|
super::common::get_spendable_note(
|
||||||
|
conn,
|
||||||
|
params,
|
||||||
|
txid,
|
||||||
|
index,
|
||||||
|
ShieldedProtocol::Orchard,
|
||||||
|
to_spendable_note,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_spendable_orchard_notes<P: consensus::Parameters>(
|
||||||
|
conn: &Connection,
|
||||||
|
params: &P,
|
||||||
|
account: AccountId,
|
||||||
|
target_value: Zatoshis,
|
||||||
|
anchor_height: BlockHeight,
|
||||||
|
exclude: &[ReceivedNoteId],
|
||||||
|
) -> Result<Vec<ReceivedNote<ReceivedNoteId, zcash_client_backend::wallet::Note>>, SqliteClientError>
|
||||||
|
{
|
||||||
|
super::common::select_spendable_notes(
|
||||||
|
conn,
|
||||||
|
params,
|
||||||
|
account,
|
||||||
|
target_value,
|
||||||
|
anchor_height,
|
||||||
|
exclude,
|
||||||
|
ShieldedProtocol::Orchard,
|
||||||
|
to_spendable_note,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Records the specified shielded output as having been received.
|
/// Records the specified shielded output as having been received.
|
||||||
///
|
///
|
||||||
/// This implementation relies on the facts that:
|
/// This implementation relies on the facts that:
|
||||||
|
@ -94,27 +236,23 @@ pub(crate) fn put_received_note<T: ReceivedOrchardOutput>(
|
||||||
) -> Result<(), SqliteClientError> {
|
) -> Result<(), SqliteClientError> {
|
||||||
let mut stmt_upsert_received_note = conn.prepare_cached(
|
let mut stmt_upsert_received_note = conn.prepare_cached(
|
||||||
"INSERT INTO orchard_received_notes
|
"INSERT INTO orchard_received_notes
|
||||||
(tx, action_index, account_id, diversifier, value, rseed, memo, nf,
|
(
|
||||||
|
tx, action_index, account_id,
|
||||||
|
diversifier, value, rho, rseed, memo, nf,
|
||||||
is_change, spent, commitment_tree_position,
|
is_change, spent, commitment_tree_position,
|
||||||
recipient_key_scope)
|
recipient_key_scope
|
||||||
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
:tx,
|
:tx, :action_index, :account_id,
|
||||||
:action_index,
|
:diversifier, :value, :rho, :rseed, :memo, :nf,
|
||||||
:account_id,
|
:is_change, :spent, :commitment_tree_position,
|
||||||
:diversifier,
|
|
||||||
:value,
|
|
||||||
:rseed,
|
|
||||||
:memo,
|
|
||||||
:nf,
|
|
||||||
:is_change,
|
|
||||||
:spent,
|
|
||||||
:commitment_tree_position,
|
|
||||||
:recipient_key_scope
|
:recipient_key_scope
|
||||||
)
|
)
|
||||||
ON CONFLICT (tx, action_index) DO UPDATE
|
ON CONFLICT (tx, action_index) DO UPDATE
|
||||||
SET account_id = :account_id,
|
SET account_id = :account_id,
|
||||||
diversifier = :diversifier,
|
diversifier = :diversifier,
|
||||||
value = :value,
|
value = :value,
|
||||||
|
rho = :rho,
|
||||||
rseed = :rseed,
|
rseed = :rseed,
|
||||||
nf = IFNULL(:nf, nf),
|
nf = IFNULL(:nf, nf),
|
||||||
memo = IFNULL(:memo, memo),
|
memo = IFNULL(:memo, memo),
|
||||||
|
@ -130,10 +268,11 @@ pub(crate) fn put_received_note<T: ReceivedOrchardOutput>(
|
||||||
|
|
||||||
let sql_args = named_params![
|
let sql_args = named_params![
|
||||||
":tx": &tx_ref,
|
":tx": &tx_ref,
|
||||||
":output_index": i64::try_from(output.index()).expect("output indices are representable as i64"),
|
":action_index": i64::try_from(output.index()).expect("output indices are representable as i64"),
|
||||||
":account_id": output.account_id().0,
|
":account_id": output.account_id().0,
|
||||||
":diversifier": diversifier.as_array(),
|
":diversifier": diversifier.as_array(),
|
||||||
":value": output.note().value().inner(),
|
":value": output.note().value().inner(),
|
||||||
|
":rho": output.note().rho().to_bytes(),
|
||||||
":rseed": &rseed.as_bytes(),
|
":rseed": &rseed.as_bytes(),
|
||||||
":nf": output.nullifier().map(|nf| nf.to_bytes()),
|
":nf": output.nullifier().map(|nf| nf.to_bytes()),
|
||||||
":memo": memo_repr(output.memo()),
|
":memo": memo_repr(output.memo()),
|
||||||
|
@ -159,11 +298,11 @@ pub(crate) fn put_received_note<T: ReceivedOrchardOutput>(
|
||||||
pub(crate) fn get_orchard_nullifiers(
|
pub(crate) fn get_orchard_nullifiers(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
query: NullifierQuery,
|
query: NullifierQuery,
|
||||||
) -> Result<Vec<(AccountId, orchard::note::Nullifier)>, SqliteClientError> {
|
) -> Result<Vec<(AccountId, Nullifier)>, SqliteClientError> {
|
||||||
// Get the nullifiers for the notes we are tracking
|
// Get the nullifiers for the notes we are tracking
|
||||||
let mut stmt_fetch_nullifiers = match query {
|
let mut stmt_fetch_nullifiers = match query {
|
||||||
NullifierQuery::Unspent => conn.prepare(
|
NullifierQuery::Unspent => conn.prepare(
|
||||||
"SELECT rn.id, rn.account_id, rn.nf
|
"SELECT rn.account_id, rn.nf
|
||||||
FROM orchard_received_notes rn
|
FROM orchard_received_notes rn
|
||||||
LEFT OUTER JOIN transactions tx
|
LEFT OUTER JOIN transactions tx
|
||||||
ON tx.id_tx = rn.spent
|
ON tx.id_tx = rn.spent
|
||||||
|
@ -171,19 +310,16 @@ pub(crate) fn get_orchard_nullifiers(
|
||||||
AND nf IS NOT NULL",
|
AND nf IS NOT NULL",
|
||||||
)?,
|
)?,
|
||||||
NullifierQuery::All => conn.prepare(
|
NullifierQuery::All => conn.prepare(
|
||||||
"SELECT rn.id, rn.account_id, rn.nf
|
"SELECT rn.account_id, rn.nf
|
||||||
FROM orchard_received_notes rn
|
FROM orchard_received_notes rn
|
||||||
WHERE nf IS NOT NULL",
|
WHERE nf IS NOT NULL",
|
||||||
)?,
|
)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let nullifiers = stmt_fetch_nullifiers.query_and_then([], |row| {
|
let nullifiers = stmt_fetch_nullifiers.query_and_then([], |row| {
|
||||||
let account = AccountId(row.get(1)?);
|
let account = AccountId(row.get(0)?);
|
||||||
let nf_bytes: [u8; 32] = row.get(2)?;
|
let nf_bytes: [u8; 32] = row.get(1)?;
|
||||||
Ok::<_, rusqlite::Error>((
|
Ok::<_, rusqlite::Error>((account, Nullifier::from_bytes(&nf_bytes).unwrap()))
|
||||||
account,
|
|
||||||
orchard::note::Nullifier::from_bytes(&nf_bytes).unwrap(),
|
|
||||||
))
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let res: Vec<_> = nullifiers.collect::<Result<_, _>>()?;
|
let res: Vec<_> = nullifiers.collect::<Result<_, _>>()?;
|
||||||
|
@ -198,7 +334,7 @@ pub(crate) fn get_orchard_nullifiers(
|
||||||
pub(crate) fn mark_orchard_note_spent(
|
pub(crate) fn mark_orchard_note_spent(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
tx_ref: i64,
|
tx_ref: i64,
|
||||||
nf: &orchard::note::Nullifier,
|
nf: &Nullifier,
|
||||||
) -> Result<bool, SqliteClientError> {
|
) -> Result<bool, SqliteClientError> {
|
||||||
let mut stmt_mark_orchard_note_spent =
|
let mut stmt_mark_orchard_note_spent =
|
||||||
conn.prepare_cached("UPDATE orchard_received_notes SET spent = ? WHERE nf = ?")?;
|
conn.prepare_cached("UPDATE orchard_received_notes SET spent = ? WHERE nf = ?")?;
|
||||||
|
@ -233,6 +369,7 @@ pub(crate) mod tests {
|
||||||
use zcash_primitives::transaction::Transaction;
|
use zcash_primitives::transaction::Transaction;
|
||||||
use zcash_protocol::{consensus::BlockHeight, memo::MemoBytes, ShieldedProtocol};
|
use zcash_protocol::{consensus::BlockHeight, memo::MemoBytes, ShieldedProtocol};
|
||||||
|
|
||||||
|
use super::select_spendable_orchard_notes;
|
||||||
use crate::{
|
use crate::{
|
||||||
error::SqliteClientError,
|
error::SqliteClientError,
|
||||||
testing::{
|
testing::{
|
||||||
|
@ -321,7 +458,14 @@ pub(crate) mod tests {
|
||||||
anchor_height: BlockHeight,
|
anchor_height: BlockHeight,
|
||||||
exclude: &[crate::ReceivedNoteId],
|
exclude: &[crate::ReceivedNoteId],
|
||||||
) -> Result<Vec<ReceivedNote<crate::ReceivedNoteId, Note>>, SqliteClientError> {
|
) -> Result<Vec<ReceivedNote<crate::ReceivedNoteId, Note>>, SqliteClientError> {
|
||||||
todo!()
|
select_spendable_orchard_notes(
|
||||||
|
&st.wallet().conn,
|
||||||
|
&st.wallet().params,
|
||||||
|
account,
|
||||||
|
target_value,
|
||||||
|
anchor_height,
|
||||||
|
exclude,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrypted_pool_outputs_count(
|
fn decrypted_pool_outputs_count(
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
|
|
||||||
use group::ff::PrimeField;
|
use group::ff::PrimeField;
|
||||||
use incrementalmerkletree::Position;
|
use incrementalmerkletree::Position;
|
||||||
use rusqlite::{named_params, params, types::Value, Connection, Row};
|
use rusqlite::{named_params, params, Connection, Row};
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use sapling::{self, Diversifier, Nullifier, Rseed};
|
use sapling::{self, Diversifier, Nullifier, Rseed};
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
|
@ -21,7 +20,7 @@ use zip32::Scope;
|
||||||
|
|
||||||
use crate::{error::SqliteClientError, AccountId, ReceivedNoteId};
|
use crate::{error::SqliteClientError, AccountId, ReceivedNoteId};
|
||||||
|
|
||||||
use super::{memo_repr, parse_scope, scope_code, wallet_birthday};
|
use super::{memo_repr, parse_scope, scope_code};
|
||||||
|
|
||||||
/// This trait provides a generalization over shielded output representations.
|
/// This trait provides a generalization over shielded output representations.
|
||||||
pub(crate) trait ReceivedSaplingOutput {
|
pub(crate) trait ReceivedSaplingOutput {
|
||||||
|
@ -192,32 +191,14 @@ pub(crate) fn get_spendable_sapling_note<P: consensus::Parameters>(
|
||||||
txid: &TxId,
|
txid: &TxId,
|
||||||
index: u32,
|
index: u32,
|
||||||
) -> Result<Option<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError> {
|
) -> Result<Option<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError> {
|
||||||
let result = conn.query_row_and_then(
|
super::common::get_spendable_note(
|
||||||
"SELECT sapling_received_notes.id, txid, output_index,
|
conn,
|
||||||
diversifier, value, rcm, commitment_tree_position,
|
params,
|
||||||
accounts.ufvk, recipient_key_scope
|
txid,
|
||||||
FROM sapling_received_notes
|
index,
|
||||||
INNER JOIN accounts on accounts.id = sapling_received_notes.account_id
|
ShieldedProtocol::Sapling,
|
||||||
INNER JOIN transactions ON transactions.id_tx = sapling_received_notes.tx
|
to_spendable_note,
|
||||||
WHERE txid = :txid
|
)
|
||||||
AND accounts.ufvk IS NOT NULL
|
|
||||||
AND recipient_key_scope IS NOT NULL
|
|
||||||
AND output_index = :output_index
|
|
||||||
AND spent IS NULL",
|
|
||||||
named_params![
|
|
||||||
":txid": txid.as_ref(),
|
|
||||||
":output_index": index,
|
|
||||||
],
|
|
||||||
|row| to_spendable_note(params, row),
|
|
||||||
);
|
|
||||||
|
|
||||||
// `OptionalExtension` doesn't work here because the error type of `Result` is already
|
|
||||||
// `SqliteClientError`
|
|
||||||
match result {
|
|
||||||
Ok(r) => Ok(r),
|
|
||||||
Err(SqliteClientError::DbError(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Utility method for determining whether we have any spendable notes
|
/// Utility method for determining whether we have any spendable notes
|
||||||
|
@ -225,25 +206,6 @@ pub(crate) fn get_spendable_sapling_note<P: consensus::Parameters>(
|
||||||
/// If the tip shard has unscanned ranges below the anchor height and greater than or equal to
|
/// If the tip shard has unscanned ranges below the anchor height and greater than or equal to
|
||||||
/// the wallet birthday, none of our notes can be spent because we cannot construct witnesses at
|
/// the wallet birthday, none of our notes can be spent because we cannot construct witnesses at
|
||||||
/// the provided anchor height.
|
/// the provided anchor height.
|
||||||
fn unscanned_tip_exists(
|
|
||||||
conn: &Connection,
|
|
||||||
anchor_height: BlockHeight,
|
|
||||||
) -> Result<bool, rusqlite::Error> {
|
|
||||||
// v_sapling_shard_unscanned_ranges only returns ranges ending on or after wallet birthday, so
|
|
||||||
// we don't need to refer to the birthday in this query.
|
|
||||||
conn.query_row(
|
|
||||||
"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 IFNULL(range.subtree_end_height, :anchor_height)
|
|
||||||
)",
|
|
||||||
named_params![":anchor_height": u32::from(anchor_height),],
|
|
||||||
|row| row.get::<_, bool>(0),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn select_spendable_sapling_notes<P: consensus::Parameters>(
|
pub(crate) fn select_spendable_sapling_notes<P: consensus::Parameters>(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
params: &P,
|
params: &P,
|
||||||
|
@ -252,88 +214,16 @@ pub(crate) fn select_spendable_sapling_notes<P: consensus::Parameters>(
|
||||||
anchor_height: BlockHeight,
|
anchor_height: BlockHeight,
|
||||||
exclude: &[ReceivedNoteId],
|
exclude: &[ReceivedNoteId],
|
||||||
) -> Result<Vec<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError> {
|
) -> Result<Vec<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError> {
|
||||||
let birthday_height = match wallet_birthday(conn)? {
|
super::common::select_spendable_notes(
|
||||||
Some(birthday) => birthday,
|
conn,
|
||||||
None => {
|
params,
|
||||||
// the wallet birthday can only be unknown if there are no accounts in the wallet; in
|
account,
|
||||||
// such a case, the wallet has no notes to spend.
|
target_value,
|
||||||
return Ok(vec![]);
|
anchor_height,
|
||||||
}
|
exclude,
|
||||||
};
|
ShieldedProtocol::Sapling,
|
||||||
|
to_spendable_note,
|
||||||
if unscanned_tip_exists(conn, anchor_height)? {
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The goal of this SQL statement is to select the oldest notes until the required
|
|
||||||
// value has been reached.
|
|
||||||
// 1) Use a window function to create a view of all notes, ordered from oldest to
|
|
||||||
// newest, with an additional column containing a running sum:
|
|
||||||
// - Unspent notes accumulate the values of all unspent notes in that note's
|
|
||||||
// account, up to itself.
|
|
||||||
// - Spent notes accumulate the values of all notes in the transaction they were
|
|
||||||
// spent in, up to itself.
|
|
||||||
//
|
|
||||||
// 2) Select all unspent notes in the desired account, along with their running sum.
|
|
||||||
//
|
|
||||||
// 3) Select all notes for which the running sum was less than the required value, as
|
|
||||||
// well as a single note for which the sum was greater than or equal to the
|
|
||||||
// required value, bringing the sum of all selected notes across the threshold.
|
|
||||||
//
|
|
||||||
// 4) Match the selected notes against the witnesses at the desired height.
|
|
||||||
let mut stmt_select_notes = conn.prepare_cached(
|
|
||||||
"WITH eligible AS (
|
|
||||||
SELECT
|
|
||||||
sapling_received_notes.id AS id, txid, output_index, diversifier, value, rcm, commitment_tree_position,
|
|
||||||
SUM(value)
|
|
||||||
OVER (PARTITION BY sapling_received_notes.account_id, spent ORDER BY sapling_received_notes.id) AS so_far,
|
|
||||||
accounts.ufvk as ufvk, recipient_key_scope
|
|
||||||
FROM sapling_received_notes
|
|
||||||
INNER JOIN accounts on accounts.id = sapling_received_notes.account_id
|
|
||||||
INNER JOIN transactions
|
|
||||||
ON transactions.id_tx = sapling_received_notes.tx
|
|
||||||
WHERE sapling_received_notes.account_id = :account
|
|
||||||
AND ufvk IS NOT NULL
|
|
||||||
AND recipient_key_scope IS NOT NULL
|
|
||||||
AND commitment_tree_position IS NOT NULL
|
|
||||||
AND spent IS NULL
|
|
||||||
AND transactions.block <= :anchor_height
|
|
||||||
AND sapling_received_notes.id 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
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
SELECT id, txid, output_index, diversifier, value, rcm, commitment_tree_position, ufvk, recipient_key_scope
|
|
||||||
FROM eligible WHERE so_far < :target_value
|
|
||||||
UNION
|
|
||||||
SELECT id, txid, output_index, diversifier, value, rcm, commitment_tree_position, ufvk, recipient_key_scope
|
|
||||||
FROM (SELECT * from eligible WHERE so_far >= :target_value LIMIT 1)",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let excluded: Vec<Value> = exclude.iter().map(|n| Value::from(n.1)).collect();
|
|
||||||
let excluded_ptr = Rc::new(excluded);
|
|
||||||
|
|
||||||
let notes = stmt_select_notes.query_and_then(
|
|
||||||
named_params![
|
|
||||||
":account": account.0,
|
|
||||||
":anchor_height": &u32::from(anchor_height),
|
|
||||||
":target_value": &u64::from(target_value),
|
|
||||||
":exclude": &excluded_ptr,
|
|
||||||
":wallet_birthday": u32::from(birthday_height)
|
|
||||||
],
|
|
||||||
|r| to_spendable_note(params, r),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
notes
|
|
||||||
.filter_map(|r| r.transpose())
|
|
||||||
.collect::<Result<_, _>>()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieves the set of nullifiers for "potentially spendable" Sapling notes that the
|
/// Retrieves the set of nullifiers for "potentially spendable" Sapling notes that the
|
||||||
|
|
|
@ -381,15 +381,12 @@ pub(crate) fn scan_complete<P: consensus::Parameters>(
|
||||||
.map(|extended| ScanRange::from_parts(range.end..extended.end, ScanPriority::FoundNote))
|
.map(|extended| ScanRange::from_parts(range.end..extended.end, ScanPriority::FoundNote))
|
||||||
.filter(|range| !range.is_empty());
|
.filter(|range| !range.is_empty());
|
||||||
|
|
||||||
replace_queue_entries::<SqliteClientError>(
|
let replacement = Some(scanned)
|
||||||
conn,
|
|
||||||
&query_range,
|
|
||||||
Some(scanned)
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(extended_before)
|
.chain(extended_before)
|
||||||
.chain(extended_after),
|
.chain(extended_after);
|
||||||
false,
|
|
||||||
)?;
|
replace_queue_entries::<SqliteClientError>(conn, &query_range, replacement, false)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -445,7 +442,7 @@ pub(crate) fn update_chain_tip<P: consensus::Parameters>(
|
||||||
// `ScanRange` uses an exclusive upper bound.
|
// `ScanRange` uses an exclusive upper bound.
|
||||||
let chain_end = new_tip + 1;
|
let chain_end = new_tip + 1;
|
||||||
|
|
||||||
// Read the maximum height from each of the the shards tables. The minimum of the two
|
// Read the maximum height from each of the shards tables. The minimum of the two
|
||||||
// gives the start of a height range that covers the last incomplete shard of both the
|
// gives the start of a height range that covers the last incomplete shard of both the
|
||||||
// Sapling and Orchard pools.
|
// Sapling and Orchard pools.
|
||||||
let sapling_shard_tip = tip_shard_end_height(conn, SAPLING_TABLES_PREFIX)?;
|
let sapling_shard_tip = tip_shard_end_height(conn, SAPLING_TABLES_PREFIX)?;
|
||||||
|
|
Loading…
Reference in New Issue