Apply suggestions from code review

Co-authored-by: str4d <thestr4d@gmail.com>
Co-authored-by: Daira Emma Hopwood <daira@jacaranda.org>
This commit is contained in:
Kris Nuttycombe 2023-11-14 10:44:21 -07:00 committed by Kris Nuttycombe
parent 33169719ce
commit aeb405ef5d
8 changed files with 103 additions and 61 deletions

View File

@ -28,8 +28,7 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::proto::`
- `PROPOSAL_SER_V1`
- `ProposalError`
- `proposal::Proposal::{from_standard_proposal, try_into_standard_proposal}`
- `proposal::ProposedInput::parse_txid`
- `proposal` module, for parsing and serializing transaction proposals.
- `impl Clone for zcash_client_backend::{
zip321::{Payment, TransactionRequest, Zip321Error, parse::Param, parse::IndexedParam},
wallet::{ReceivedSaplingNote, WalletTransparentOutput},

View File

@ -31,7 +31,7 @@ message Proposal {
}
message SaplingInputs {
// Returns the Sapling anchor height to be used in creating the transaction.
// The Sapling anchor height to be used in creating the transaction
uint32 anchorHeight = 1;
// The unique identifier and amount for each proposed Sapling input
repeated ProposedInput inputs = 2;
@ -46,7 +46,7 @@ message ProposedInput {
// The fee rule used in constructing a Proposal
enum FeeRule {
// Protobuf requires that enums have a zero descriminant as the default
// Protobuf requires that enums have a zero discriminant as the default
// value. However, we need to require that a known fee rule is selected,
// and we do not want to fall back to any default, so sending the
// FeeRuleNotSpecified value will be treated as an error.

View File

@ -222,8 +222,11 @@ 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.
/// Fetches a spendable Sapling note by indexing into the specified transaction's
/// [`sapling::Bundle::shielded_outputs`].
///
/// Returns `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,
@ -247,8 +250,10 @@ 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.
/// Fetches a spendable transparent output.
///
/// Returns `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,

View File

@ -90,10 +90,25 @@ pub struct Proposal<FeeRuleT, NoteRef> {
is_shielding: bool,
}
/// Errors that
#[derive(Debug, Clone)]
pub enum ProposalError {
RequestInvalid,
Overflow,
BalanceError {
input_total: NonNegativeAmount,
output_total: NonNegativeAmount,
},
ShieldingFlagInvalid,
}
impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
/// Constructs a [`Proposal`] from its constituent parts.
/// Constructs a validated [`Proposal`] from its constituent parts.
///
/// This operation validaes the proposal for balance consistency and agreement between
/// the `is_shielding` flag and the structure of the proposal.
#[allow(clippy::too_many_arguments)]
pub(crate) fn from_parts(
pub fn from_parts(
transaction_request: TransactionRequest,
transparent_inputs: Vec<WalletTransparentOutput>,
sapling_inputs: Option<SaplingInputs<NoteRef>>,
@ -101,19 +116,35 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
fee_rule: FeeRuleT,
min_target_height: BlockHeight,
is_shielding: bool,
) -> Result<Self, ()> {
let transparent_total = transparent_inputs
) -> Result<Self, ProposalError> {
let transparent_input_total = transparent_inputs
.iter()
.map(|out| out.txout().value)
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| (acc? + a).ok_or(()))?;
let sapling_total = sapling_inputs
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
(acc? + a).ok_or(ProposalError::Overflow)
})?;
let sapling_input_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(())?;
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
(acc? + a).ok_or(ProposalError::Overflow)
})?;
let input_total =
(transparent_input_total + sapling_input_total).ok_or(ProposalError::Overflow)?;
let output_total = (transaction_request.total()? + balance.total()).ok_or(())?;
let request_total = transaction_request
.total()
.map_err(|_| ProposalError::RequestInvalid)?;
let output_total = (request_total + balance.total()).ok_or(ProposalError::Overflow)?;
if is_shielding
&& (transparent_input_total == NonNegativeAmount::ZERO
|| sapling_input_total > NonNegativeAmount::ZERO
|| request_total > NonNegativeAmount::ZERO)
{
return Err(ProposalError::ShieldingFlagInvalid);
}
if input_total == output_total {
Ok(Self {
@ -126,7 +157,10 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
is_shielding,
})
} else {
Err(())
Err(ProposalError::BalanceError {
input_total,
output_total,
})
}
}

View File

@ -1,6 +1,6 @@
//! Generated code for handling light client protobuf structs.
use std::io;
use std::{array::TryFromSliceError, io};
use incrementalmerkletree::frontier::CommitmentTree;
@ -22,7 +22,7 @@ use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
use crate::{
data_api::{
wallet::input_selection::{Proposal, SaplingInputs},
wallet::input_selection::{Proposal, ProposalError, SaplingInputs},
PoolType, SaplingInputSource, ShieldedProtocol, TransparentInputSource,
},
fees::{ChangeValue, TransactionBalance},
@ -207,10 +207,10 @@ impl service::TreeState {
pub const PROPOSAL_SER_V1: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProposalError<DbError> {
#[derive(Debug, Clone)]
pub enum ProposalDecodingError<DbError> {
Zip321(Zip321Error),
TxIdInvalid(Vec<u8>),
TxIdInvalid(TryFromSliceError),
InputRetrieval(DbError),
InputNotFound(TxId, PoolType, u32),
BalanceInvalid,
@ -218,17 +218,18 @@ pub enum ProposalError<DbError> {
VersionInvalid(u32),
ZeroMinConf,
FeeRuleNotSpecified,
ProposalInvalid(ProposalError),
}
impl<E> From<Zip321Error> for ProposalError<E> {
impl<E> From<Zip321Error> for ProposalDecodingError<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()?))
pub fn parse_txid(&self) -> Result<TxId, TryFromSliceError> {
Ok(TxId::from_bytes(self.txid[..].try_into()?))
}
}
@ -311,7 +312,7 @@ impl proposal::Proposal {
&self,
params: &P,
wallet_db: &DbT,
) -> Result<Proposal<StandardFeeRule, DbT::NoteRef>, ProposalError<DbError>>
) -> Result<Proposal<StandardFeeRule, DbT::NoteRef>, ProposalDecodingError<DbError>>
where
DbT: TransparentInputSource<Error = DbError> + SaplingInputSource<Error = DbError>,
{
@ -323,7 +324,7 @@ impl proposal::Proposal {
proposal::FeeRule::Zip313 => StandardFeeRule::Zip313,
proposal::FeeRule::Zip317 => StandardFeeRule::Zip317,
proposal::FeeRule::NotSpecified => {
return Err(ProposalError::FeeRuleNotSpecified);
return Err(ProposalDecodingError::FeeRuleNotSpecified);
}
};
@ -333,14 +334,16 @@ impl proposal::Proposal {
.transparent_inputs
.iter()
.map(|t_in| {
let txid = t_in.parse_txid().map_err(ProposalError::TxIdInvalid)?;
let txid = t_in
.parse_txid()
.map_err(ProposalDecodingError::TxIdInvalid)?;
let outpoint = OutPoint::new(txid.into(), t_in.index);
wallet_db
.get_unspent_transparent_output(&outpoint)
.map_err(ProposalError::InputRetrieval)?
.map_err(ProposalDecodingError::InputRetrieval)?
.ok_or_else(|| {
ProposalError::InputNotFound(
ProposalDecodingError::InputNotFound(
txid,
PoolType::Transparent,
t_in.index,
@ -353,14 +356,16 @@ impl proposal::Proposal {
s_in.inputs
.iter()
.map(|s_in| {
let txid = s_in.parse_txid().map_err(ProposalError::TxIdInvalid)?;
let txid = s_in
.parse_txid()
.map_err(ProposalDecodingError::TxIdInvalid)?;
wallet_db
.get_spendable_sapling_note(&txid, s_in.index)
.map_err(ProposalError::InputRetrieval)
.map_err(ProposalDecodingError::InputRetrieval)
.and_then(|opt| {
opt.ok_or_else(|| {
ProposalError::InputNotFound(
ProposalDecodingError::InputNotFound(
txid,
PoolType::Shielded(ShieldedProtocol::Sapling),
s_in.index,
@ -377,37 +382,42 @@ impl proposal::Proposal {
.transpose()
});
let balance = self.balance.as_ref().ok_or(ProposalError::BalanceInvalid)?;
let proto_balance = self
.balance
.as_ref()
.ok_or(ProposalDecodingError::BalanceInvalid)?;
let balance = TransactionBalance::new(
balance
proto_balance
.proposed_change
.iter()
.filter_map(|cv| {
cv.value
.as_ref()
.map(|cv| -> Result<ChangeValue, ProposalError<_>> {
cv.value.as_ref().map(
|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
match cv {
proposal::change_value::Value::SaplingValue(sc) => {
Ok(ChangeValue::sapling(
NonNegativeAmount::from_u64(sc.amount)
.map_err(|_| ProposalError::BalanceInvalid)?,
NonNegativeAmount::from_u64(sc.amount).map_err(
|_| ProposalDecodingError::BalanceInvalid,
)?,
sc.memo
.as_ref()
.map(|bytes| {
MemoBytes::from_bytes(&bytes.value)
.map_err(ProposalError::MemoInvalid)
MemoBytes::from_bytes(&bytes.value).map_err(
ProposalDecodingError::MemoInvalid,
)
})
.transpose()?,
))
}
}
})
},
)
})
.collect::<Result<Vec<_>, _>>()?,
NonNegativeAmount::from_u64(balance.fee_required)
.map_err(|_| ProposalError::BalanceInvalid)?,
NonNegativeAmount::from_u64(proto_balance.fee_required)
.map_err(|_| ProposalDecodingError::BalanceInvalid)?,
)
.map_err(|_| ProposalError::BalanceInvalid)?;
.map_err(|_| ProposalDecodingError::BalanceInvalid)?;
Proposal::from_parts(
transaction_request,
@ -418,9 +428,9 @@ impl proposal::Proposal {
self.min_target_height.into(),
self.is_shielding,
)
.map_err(|_| ProposalError::BalanceInvalid)
.map_err(ProposalDecodingError::ProposalInvalid)
}
other => Err(ProposalError::VersionInvalid(other)),
other => Err(ProposalDecodingError::VersionInvalid(other)),
}
}
}

View File

@ -154,8 +154,8 @@ impl TransactionRequest {
/// 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. .
/// Returns `Err` in the case of overflow, or the value is
/// outside the range `0..=MAX_MONEY` zatoshis.
pub fn total(&self) -> Result<NonNegativeAmount, ()> {
if self.payments.is_empty() {
Ok(NonNegativeAmount::ZERO)

View File

@ -206,15 +206,9 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> TransparentInput
fn get_unspent_transparent_output(
&self,
_outpoint: &OutPoint,
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."
);
wallet::get_unspent_transparent_output(self.conn.borrow(), outpoint)
}
fn get_unspent_transparent_outputs(

View File

@ -135,8 +135,8 @@ 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
// 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_sapling_note(