zcash_client_backend: add `Display` and `Error` impls for proposal parsing errors.

This commit is contained in:
Kris Nuttycombe 2023-11-14 12:36:25 -07:00
parent aeb405ef5d
commit 7aab6fd7a7
5 changed files with 196 additions and 21 deletions

View File

@ -81,7 +81,7 @@ where
FE: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
match self {
Error::DataSource(e) => {
write!(
f,

View File

@ -1,7 +1,7 @@
//! Types related to the process of selecting inputs to be spent given a transaction request.
use core::marker::PhantomData;
use std::fmt::{self, Debug};
use std::fmt::{self, Debug, Display};
use nonempty::NonEmpty;
use zcash_primitives::{
@ -90,18 +90,58 @@ pub struct Proposal<FeeRuleT, NoteRef> {
is_shielding: bool,
}
/// Errors that
/// Errors that can occur in construction of a [`Proposal`].
#[derive(Debug, Clone)]
pub enum ProposalError {
RequestInvalid,
/// The total output value of the transaction request is not a valid Zcash amount.
RequestTotalInvalid,
/// The total of transaction inputs overflows the valid range of Zcash values.
Overflow,
/// The input total and output total of the payment request are not equal to one another. The
/// sum of transaction outputs, change, and fees is required to be exactly equal to the value
/// of provided inputs.
BalanceError {
input_total: NonNegativeAmount,
output_total: NonNegativeAmount,
},
ShieldingFlagInvalid,
/// The `is_shielding` flag may only be set to `true` under the following conditions:
/// * The total of transparent inputs is nonzero
/// * There exist no Sapling inputs
/// * There provided transaction request is empty; i.e. the only output values specified
/// are change and fee amounts.
ShieldingInvalid,
}
impl Display for ProposalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProposalError::RequestTotalInvalid => write!(
f,
"The total requested output value is not a valid Zcash amount."
),
ProposalError::Overflow => write!(
f,
"The total of transaction inputs overflows the valid range of Zcash values."
),
ProposalError::BalanceError {
input_total,
output_total,
} => write!(
f,
"Balance error: the output total {} was not equal to the input total {}",
u64::from(*output_total),
u64::from(*input_total)
),
ProposalError::ShieldingInvalid => write!(
f,
"The proposal violates the rules for a shielding transaction."
),
}
}
}
impl std::error::Error for ProposalError {}
impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
/// Constructs a validated [`Proposal`] from its constituent parts.
///
@ -135,7 +175,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
let request_total = transaction_request
.total()
.map_err(|_| ProposalError::RequestInvalid)?;
.map_err(|_| ProposalError::RequestTotalInvalid)?;
let output_total = (request_total + balance.total()).ok_or(ProposalError::Overflow)?;
if is_shielding
@ -143,7 +183,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|| sapling_input_total > NonNegativeAmount::ZERO
|| request_total > NonNegativeAmount::ZERO)
{
return Err(ProposalError::ShieldingFlagInvalid);
return Err(ProposalError::ShieldingInvalid);
}
if input_total == output_total {

View File

@ -1,6 +1,10 @@
//! Generated code for handling light client protobuf structs.
use std::{array::TryFromSliceError, io};
use std::{
array::TryFromSliceError,
fmt::{self, Display},
io,
};
use incrementalmerkletree::frontier::CommitmentTree;
@ -205,20 +209,34 @@ impl service::TreeState {
}
}
/// Constant for the V1 proposal serialization version.
pub const PROPOSAL_SER_V1: u32 = 1;
/// Errors that can occur in the process of decoding a [`Proposal`] from its protobuf
/// representation.
#[derive(Debug, Clone)]
pub enum ProposalDecodingError<DbError> {
/// The ZIP 321 transaction request URI was invalid.
Zip321(Zip321Error),
/// A transaction identifier string did not decode to a valid transaction ID.
TxIdInvalid(TryFromSliceError),
/// A failure occurred trying to retrievean unspent note or UTXO from the wallet database.
InputRetrieval(DbError),
/// The unspent note or UTXO corresponding to a proposal input was not found in the wallet
/// database.
InputNotFound(TxId, PoolType, u32),
/// The transaction balance, or a component thereof, failed to decode correctly.
BalanceInvalid,
/// Failed to decode a ZIP-302 compliant memo from the provided memo bytes.
MemoInvalid(memo::Error),
/// The serialization version returned by the protobuf was not recognized.
VersionInvalid(u32),
ZeroMinConf,
/// The proposal did not correctly specify a standard fee rule.
FeeRuleNotSpecified,
/// The proposal violated balance or structural constraints.
ProposalInvalid(ProposalError),
/// A `sapling_inputs` field was present, but contained no input note references.
EmptySaplingInputs,
}
impl<E> From<Zip321Error> for ProposalDecodingError<E> {
@ -227,6 +245,55 @@ impl<E> From<Zip321Error> for ProposalDecodingError<E> {
}
}
impl<E: Display> Display for ProposalDecodingError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProposalDecodingError::Zip321(err) => write!(f, "Transaction request invalid: {}", err),
ProposalDecodingError::TxIdInvalid(err) => {
write!(f, "Invalid transaction id: {:?}", err)
}
ProposalDecodingError::InputRetrieval(err) => write!(
f,
"An error occurred retrieving a transaction intput: {}",
err
),
ProposalDecodingError::InputNotFound(txid, pool, idx) => write!(
f,
"No {} input found for txid {}, index {}",
pool, txid, idx
),
ProposalDecodingError::BalanceInvalid => {
write!(f, "An error occurred decoding the proposal balance.")
}
ProposalDecodingError::MemoInvalid(err) => {
write!(f, "An error occurred decoding a proposed memo: {}", err)
}
ProposalDecodingError::VersionInvalid(v) => {
write!(f, "Unrecognized proposal version {}", v)
}
ProposalDecodingError::FeeRuleNotSpecified => {
write!(f, "Proposal did not specify a known fee rule.")
}
ProposalDecodingError::ProposalInvalid(err) => write!(f, "{}", err),
ProposalDecodingError::EmptySaplingInputs => write!(
f,
"A `sapling_inputs` field was present, but contained no Sapling note references."
),
}
}
}
impl<E: std::error::Error + 'static> std::error::Error for ProposalDecodingError<E> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ProposalDecodingError::Zip321(e) => Some(e),
ProposalDecodingError::InputRetrieval(e) => Some(e),
ProposalDecodingError::MemoInvalid(e) => Some(e),
_ => None,
}
}
}
impl proposal::ProposedInput {
pub fn parse_txid(&self) -> Result<TxId, TryFromSliceError> {
Ok(TxId::from_bytes(self.txid[..].try_into()?))
@ -272,7 +339,7 @@ impl proposal::Proposal {
.balance()
.proposed_change()
.iter()
.map(|cv| match cv {
.map(|change| match change {
ChangeValue::Sapling { value, memo } => proposal::ChangeValue {
value: Some(proposal::change_value::Value::SaplingValue(
proposal::SaplingChange {
@ -342,7 +409,7 @@ impl proposal::Proposal {
wallet_db
.get_unspent_transparent_output(&outpoint)
.map_err(ProposalDecodingError::InputRetrieval)?
.ok_or_else(|| {
.ok_or({
ProposalDecodingError::InputNotFound(
txid,
PoolType::Transparent,
@ -352,7 +419,7 @@ impl proposal::Proposal {
})
.collect::<Result<Vec<_>, _>>()?;
let sapling_inputs = self.sapling_inputs.as_ref().and_then(|s_in| {
let sapling_inputs = self.sapling_inputs.as_ref().map(|s_in| {
s_in.inputs
.iter()
.map(|s_in| {
@ -364,7 +431,7 @@ impl proposal::Proposal {
.get_spendable_sapling_note(&txid, s_in.index)
.map_err(ProposalDecodingError::InputRetrieval)
.and_then(|opt| {
opt.ok_or_else(|| {
opt.ok_or({
ProposalDecodingError::InputNotFound(
txid,
PoolType::Shielded(ShieldedProtocol::Sapling),
@ -374,12 +441,13 @@ impl proposal::Proposal {
})
})
.collect::<Result<Vec<_>, _>>()
.map(|notes| {
NonEmpty::from_vec(notes).map(|notes| {
SaplingInputs::from_parts(s_in.anchor_height.into(), notes)
})
.and_then(|notes| {
NonEmpty::from_vec(notes)
.map(|notes| {
SaplingInputs::from_parts(s_in.anchor_height.into(), notes)
})
.ok_or(ProposalDecodingError::EmptySaplingInputs)
})
.transpose()
});
let proto_balance = self
@ -390,8 +458,11 @@ impl proposal::Proposal {
proto_balance
.proposed_change
.iter()
.filter_map(|cv| {
cv.value.as_ref().map(
.filter_map(|change| {
// An empty `value` field can be treated as though the whole
// `ChangeValue` was absent; this optionality is an artifact of
// protobuf encoding.
change.value.as_ref().map(
|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
match cv {
proposal::change_value::Value::SaplingValue(sc) => {

View File

@ -5,7 +5,10 @@
//!
//! The specification for ZIP 321 URIs may be found at <https://zips.z.cash/zip-0321>
use core::fmt::Debug;
use std::collections::HashMap;
use std::{
collections::HashMap,
fmt::{self, Display},
};
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
use nom::{
@ -45,6 +48,51 @@ pub enum Zip321Error {
ParseError(String),
}
impl Display for Zip321Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Zip321Error::InvalidBase64(err) => {
write!(f, "Memo value was not correctly base64 encoded: {:?}", err)
}
Zip321Error::MemoBytesError(err) => write!(
f,
"Memo exceeded maximum length or violated utf-8 encodiding restrictions: {:?}.",
err
),
Zip321Error::TooManyPayments(n) => write!(
f,
"Cannot create a Zcash transaction containing {} payments",
n
),
Zip321Error::DuplicateParameter(param, idx) => write!(
f,
"There is a duplicate {} parameter at index {}",
param.name(),
idx
),
Zip321Error::TransparentMemo(idx) => write!(
f,
"Payment {} is invalid: cannot send a memo to a transparent recipient address.",
idx
),
Zip321Error::RecipientMissing(idx) => {
write!(f, "Payment {} is missing its recipient address.", idx)
}
Zip321Error::ParseError(s) => write!(f, "Parse failure: {}", s),
}
}
}
impl std::error::Error for Zip321Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Zip321Error::InvalidBase64(err) => Some(err),
Zip321Error::MemoBytesError(err) => Some(err),
_ => None,
}
}
}
/// Converts a [`MemoBytes`] value to a ZIP 321 compatible base64-encoded string.
///
/// [`MemoBytes`]: zcash_primitives::memo::MemoBytes
@ -441,6 +489,20 @@ mod parse {
Other(String, String),
}
impl Param {
/// Returns the name of the parameter from which this value was parsed.
pub fn name(&self) -> String {
match self {
Param::Addr(_) => "address".to_owned(),
Param::Amount(_) => "amount".to_owned(),
Param::Memo(_) => "memo".to_owned(),
Param::Label(_) => "label".to_owned(),
Param::Message(_) => "message".to_owned(),
Param::Other(name, _) => name.clone(),
}
}
}
/// A [`Param`] value with its associated index.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexedParam {

View File

@ -1071,6 +1071,8 @@ pub(crate) fn input_selector(
GreedyInputSelector::new(change_strategy, DustOutputPolicy::default())
}
// Checks that a protobuf proposal serialized from the provided proposal value correctly parses to
// the same proposal value.
pub(crate) fn check_proposal_serialization_roundtrip(
db_data: &WalletDb<rusqlite::Connection, Network>,
proposal: &Proposal<StandardFeeRule, ReceivedNoteId>,