Skip to content

Commit

Permalink
Report proposal errors earlier where possible.
Browse files Browse the repository at this point in the history
Signed-off-by: Daira-Emma Hopwood <[email protected]>
  • Loading branch information
daira committed Jul 2, 2024
1 parent 8636daa commit 5669bb7
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 82 deletions.
32 changes: 26 additions & 6 deletions zcash_client_backend/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,25 @@ funds to those addresses. See [ZIP 320](https://zips.z.cash/zip-0320) for detail
- `WalletRead` has new `get_reserved_ephemeral_addresses` and
`get_transparent_address_metadata` methods.
- `WalletWrite` has a new `reserve_next_n_ephemeral_addresses` method.
- `error::Error` has a new `Address` variant.
- `error::Error` and `proposal::ProposalError`:
- `ProposalError` has new variants `SpendsChange`, `EphemeralOutputLeftUnspent`,
`PaysTexFromShielded`, `TooLarge`, `SpendsPaymentFromUnsupportedPool`,
`PaysUnsupportedPoolRecipient`, and `PaysUnsupportedTexRecipient`
(some of which are conditional on the "transparent-inputs" feature).
Of these, `SpendsChange` and `SpendsPaymentFromUnsupportedPool` are
currently reported when a `Proposal` or one of its `Step`s is constructed,
and the others are reported from `create_proposed_transactions`.
This may be changed in future to report these errors earlier; callers
should not rely on being able to construct `Proposal`s or `Step`s that
would result in these errors if a transaction were created from them.
- The `Error::ProposalNotSupported` variant now has an argument of type
`ProposalError` giving the more specific error. This will be used to
report the errors mentioned above that have "Unsupported" in their
names.
- The `Error::UnsupportedChangeType` variant has been removed since it
cannot occur (see `data_api::fees::TransactionBalance` below).
- `Error` has a new variant `Address`.
- `wallet::input_selection::InputSelectorError` has a new `Address` variant.
- `zcash_client_backend::proto::proposal::Proposal::{from_standard_proposal,
try_into_standard_proposal}` each no longer require a `consensus::Parameters`
argument.
- `zcash_client_backend::data_api::fees`
- When the "transparent-inputs" feature is enabled, `ChangeValue` can also
represent an ephemeral transparent output in a proposal. Accordingly, the
Expand All @@ -73,10 +87,16 @@ funds to those addresses. See [ZIP 320](https://zips.z.cash/zip-0320) for detail
outputs. Passing `&EphemeralParameters::NONE` will retain the previous
behaviour (and is necessary when the "transparent-inputs" feature is
not enabled).
- `TransactionBalance::new` now enforces that the change and ephemeral
outputs are for supported pools, and will return an error if they are
not. This makes `data_api::error::Error::UnsupportedChangeType` impossible,
so it has been removed as mentioned above.
- `zcash_client_backend::input_selection::GreedyInputSelectorError` has a
new variant `UnsupportedTexAddress`.
- `zcash_client_backend::proto::ProposalDecodingError` has a new variant
`InvalidEphemeralRecipient`.
- `zcash_client_backend::proto`:
- `ProposalDecodingError` has a new variant `InvalidEphemeralRecipient`.
- `proposal::Proposal::{from_standard_proposal, try_into_standard_proposal}`
each no longer require a `consensus::Parameters` argument.
- `zcash_client_backend::wallet::Recipient` variants have changed. Instead of
wrapping protocol-address types, the `External` and `InternalAccount` variants
now wrap a `zcash_address::ZcashAddress`. This simplifies the process of
Expand Down
49 changes: 25 additions & 24 deletions zcash_client_backend/src/data_api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ use zcash_primitives::transaction::{
use crate::address::UnifiedAddress;
use crate::data_api::wallet::input_selection::InputSelectorError;
use crate::proposal::ProposalError;
use crate::PoolType;

#[cfg(feature = "transparent-inputs")]
use zcash_primitives::legacy::TransparentAddress;
Expand All @@ -33,15 +32,17 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
/// An error in note selection
NoteSelection(SelectionError),

/// An error in transaction proposal construction
/// An error in transaction proposal construction. This indicates that the proposal
/// violated balance or structural constraints.
Proposal(ProposalError),

/// The proposal was structurally valid, but tried to do one of these unsupported things:
/// * spend a prior shielded output;
/// * pay to an output pool for which the corresponding feature is not enabled;
/// * pay to a TEX address if the "transparent-inputs" feature is not enabled;
/// or exceeded an implementation limit.
ProposalNotSupported,
/// A proposal was structurally valid, but not supported by the current feature or
/// chain configuration.
///
/// This is indicative of a programming error; a transaction proposal that presumes
/// support for the specified pool was decoded or executed using an application that
/// does not provide such support.
ProposalNotSupported(ProposalError),

/// No account could be found corresponding to a provided spending key.
KeyNotRecognized,
Expand All @@ -65,13 +66,6 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
/// It is forbidden to provide a memo when constructing a transparent output.
MemoForbidden,

/// Attempted to send change to an unsupported pool.
///
/// This is indicative of a programming error; execution of a transaction proposal that
/// presumes support for the specified pool was performed using an application that does not
/// provide such support.
UnsupportedChangeType(PoolType),

/// Attempted to create a spend to an unsupported Unified Address receiver
NoSupportedReceivers(Box<UnifiedAddress>),

Expand Down Expand Up @@ -119,12 +113,10 @@ where
Error::Proposal(e) => {
write!(f, "Input selection attempted to construct an invalid proposal: {}", e)
}
Error::ProposalNotSupported => write!(
f,
"The proposal was valid but tried to do something that is not supported \
(spend shielded outputs of prior transaction steps or use a feature that \
is not enabled), or exceeded an implementation limit.",
),
Error::ProposalNotSupported(e) => {

Check warning on line 116 in zcash_client_backend/src/data_api/error.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/error.rs#L116

Added line #L116 was not covered by tests
// The `ProposalError` message is complete in this context too.
write!(f, "{}", e)

Check warning on line 118 in zcash_client_backend/src/data_api/error.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/error.rs#L118

Added line #L118 was not covered by tests
}
Error::KeyNotRecognized => {
write!(
f,
Expand All @@ -145,7 +137,6 @@ where
Error::ScanRequired => write!(f, "Must scan blocks first"),
Error::Builder(e) => write!(f, "An error occurred building the transaction: {}", e),
Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."),
Error::UnsupportedChangeType(t) => write!(f, "Attempted to send change to an unsupported pool type: {}", t),
Error::NoSupportedReceivers(ua) => write!(
f,
"A recipient's unified address does not contain any receivers to which the wallet can send funds; required one of {}",
Expand Down Expand Up @@ -178,6 +169,7 @@ where
Error::CommitmentTree(e) => Some(e),
Error::NoteSelection(e) => Some(e),
Error::Proposal(e) => Some(e),
Error::ProposalNotSupported(e) => Some(e),

Check warning on line 172 in zcash_client_backend/src/data_api/error.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/error.rs#L172

Added line #L172 was not covered by tests
Error::Builder(e) => Some(e),
_ => None,
}
Expand All @@ -192,7 +184,15 @@ impl<DE, CE, SE, FE> From<builder::Error<FE>> for Error<DE, CE, SE, FE> {

impl<DE, CE, SE, FE> From<ProposalError> for Error<DE, CE, SE, FE> {
fn from(e: ProposalError) -> Self {
Error::Proposal(e)
match e {
// These errors concern feature support or unimplemented functionality.
ProposalError::SpendsPaymentFromUnsupportedPool(_)
| ProposalError::PaysUnsupportedPoolRecipient(_)
| ProposalError::PaysUnsupportedTexRecipient => Error::ProposalNotSupported(e),

Check warning on line 191 in zcash_client_backend/src/data_api/error.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/error.rs#L189-L191

Added lines #L189 - L191 were not covered by tests

// These errors concern balance or structural validity.
_ => Error::Proposal(e),
}
}
}

Expand All @@ -213,7 +213,8 @@ impl<DE, CE, SE, FE> From<InputSelectorError<DE, SE>> for Error<DE, CE, SE, FE>
match e {
InputSelectorError::DataSource(e) => Error::DataSource(e),
InputSelectorError::Selection(e) => Error::NoteSelection(e),
InputSelectorError::Proposal(e) => Error::Proposal(e),
// Choose `Error::Proposal` or `Error::ProposalNotSupported` as applicable.
InputSelectorError::Proposal(e) => e.into(),

Check warning on line 217 in zcash_client_backend/src/data_api/error.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/error.rs#L217

Added line #L217 was not covered by tests
InputSelectorError::InsufficientFunds {
available,
required,
Expand Down
59 changes: 24 additions & 35 deletions zcash_client_backend/src/data_api/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -645,9 +645,15 @@ where
.expect("proposal.steps is NonEmpty"))
}

// `unused_transparent_outputs` maps `StepOutput`s for transparent outputs
// that have not been consumed so far, to the corresponding pair of
// `TransparentAddress` and `Outpoint`.
/// Creates a transaction corresponding to a proposal step.
///
/// Since this is only called by `create_proposed_transactions` which takes
/// a fully validated `Proposal` as input, we may assume that the proposal,
/// including references to prior steps, is structurally valid.
///
/// `unused_transparent_outputs` maps `StepOutput`s for transparent outputs
/// that have not been consumed so far, to the corresponding pair of
/// `TransparentAddress` and `Outpoint`.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
Expand All @@ -671,40 +677,16 @@ where
ParamsT: consensus::Parameters + Clone,
FeeRuleT: FeeRule,
{
#[cfg(feature = "transparent-inputs")]
#[allow(unused_variables)]
let step_index = prior_step_results.len();

// We only support spending transparent payments or transparent ephemeral outputs from a
// prior step (when "transparent-inputs" is enabled).
// prior step (when "transparent-inputs" is enabled). We don't need to check that here
// because it is checked by construction in `proposal::Step`.
//
// TODO: Maybe support spending prior shielded outputs at some point? Doing so would require
// a higher-level approach in the wallet that waits for transactions with shielded outputs to
// be mined and only then attempts to perform the next step.
for input_ref in proposal_step.prior_step_inputs() {
let (prior_step, _) = prior_step_results
.get(input_ref.step_index())
.ok_or(ProposalError::ReferenceError(*input_ref))?;

#[allow(unused_variables)]
let output_pool = match input_ref.output_index() {
StepOutputIndex::Payment(i) => prior_step.payment_pools().get(&i).cloned(),
StepOutputIndex::Change(i) => match prior_step.balance().proposed_change().get(i) {
Some(change) if !change.is_ephemeral() => {
return Err(ProposalError::SpendsChange(*input_ref).into());
}
other => other.map(|change| change.output_pool()),
},
}
.ok_or(ProposalError::ReferenceError(*input_ref))?;

// Return an error on trying to spend a prior output that is not supported.
#[cfg(feature = "transparent-inputs")]
if output_pool != PoolType::TRANSPARENT {
return Err(Error::ProposalNotSupported);
}
#[cfg(not(feature = "transparent-inputs"))]
return Err(Error::ProposalNotSupported);
}

let account_id = wallet_db
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
Expand Down Expand Up @@ -1037,7 +1019,9 @@ where
Address::Unified(ua) => match output_pool {

Check warning on line 1019 in zcash_client_backend/src/data_api/wallet.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/wallet.rs#L1019

Added line #L1019 was not covered by tests
#[cfg(not(feature = "orchard"))]
PoolType::Shielded(ShieldedProtocol::Orchard) => {

Check warning on line 1021 in zcash_client_backend/src/data_api/wallet.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/wallet.rs#L1021

Added line #L1021 was not covered by tests
return Err(Error::ProposalNotSupported);
// TODO: check this in `Step::from_parts`. We cannot do so currently
// because we don't know the `network_type` there.
return Err(ProposalError::PaysUnsupportedPoolRecipient(*output_pool).into());

Check warning on line 1024 in zcash_client_backend/src/data_api/wallet.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/wallet.rs#L1024

Added line #L1024 was not covered by tests
}
#[cfg(feature = "orchard")]
PoolType::Shielded(ShieldedProtocol::Orchard) => {
Expand All @@ -1061,11 +1045,13 @@ where
}
#[cfg(not(feature = "transparent-inputs"))]
Address::Tex(_) => {
return Err(Error::ProposalNotSupported);
// TODO: check this in `Step::from_parts`.
return Err(ProposalError::PaysUnsupportedPoolRecipient(*output_pool).into());

Check warning on line 1049 in zcash_client_backend/src/data_api/wallet.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/wallet.rs#L1049

Added line #L1049 was not covered by tests
}
#[cfg(feature = "transparent-inputs")]
Address::Tex(data) => {
if has_shielded_inputs {
// TODO: check this in `Step::from_parts`.
return Err(ProposalError::PaysTexFromShielded.into());

Check warning on line 1055 in zcash_client_backend/src/data_api/wallet.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/wallet.rs#L1055

Added line #L1055 was not covered by tests
}
let to = TransparentAddress::PublicKeyHash(data);
Expand Down Expand Up @@ -1098,8 +1084,9 @@ where
))
}
PoolType::Shielded(ShieldedProtocol::Orchard) => {
// `TransactionBalance` enforces that change is for a supported pool.
#[cfg(not(feature = "orchard"))]
return Err(Error::UnsupportedChangeType(output_pool));
unreachable!();

#[cfg(feature = "orchard")]
{
Expand All @@ -1121,8 +1108,10 @@ where
}
}
PoolType::Transparent => {
// `ChangeValue` cannot be constructed with a transparent output pool
// if "transparent-inputs" is not enabled.
#[cfg(not(feature = "transparent-inputs"))]
return Err(Error::UnsupportedChangeType(output_pool));
unreachable!()
}
}
}
Expand All @@ -1142,7 +1131,7 @@ where
})
.collect();
if ephemeral_outputs.len() > i32::MAX as usize {
return Err(Error::ProposalNotSupported);
return Err(ProposalError::TooLarge("too many ephemeral outputs".to_owned()).into());

Check warning on line 1134 in zcash_client_backend/src/data_api/wallet.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/data_api/wallet.rs#L1134

Added line #L1134 was not covered by tests
}
let addresses_and_metadata = wallet_db
.reserve_next_n_ephemeral_addresses(
Expand Down
14 changes: 14 additions & 0 deletions zcash_client_backend/src/fees.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ pub struct TransactionBalance {

impl TransactionBalance {
/// Constructs a new balance from its constituent parts.
///
/// Returns `Err(())` if the proposed change is for an unsupported pool or
/// the total value overflows.
pub fn new(
proposed_change: Vec<ChangeValue>,
fee_required: NonNegativeAmount,
Expand All @@ -137,6 +140,17 @@ impl TransactionBalance {
.sum::<Option<NonNegativeAmount>>()
.ok_or(())?;

#[cfg(not(feature = "orchard"))]
if proposed_change
.iter()
.any(|cv| cv.output_pool() == PoolType::ORCHARD)
{
return Err(());

Check warning on line 148 in zcash_client_backend/src/fees.rs

View check run for this annotation

Codecov / codecov/patch

zcash_client_backend/src/fees.rs#L148

Added line #L148 was not covered by tests
}
// A `ChangeValue` in the transparent pool can only be ephemeral by
// construction, and only when "transparent-inputs" is enabled, so we
// do not need to check that.

Ok(Self {
proposed_change,
fee_required,
Expand Down
5 changes: 5 additions & 0 deletions zcash_client_backend/src/fees/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ where
let (change_pool, sapling_change, orchard_change) =
single_change_output_policy(&net_flows, fallback_change_pool);

#[cfg(not(feature = "orchard"))]
assert!(change_pool != ShieldedProtocol::Orchard);

// We don't create a fully-transparent transaction if a change memo is used.
let transparent = net_flows.is_transparent() && change_memo.is_none();

Expand Down Expand Up @@ -431,6 +434,8 @@ where
.map(ChangeValue::ephemeral_transparent),
);

// Any error here can only be overflow, because we checked that `change_pool`
// is supported and only constructed `change` to that pool.
TransactionBalance::new(change, fee).map_err(|_| overflow())
}

Expand Down
Loading

0 comments on commit 5669bb7

Please sign in to comment.