zcash_client_backend: Add serialization & parsing for protobuf Proposal representation.

This commit is contained in:
Kris Nuttycombe 2023-10-12 13:48:20 -06:00
parent 572563338b
commit 33169719ce
14 changed files with 517 additions and 55 deletions

View File

@ -15,6 +15,7 @@ and this library adheres to Rust's notion of
- `ScannedBlock::sapling_tree_size`.
- `ScannedBlock::orchard_tree_size`.
- `wallet::propose_standard_transfer_to_address`
- `wallet::input_selection::Proposal::from_parts`
- `wallet::input_selection::SaplingInputs`
- `wallet::input_selection::ShieldingSelector` has been
factored out from the `InputSelector` trait to separate out transparent
@ -23,6 +24,22 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::wallet`:
- `ReceivedSaplingNote::from_parts`
- `ReceivedSaplingNote::{txid, output_index, diversifier, rseed, note_commitment_tree_position}`
- `zcash_client_backend::zip321::TransactionRequest::total`
- `zcash_client_backend::proto::`
- `PROPOSAL_SER_V1`
- `ProposalError`
- `proposal::Proposal::{from_standard_proposal, try_into_standard_proposal}`
- `proposal::ProposedInput::parse_txid`
- `impl Clone for zcash_client_backend::{
zip321::{Payment, TransactionRequest, Zip321Error, parse::Param, parse::IndexedParam},
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
wallet::input_selection::{Proposal, SaplingInputs},
}`
- `impl {PartialEq, Eq} for zcash_client_backend::{
zip321::{Zip321Error, parse::Param, parse::IndexedParam},
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
wallet::input_selection::{Proposal, SaplingInputs},
}`
### Changed
- `zcash_client_backend::data_api`:
@ -42,11 +59,11 @@ and this library adheres to Rust's notion of
backend-specific note identifier. The related `NoteRef` type parameter has
been removed from `error::Error`.
- A new variant `UnsupportedPoolType` has been added.
- `wallet::shield_transparent_funds` no longer
takes a `memo` argument; instead, memos to be associated with the shielded
outputs should be specified in the construction of the value of the
`input_selector` argument, which is used to construct the proposed shielded
values as internal "change" outputs.
- `wallet::shield_transparent_funds` no longer takes a `memo` argument;
instead, memos to be associated with the shielded outputs should be
specified in the construction of the value of the `input_selector`
argument, which is used to construct the proposed shielded values as
internal "change" outputs.
- `wallet::create_proposed_transaction` no longer takes a
`change_memo` argument; instead, change memos are represented in the
individual values of the `proposed_change` field of the `Proposal`'s

View File

@ -17,7 +17,10 @@ use zcash_primitives::{
memo::{Memo, MemoBytes},
sapling::{self, Node, NOTE_COMMITMENT_TREE_DEPTH},
transaction::{
components::amount::{Amount, NonNegativeAmount},
components::{
amount::{Amount, NonNegativeAmount},
OutPoint,
},
Transaction, TxId,
},
zip32::AccountId,
@ -31,9 +34,6 @@ use crate::{
wallet::{ReceivedSaplingNote, WalletTransparentOutput, WalletTx},
};
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::transaction::components::OutPoint;
use self::chain::CommitmentTreeRoot;
use self::scanning::ScanRange;
@ -222,6 +222,14 @@ pub trait SaplingInputSource {
/// or a UUID.
type NoteRef: Copy + Debug + Eq + Ord;
/// Returns a received Sapling note, or Ok(None) if the note is not known to belong to the
/// wallet or if the note is not spendable.
fn get_spendable_sapling_note(
&self,
txid: &TxId,
index: u32,
) -> Result<Option<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(
@ -235,11 +243,17 @@ pub trait SaplingInputSource {
/// 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 received transparent UTXO, or Ok(None) if the UTXO is not known to belong to the
/// wallet or is not spendable.
fn get_unspent_transparent_output(
&self,
outpoint: &OutPoint,
) -> Result<Option<WalletTransparentOutput>, 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(
@ -1008,6 +1022,14 @@ pub mod testing {
type Error = ();
type NoteRef = u32;
fn get_spendable_sapling_note(
&self,
_txid: &TxId,
_index: u32,
) -> Result<Option<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
Ok(None)
}
fn select_spendable_sapling_notes(
&self,
_account: AccountId,
@ -1023,6 +1045,13 @@ pub mod testing {
impl TransparentInputSource for MockWalletDb {
type Error = ();
fn get_unspent_transparent_output(
&self,
_outpoint: &OutPoint,
) -> Result<Option<WalletTransparentOutput>, Self::Error> {
Ok(None)
}
fn get_unspent_transparent_outputs(
&self,
_address: &TransparentAddress,

View File

@ -79,6 +79,7 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
}
/// The inputs to be consumed and outputs to be produced in a proposed transaction.
#[derive(Clone, PartialEq, Eq)]
pub struct Proposal<FeeRuleT, NoteRef> {
transaction_request: TransactionRequest,
transparent_inputs: Vec<WalletTransparentOutput>,
@ -90,6 +91,45 @@ pub struct Proposal<FeeRuleT, NoteRef> {
}
impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
/// Constructs a [`Proposal`] from its constituent parts.
#[allow(clippy::too_many_arguments)]
pub(crate) fn from_parts(
transaction_request: TransactionRequest,
transparent_inputs: Vec<WalletTransparentOutput>,
sapling_inputs: Option<SaplingInputs<NoteRef>>,
balance: TransactionBalance,
fee_rule: FeeRuleT,
min_target_height: BlockHeight,
is_shielding: bool,
) -> Result<Self, ()> {
let transparent_total = transparent_inputs
.iter()
.map(|out| out.txout().value)
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| (acc? + a).ok_or(()))?;
let sapling_total = sapling_inputs
.iter()
.flat_map(|s_in| s_in.notes().iter())
.map(|out| out.value())
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| (acc? + a).ok_or(()))?;
let input_total = (transparent_total + sapling_total).ok_or(())?;
let output_total = (transaction_request.total()? + balance.total()).ok_or(())?;
if input_total == output_total {
Ok(Self {
transaction_request,
transparent_inputs,
sapling_inputs,
balance,
fee_rule,
min_target_height,
is_shielding,
})
} else {
Err(())
}
}
/// Returns the transaction request that describes the payments to be made.
pub fn transaction_request(&self) -> &TransactionRequest {
&self.transaction_request
@ -147,12 +187,24 @@ impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
}
/// The Sapling inputs to a proposed transaction.
#[derive(Clone, PartialEq, Eq)]
pub struct SaplingInputs<NoteRef> {
anchor_height: BlockHeight,
notes: NonEmpty<ReceivedSaplingNote<NoteRef>>,
}
impl<NoteRef> SaplingInputs<NoteRef> {
/// Constructs a [`SaplingInputs`] from its constituent parts.
pub fn from_parts(
anchor_height: BlockHeight,
notes: NonEmpty<ReceivedSaplingNote<NoteRef>>,
) -> Self {
Self {
anchor_height,
notes,
}
}
/// Returns the anchor height for Sapling inputs that should be used when constructing the
/// proposed transaction.
pub fn anchor_height(&self) -> BlockHeight {

View File

@ -54,6 +54,9 @@ impl ChangeValue {
pub struct TransactionBalance {
proposed_change: Vec<ChangeValue>,
fee_required: NonNegativeAmount,
// A cache for the sum of proposed change and fee; we compute it on construction anyway, so we
// cache the resulting value.
total: NonNegativeAmount,
}

View File

@ -3,16 +3,32 @@
use std::io;
use incrementalmerkletree::frontier::CommitmentTree;
use nonempty::NonEmpty;
use zcash_primitives::{
block::{BlockHash, BlockHeader},
consensus::BlockHeight,
consensus::{self, BlockHeight, Parameters},
memo::{self, MemoBytes},
merkle_tree::read_commitment_tree,
sapling::{self, note::ExtractedNoteCommitment, Node, Nullifier, NOTE_COMMITMENT_TREE_DEPTH},
transaction::TxId,
transaction::{
components::{amount::NonNegativeAmount, OutPoint},
fees::StandardFeeRule,
TxId,
},
};
use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
use crate::{
data_api::{
wallet::input_selection::{Proposal, SaplingInputs},
PoolType, SaplingInputSource, ShieldedProtocol, TransparentInputSource,
},
fees::{ChangeValue, TransactionBalance},
zip321::{TransactionRequest, Zip321Error},
};
#[rustfmt::skip]
#[allow(unknown_lints)]
#[allow(clippy::derive_partial_eq_without_eq)]
@ -188,3 +204,223 @@ impl service::TreeState {
read_commitment_tree::<Node, _, NOTE_COMMITMENT_TREE_DEPTH>(&sapling_tree_bytes[..])
}
}
pub const PROPOSAL_SER_V1: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProposalError<DbError> {
Zip321(Zip321Error),
TxIdInvalid(Vec<u8>),
InputRetrieval(DbError),
InputNotFound(TxId, PoolType, u32),
BalanceInvalid,
MemoInvalid(memo::Error),
VersionInvalid(u32),
ZeroMinConf,
FeeRuleNotSpecified,
}
impl<E> From<Zip321Error> for ProposalError<E> {
fn from(value: Zip321Error) -> Self {
Self::Zip321(value)
}
}
impl proposal::ProposedInput {
pub fn parse_txid(&self) -> Result<TxId, Vec<u8>> {
Ok(TxId::from_bytes(self.txid.clone().try_into()?))
}
}
impl proposal::Proposal {
/// Serializes a [`Proposal`] based upon a supported [`StandardFeeRule`] to its protobuf
/// representation.
pub fn from_standard_proposal<P: Parameters, NoteRef>(
params: &P,
value: &Proposal<StandardFeeRule, NoteRef>,
) -> Option<Self> {
let transaction_request = value.transaction_request().to_uri(params)?;
let transparent_inputs = value
.transparent_inputs()
.iter()
.map(|utxo| proposal::ProposedInput {
txid: utxo.outpoint().hash().to_vec(),
index: utxo.outpoint().n(),
value: utxo.txout().value.into(),
})
.collect();
let sapling_inputs = value
.sapling_inputs()
.map(|sapling_inputs| proposal::SaplingInputs {
anchor_height: sapling_inputs.anchor_height().into(),
inputs: sapling_inputs
.notes()
.iter()
.map(|rec_note| proposal::ProposedInput {
txid: rec_note.txid().as_ref().to_vec(),
index: rec_note.output_index().into(),
value: rec_note.value().into(),
})
.collect(),
});
let balance = Some(proposal::TransactionBalance {
proposed_change: value
.balance()
.proposed_change()
.iter()
.map(|cv| match cv {
ChangeValue::Sapling { value, memo } => proposal::ChangeValue {
value: Some(proposal::change_value::Value::SaplingValue(
proposal::SaplingChange {
amount: (*value).into(),
memo: memo.as_ref().map(|memo_bytes| proposal::MemoBytes {
value: memo_bytes.as_slice().to_vec(),
}),
},
)),
},
})
.collect(),
fee_required: value.balance().fee_required().into(),
});
#[allow(deprecated)]
Some(proposal::Proposal {
proto_version: PROPOSAL_SER_V1,
transaction_request,
transparent_inputs,
sapling_inputs,
balance,
fee_rule: match value.fee_rule() {
StandardFeeRule::PreZip313 => proposal::FeeRule::PreZip313,
StandardFeeRule::Zip313 => proposal::FeeRule::Zip313,
StandardFeeRule::Zip317 => proposal::FeeRule::Zip317,
}
.into(),
min_target_height: value.min_target_height().into(),
is_shielding: value.is_shielding(),
})
}
/// Attempts to parse a [`Proposal`] based upon a supported [`StandardFeeRule`] from its
/// protobuf representation.
pub fn try_into_standard_proposal<P: consensus::Parameters, DbT, DbError>(
&self,
params: &P,
wallet_db: &DbT,
) -> Result<Proposal<StandardFeeRule, DbT::NoteRef>, ProposalError<DbError>>
where
DbT: TransparentInputSource<Error = DbError> + SaplingInputSource<Error = DbError>,
{
match self.proto_version {
PROPOSAL_SER_V1 => {
#[allow(deprecated)]
let fee_rule = match self.fee_rule() {
proposal::FeeRule::PreZip313 => StandardFeeRule::PreZip313,
proposal::FeeRule::Zip313 => StandardFeeRule::Zip313,
proposal::FeeRule::Zip317 => StandardFeeRule::Zip317,
proposal::FeeRule::NotSpecified => {
return Err(ProposalError::FeeRuleNotSpecified);
}
};
let transaction_request =
TransactionRequest::from_uri(params, &self.transaction_request)?;
let transparent_inputs = self
.transparent_inputs
.iter()
.map(|t_in| {
let txid = t_in.parse_txid().map_err(ProposalError::TxIdInvalid)?;
let outpoint = OutPoint::new(txid.into(), t_in.index);
wallet_db
.get_unspent_transparent_output(&outpoint)
.map_err(ProposalError::InputRetrieval)?
.ok_or_else(|| {
ProposalError::InputNotFound(
txid,
PoolType::Transparent,
t_in.index,
)
})
})
.collect::<Result<Vec<_>, _>>()?;
let sapling_inputs = self.sapling_inputs.as_ref().and_then(|s_in| {
s_in.inputs
.iter()
.map(|s_in| {
let txid = s_in.parse_txid().map_err(ProposalError::TxIdInvalid)?;
wallet_db
.get_spendable_sapling_note(&txid, s_in.index)
.map_err(ProposalError::InputRetrieval)
.and_then(|opt| {
opt.ok_or_else(|| {
ProposalError::InputNotFound(
txid,
PoolType::Shielded(ShieldedProtocol::Sapling),
s_in.index,
)
})
})
})
.collect::<Result<Vec<_>, _>>()
.map(|notes| {
NonEmpty::from_vec(notes).map(|notes| {
SaplingInputs::from_parts(s_in.anchor_height.into(), notes)
})
})
.transpose()
});
let balance = self.balance.as_ref().ok_or(ProposalError::BalanceInvalid)?;
let balance = TransactionBalance::new(
balance
.proposed_change
.iter()
.filter_map(|cv| {
cv.value
.as_ref()
.map(|cv| -> Result<ChangeValue, ProposalError<_>> {
match cv {
proposal::change_value::Value::SaplingValue(sc) => {
Ok(ChangeValue::sapling(
NonNegativeAmount::from_u64(sc.amount)
.map_err(|_| ProposalError::BalanceInvalid)?,
sc.memo
.as_ref()
.map(|bytes| {
MemoBytes::from_bytes(&bytes.value)
.map_err(ProposalError::MemoInvalid)
})
.transpose()?,
))
}
}
})
})
.collect::<Result<Vec<_>, _>>()?,
NonNegativeAmount::from_u64(balance.fee_required)
.map_err(|_| ProposalError::BalanceInvalid)?,
)
.map_err(|_| ProposalError::BalanceInvalid)?;
Proposal::from_parts(
transaction_request,
transparent_inputs,
sapling_inputs.transpose()?,
balance,
fee_rule,
self.min_target_height.into(),
self.is_shielding,
)
.map_err(|_| ProposalError::BalanceInvalid)
}
other => Err(ProposalError::VersionInvalid(other)),
}
}
}

View File

@ -29,7 +29,7 @@ pub struct WalletTx<N> {
pub sapling_outputs: Vec<WalletSaplingOutput<N>>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalletTransparentOutput {
outpoint: OutPoint,
txout: TxOut,
@ -175,7 +175,7 @@ impl<N> WalletSaplingOutput<N> {
/// Information about a note that is tracked by the wallet that is available for spending,
/// with sufficient information for use in note selection.
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceivedSaplingNote<NoteRef> {
note_id: NoteRef,
txid: TxId,

View File

@ -24,7 +24,7 @@ use std::cmp::Ordering;
use crate::address::RecipientAddress;
/// Errors that may be produced in decoding of payment requests.
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Zip321Error {
/// A memo field in the ZIP 321 URI was not properly base-64 encoded
InvalidBase64(base64::DecodeError),
@ -63,7 +63,7 @@ pub fn memo_from_base64(s: &str) -> Result<MemoBytes, Zip321Error> {
}
/// A single payment being requested.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Payment {
/// The payment address to which the payment should be sent.
pub recipient_address: RecipientAddress,
@ -121,7 +121,7 @@ impl Payment {
/// When constructing a transaction in response to such a request,
/// a separate output should be added to the transaction for each
/// payment value in the request.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransactionRequest {
payments: Vec<Payment>,
}
@ -152,6 +152,21 @@ impl TransactionRequest {
&self.payments[..]
}
/// Returns the total value of payments to be made.
///
/// Returns `Err` in the case of overflow, if payment values are negative, or the value is
/// outside the valid range of Zcash values. .
pub fn total(&self) -> Result<NonNegativeAmount, ()> {
if self.payments.is_empty() {
Ok(NonNegativeAmount::ZERO)
} else {
self.payments
.iter()
.map(|p| p.amount)
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| (acc? + a).ok_or(()))
}
}
/// A utility for use in tests to help check round-trip serialization properties.
#[cfg(any(test, feature = "test-dependencies"))]
pub(in crate::zip321) fn normalize<P: consensus::Parameters>(&mut self, params: &P) {
@ -416,7 +431,7 @@ mod parse {
/// A data type that defines the possible parameter types which may occur within a
/// ZIP 321 URI.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Param {
Addr(Box<RecipientAddress>),
Amount(NonNegativeAmount),
@ -427,7 +442,7 @@ mod parse {
}
/// A [`Param`] value with its associated index.
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexedParam {
pub param: Param,
pub payment_index: usize,

View File

@ -173,6 +173,14 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> SaplingInputSour
type Error = SqliteClientError;
type NoteRef = ReceivedNoteId;
fn get_spendable_sapling_note(
&self,
txid: &TxId,
index: u32,
) -> Result<Option<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
wallet::sapling::get_spendable_sapling_note(self.conn.borrow(), txid, index)
}
fn select_spendable_sapling_notes(
&self,
account: AccountId,
@ -196,6 +204,19 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> TransparentInput
{
type Error = SqliteClientError;
fn get_unspent_transparent_output(
&self,
_outpoint: &OutPoint,
) -> Result<Option<WalletTransparentOutput>, Self::Error> {
#[cfg(feature = "transparent-inputs")]
return wallet::get_unspent_transparent_output(self.conn.borrow(), _outpoint);
#[cfg(not(feature = "transparent-inputs"))]
panic!(
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
}
fn get_unspent_transparent_outputs(
&self,
address: &TransparentAddress,

View File

@ -14,7 +14,6 @@ use tempfile::NamedTempFile;
#[cfg(feature = "unstable")]
use tempfile::TempDir;
use zcash_client_backend::fees::{standard, DustOutputPolicy};
#[allow(deprecated)]
use zcash_client_backend::{
address::RecipientAddress,
@ -37,6 +36,10 @@ use zcash_client_backend::{
wallet::OvkPolicy,
zip321,
};
use zcash_client_backend::{
fees::{standard, DustOutputPolicy},
proto::proposal,
};
use zcash_note_encryption::Domain;
use zcash_primitives::{
block::BlockHash,
@ -555,7 +558,7 @@ impl<Cache> TestState<Cache> {
>,
> {
let params = self.network();
propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>(
let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>(
&mut self.db_data,
&params,
fee_rule,
@ -565,7 +568,13 @@ impl<Cache> TestState<Cache> {
amount,
memo,
change_memo,
)
);
if let Ok(proposal) = &result {
check_proposal_serialization_roundtrip(self.wallet(), proposal);
}
result
}
/// Invokes [`propose_shielding`] with the given arguments.
@ -1061,3 +1070,15 @@ pub(crate) fn input_selector(
let change_strategy = standard::SingleOutputChangeStrategy::new(fee_rule, change_memo);
GreedyInputSelector::new(change_strategy, DustOutputPolicy::default())
}
pub(crate) fn check_proposal_serialization_roundtrip(
db_data: &WalletDb<rusqlite::Connection, Network>,
proposal: &Proposal<StandardFeeRule, ReceivedNoteId>,
) {
let proposal_proto = proposal::Proposal::from_standard_proposal(&db_data.params, proposal);
assert_matches!(proposal_proto, Some(_));
let deserialized_proposal = proposal_proto
.unwrap()
.try_into_standard_proposal(&db_data.params, db_data);
assert_matches!(deserialized_proposal, Ok(r) if &r == proposal);
}

View File

@ -110,6 +110,7 @@ use self::scanning::{parse_priority_code, replace_queue_entries};
#[cfg(feature = "transparent-inputs")]
use {
crate::UtxoId,
rusqlite::Row,
std::collections::BTreeSet,
zcash_client_backend::{address::AddressMetadata, wallet::WalletTransparentOutput},
zcash_primitives::{
@ -1287,6 +1288,65 @@ pub(crate) fn truncate_to_height<P: consensus::Parameters>(
Ok(())
}
#[cfg(feature = "transparent-inputs")]
fn to_unspent_transparent_output(row: &Row) -> Result<WalletTransparentOutput, SqliteClientError> {
let txid: Vec<u8> = row.get(0)?;
let mut txid_bytes = [0u8; 32];
txid_bytes.copy_from_slice(&txid);
let index: u32 = row.get(1)?;
let script_pubkey = Script(row.get(2)?);
let raw_value: i64 = row.get(3)?;
let value = NonNegativeAmount::from_nonnegative_i64(raw_value).map_err(|_| {
SqliteClientError::CorruptedData(format!("Invalid UTXO value: {}", raw_value))
})?;
let height: u32 = row.get(4)?;
let outpoint = OutPoint::new(txid_bytes, index);
WalletTransparentOutput::from_parts(
outpoint,
TxOut {
value,
script_pubkey,
},
BlockHeight::from(height),
)
.ok_or_else(|| {
SqliteClientError::CorruptedData(
"Txout script_pubkey value did not correspond to a P2PKH or P2SH address".to_string(),
)
})
}
#[cfg(feature = "transparent-inputs")]
pub(crate) fn get_unspent_transparent_output(
conn: &rusqlite::Connection,
outpoint: &OutPoint,
) -> Result<Option<WalletTransparentOutput>, SqliteClientError> {
let mut stmt_select_utxo = conn.prepare_cached(
"SELECT u.prevout_txid, u.prevout_idx, u.script, u.value_zat, u.height
FROM utxos u
LEFT OUTER JOIN transactions tx
ON tx.id_tx = u.spent_in_tx
WHERE u.prevout_txid = :txid
AND u.prevout_idx = :output_index
AND tx.block IS NULL",
)?;
let result: Result<Option<WalletTransparentOutput>, SqliteClientError> = stmt_select_utxo
.query_and_then(
named_params![
":txid": outpoint.hash(),
":output_index": outpoint.n()
],
to_unspent_transparent_output,
)?
.next()
.transpose();
result
}
/// Returns unspent transparent outputs that have been received by this wallet at the given
/// transparent address, such that the block that included the transaction was mined at a
/// height less than or equal to the provided `max_height`.
@ -1303,7 +1363,7 @@ pub(crate) fn get_unspent_transparent_outputs<P: consensus::Parameters>(
.unwrap_or(max_height)
.saturating_sub(PRUNING_DEPTH);
let mut stmt_blocks = conn.prepare(
let mut stmt_utxos = conn.prepare(
"SELECT u.prevout_txid, u.prevout_idx, u.script,
u.value_zat, u.height
FROM utxos u
@ -1317,45 +1377,18 @@ pub(crate) fn get_unspent_transparent_outputs<P: consensus::Parameters>(
let addr_str = address.encode(params);
let mut utxos = Vec::<WalletTransparentOutput>::new();
let mut rows = stmt_blocks.query(named_params![
let mut rows = stmt_utxos.query(named_params![
":address": addr_str,
":max_height": u32::from(max_height),
":stable_height": u32::from(stable_height),
])?;
let excluded: BTreeSet<OutPoint> = exclude.iter().cloned().collect();
while let Some(row) = rows.next()? {
let txid: Vec<u8> = row.get(0)?;
let mut txid_bytes = [0u8; 32];
txid_bytes.copy_from_slice(&txid);
let index: u32 = row.get(1)?;
let script_pubkey = Script(row.get(2)?);
let value_raw: i64 = row.get(3)?;
let value = NonNegativeAmount::from_nonnegative_i64(value_raw).map_err(|_| {
SqliteClientError::CorruptedData(format!("Negative utxo value: {}", value_raw))
})?;
let height: u32 = row.get(4)?;
let outpoint = OutPoint::new(txid_bytes, index);
if excluded.contains(&outpoint) {
let output = to_unspent_transparent_output(row)?;
if excluded.contains(output.outpoint()) {
continue;
}
let output = WalletTransparentOutput::from_parts(
outpoint,
TxOut {
value,
script_pubkey,
},
BlockHeight::from(height),
)
.ok_or_else(|| {
SqliteClientError::CorruptedData(
"Txout script_pubkey value did not correspond to a P2PKH or P2SH address"
.to_string(),
)
})?;
utxos.push(output);
}

View File

@ -135,6 +135,38 @@ fn to_spendable_note(row: &Row) -> Result<ReceivedSaplingNote<ReceivedNoteId>, S
))
}
// The `clippy::let_and_return` lint is explicitly allowed here because a bug in the Rust compiler
// (https://github.com/rust-lang/rust/issues/114633) fails to identify that the `result` temporary
// is required in order to resolve the borrows involved in the `query_and_then` call.
#[allow(clippy::let_and_return)]
pub(crate) fn get_spendable_sapling_note(
conn: &Connection,
txid: &TxId,
index: u32,
) -> Result<Option<ReceivedSaplingNote<ReceivedNoteId>>, SqliteClientError> {
let mut stmt_select_note = 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 txid = :txid
AND output_index = :output_index
AND spent IS NULL",
)?;
let result = stmt_select_note
.query_and_then(
named_params![
":txid": txid.as_ref(),
":output_index": index,
],
to_spendable_note,
)?
.next()
.transpose();
result
}
/// Utility method for determining whether we have any spendable notes
///
/// If the tip shard has unscanned ranges below the anchor height and greater than or equal to

View File

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

View File

@ -28,7 +28,7 @@ where
}
/// Errors that may result from attempting to construct an invalid memo.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
InvalidUtf8(std::str::Utf8Error),
TooLong(usize),

View File

@ -16,7 +16,7 @@ pub(super) mod nullifier;
/// Before ZIP 212, the note commitment trapdoor `rcm` must be a scalar value.
/// After ZIP 212, the note randomness `rseed` is a 32-byte sequence, used to derive
/// both the note commitment trapdoor `rcm` and the ephemeral private key `esk`.
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Rseed {
BeforeZip212(jubjub::Fr),
AfterZip212([u8; 32]),