diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 35497ef653..f1afad0aec 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -12,6 +12,7 @@ and this library adheres to Rust's notion of - `TransparentInputSource` - `SaplingInputSource` - `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 @@ -20,6 +21,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`: @@ -33,11 +50,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 diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index c623de26d3..7b79e8eaa6 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -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>, Self::Error>; + /// Returns a list of spendable Sapling notes sufficient to cover the specified target value, /// if possible. fn select_spendable_sapling_notes( @@ -240,6 +248,13 @@ 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, 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( @@ -979,6 +994,14 @@ pub mod testing { type Error = (); type NoteRef = u32; + fn get_spendable_sapling_note( + &self, + _txid: &TxId, + _index: u32, + ) -> Result>, Self::Error> { + Ok(None) + } + fn select_spendable_sapling_notes( &self, _account: AccountId, @@ -994,6 +1017,13 @@ pub mod testing { impl TransparentInputSource for MockWalletDb { type Error = (); + fn get_unspent_transparent_output( + &self, + _outpoint: &OutPoint, + ) -> Result, Self::Error> { + Ok(None) + } + fn get_unspent_transparent_outputs( &self, _address: &TransparentAddress, diff --git a/zcash_client_backend/src/data_api/wallet/input_selection.rs b/zcash_client_backend/src/data_api/wallet/input_selection.rs index ccba6de006..1e675f3135 100644 --- a/zcash_client_backend/src/data_api/wallet/input_selection.rs +++ b/zcash_client_backend/src/data_api/wallet/input_selection.rs @@ -79,6 +79,7 @@ impl fmt::Display for InputSelectorError { transaction_request: TransactionRequest, transparent_inputs: Vec, @@ -90,6 +91,45 @@ pub struct Proposal { } impl Proposal { + /// Constructs a [`Proposal`] from its constituent parts. + #[allow(clippy::too_many_arguments)] + pub(crate) fn from_parts( + transaction_request: TransactionRequest, + transparent_inputs: Vec, + sapling_inputs: Option>, + balance: TransactionBalance, + fee_rule: FeeRuleT, + min_target_height: BlockHeight, + is_shielding: bool, + ) -> Result { + 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 Debug for Proposal { } /// The Sapling inputs to a proposed transaction. +#[derive(Clone, PartialEq, Eq)] pub struct SaplingInputs { anchor_height: BlockHeight, notes: NonEmpty>, } impl SaplingInputs { + /// Constructs a [`SaplingInputs`] from its constituent parts. + pub fn from_parts( + anchor_height: BlockHeight, + notes: NonEmpty>, + ) -> 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 { diff --git a/zcash_client_backend/src/fees.rs b/zcash_client_backend/src/fees.rs index a10a119e33..cf52956c72 100644 --- a/zcash_client_backend/src/fees.rs +++ b/zcash_client_backend/src/fees.rs @@ -54,6 +54,9 @@ impl ChangeValue { pub struct TransactionBalance { proposed_change: Vec, 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, } diff --git a/zcash_client_backend/src/proto.rs b/zcash_client_backend/src/proto.rs index a440750fb0..445a3c4ba3 100644 --- a/zcash_client_backend/src/proto.rs +++ b/zcash_client_backend/src/proto.rs @@ -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::{note::ExtractedNoteCommitment, Node, Nullifier, NOTE_COMMITMENT_TREE_DEPTH}, - transaction::{components::sapling, TxId}, + transaction::{ + components::{amount::NonNegativeAmount, sapling, 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)] @@ -182,3 +198,223 @@ impl service::TreeState { read_commitment_tree::(&sapling_tree_bytes[..]) } } + +pub const PROPOSAL_SER_V1: u32 = 1; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProposalError { + Zip321(Zip321Error), + TxIdInvalid(Vec), + InputRetrieval(DbError), + InputNotFound(TxId, PoolType, u32), + BalanceInvalid, + MemoInvalid(memo::Error), + VersionInvalid(u32), + ZeroMinConf, + FeeRuleNotSpecified, +} + +impl From for ProposalError { + fn from(value: Zip321Error) -> Self { + Self::Zip321(value) + } +} + +impl proposal::ProposedInput { + pub fn parse_txid(&self) -> Result> { + 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( + params: &P, + value: &Proposal, + ) -> Option { + 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( + &self, + params: &P, + wallet_db: &DbT, + ) -> Result, ProposalError> + where + DbT: TransparentInputSource + SaplingInputSource, + { + 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::, _>>()?; + + 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::, _>>() + .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> { + 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::, _>>()?, + 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)), + } + } +} diff --git a/zcash_client_backend/src/wallet.rs b/zcash_client_backend/src/wallet.rs index 63863f2e07..35b32fffca 100644 --- a/zcash_client_backend/src/wallet.rs +++ b/zcash_client_backend/src/wallet.rs @@ -29,7 +29,7 @@ pub struct WalletTx { pub sapling_outputs: Vec>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct WalletTransparentOutput { outpoint: OutPoint, txout: TxOut, @@ -175,7 +175,7 @@ impl WalletSaplingOutput { /// 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 { note_id: NoteRef, txid: TxId, diff --git a/zcash_client_backend/src/zip321.rs b/zcash_client_backend/src/zip321.rs index 28523f677a..06d18e8d24 100644 --- a/zcash_client_backend/src/zip321.rs +++ b/zcash_client_backend/src/zip321.rs @@ -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 { } /// 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, } @@ -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 { + 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(&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), 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, diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 047c1d6ea1..85d0500435 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -173,6 +173,14 @@ impl, P: consensus::Parameters> SaplingInputSour type Error = SqliteClientError; type NoteRef = ReceivedNoteId; + fn get_spendable_sapling_note( + &self, + txid: &TxId, + index: u32, + ) -> Result>, 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, P: consensus::Parameters> TransparentInput { type Error = SqliteClientError; + fn get_unspent_transparent_output( + &self, + _outpoint: &OutPoint, + ) -> Result, 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, diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index aa624c5425..57927e2ad5 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -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 TestState { >, > { let params = self.network(); - propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>( + let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>( &mut self.db_data, ¶ms, fee_rule, @@ -565,7 +568,13 @@ impl TestState { amount, memo, change_memo, - ) + ); + + if let Ok(proposal) = &result { + check_proposal_serialization_roundtrip(self.wallet(), proposal); + } + + result } /// Invokes [`propose_shielding`] with the given arguments. @@ -1051,3 +1060,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, + proposal: &Proposal, +) { + 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); +} diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index eaa45485f1..c30450d318 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -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::{ @@ -1267,6 +1268,65 @@ pub(crate) fn truncate_to_height( Ok(()) } +#[cfg(feature = "transparent-inputs")] +fn to_unspent_transparent_output(row: &Row) -> Result { + let txid: Vec = 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, 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, 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`. @@ -1283,7 +1343,7 @@ pub(crate) fn get_unspent_transparent_outputs( .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 @@ -1297,45 +1357,18 @@ pub(crate) fn get_unspent_transparent_outputs( let addr_str = address.encode(params); let mut utxos = Vec::::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 = exclude.iter().cloned().collect(); while let Some(row) = rows.next()? { - let txid: Vec = 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); } diff --git a/zcash_client_sqlite/src/wallet/sapling.rs b/zcash_client_sqlite/src/wallet/sapling.rs index 970e9e33dc..ee53c1ba2a 100644 --- a/zcash_client_sqlite/src/wallet/sapling.rs +++ b/zcash_client_sqlite/src/wallet/sapling.rs @@ -135,6 +135,38 @@ fn to_spendable_note(row: &Row) -> Result, 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>, 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 diff --git a/zcash_primitives/CHANGELOG.md b/zcash_primitives/CHANGELOG.md index 52c64dc4d7..2a0a0823ad 100644 --- a/zcash_primitives/CHANGELOG.md +++ b/zcash_primitives/CHANGELOG.md @@ -60,6 +60,9 @@ and this library adheres to Rust's notion of - `impl From for zcash_primitives::sapling::value::NoteValue` - `impl Sum for Option` - `impl<'a> Sum<&'a NonNegativeAmount> for Option` +- `impl {Clone, PartialEq, Eq} for zcash_primitives::memo::Error` +- `impl {PartialEq, Eq} for zcash_primitives::sapling::note::Rseed` +- `impl From for [u8; 32]` ### Changed - `zcash_primitives::sapling`: diff --git a/zcash_primitives/src/memo.rs b/zcash_primitives/src/memo.rs index 8143eec69d..c6e97b66f7 100644 --- a/zcash_primitives/src/memo.rs +++ b/zcash_primitives/src/memo.rs @@ -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), diff --git a/zcash_primitives/src/sapling/note.rs b/zcash_primitives/src/sapling/note.rs index 2e5b21bc6b..f92521a8ec 100644 --- a/zcash_primitives/src/sapling/note.rs +++ b/zcash_primitives/src/sapling/note.rs @@ -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]),