zcash_client_backend: Modify `Proposal` to make multi-step transactions representable.

This commit is contained in:
Kris Nuttycombe 2024-02-12 16:34:44 -07:00
parent 1db3109cb4
commit beeea7b44e
14 changed files with 781 additions and 312 deletions

1
Cargo.lock generated
View File

@ -3062,6 +3062,7 @@ dependencies = [
"incrementalmerkletree",
"jubjub",
"maybe-rayon",
"nonempty",
"orchard",
"proptest",
"prost",

View File

@ -47,6 +47,7 @@ and this library adheres to Rust's notion of
}`
- `WalletSummary::next_sapling_subtree_index`
- `wallet::propose_standard_transfer_to_address`
- `wallet::create_proposed_transactions`
- `wallet::input_selection::ShieldedInputs`
- `wallet::input_selection::ShieldingSelector` has been
factored out from the `InputSelector` trait to separate out transparent
@ -60,9 +61,10 @@ and this library adheres to Rust's notion of
- `wallet::TransparentAddressMetadata` (which replaces `zcash_keys::address::AddressMetadata`).
- `zcash_client_backend::zip321::TransactionRequest::total`
- `zcash_client_backend::zip321::parse::Param::name`
- `zcash_client_backend::proposal`
- `Proposal::{from_parts, shielded_inputs, payment_pools}`
- `zcash_client_backend::proto::`
- `zcash_client_backend::proposal`:
- `Proposal::{shielded_inputs, payment_pools, single_step, multi_step}`
- `Step`
- `zcash_client_backend::proto`:
- `PROPOSAL_SER_V1`
- `ProposalDecodingError`
- `proposal` module, for parsing and serializing transaction proposals.
@ -76,8 +78,8 @@ and this library adheres to Rust's notion of
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
proposal::{Proposal, SaplingInputs},
}`
- `zcash_client_backend::zip321
` `TransactionRequest::{total, from_indexed}`
- `zcash_client_backend::zip321`:
- `TransactionRequest::{total, from_indexed}`
- `parse::Param::name`
### Moved
@ -117,32 +119,44 @@ and this library adheres to Rust's notion of
- The `NoteMismatch` variant now wraps a `NoteId` instead of a
backend-specific note identifier. The related `NoteRef` type parameter has
been removed from `error::Error`.
- A new variant `UnsupportedPoolType` has been added.
- A new variant `NoSupportedReceivers` has been added.
- A new variant `NoSpendingKey` has been added.
- A new variant `Proposal` has been added.
- New variants have been added:
- `Error::UnsupportedPoolType`
- `Error::NoSupportedReceivers`
- `Error::NoSpendingKey`
- `Error::Proposal`
- `Error::ProposalNotSupported`
- Variant `ChildIndexOutOfRange` has been removed.
- `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
`TransactionBalance`.
- `wallet::create_proposed_transaction` now takes its `proposal` argument
by reference instead of as an owned value.
- `wallet::create_proposed_transaction` no longer takes a `min_confirmations`
argument. Instead, it uses the anchor height from its `proposal` argument.
- `wallet::create_spend_to_address` now takes an additional
`change_memo` argument.
internal "change" outputs. Also, it returns its result as a `NonEmpty<TxId>`
instead of a single `TxId`.
- `wallet::create_proposed_transaction` has been replaced by
`wallet::create_proposed_transactions`. Relative to the prior method,
the new method has the following changes:
- It 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 `TransactionBalance`.
- `wallet::create_proposed_transactions` takes its `proposal` argument
by reference instead of as an owned value.
- `wallet::create_proposed_transactions` no longer takes a `min_confirmations`
argument. Instead, it uses the anchor height from its `proposal` argument.
- `wallet::create_proposed_transactions` forces implementations to ignore
the database identifiers for its contained notes by universally quantifying
the `NoteRef` type parameter.
- It returns a `NonEmpty<TxId>` instead of a single `TxId` value.
- `wallet::create_spend_to_address` now takes an additional `change_memo`
argument. It also returns its result as a `NonEmpty<TxId>` instead of a
single `TxId`.
- `wallet::spend` returns its result as a `NonEmpty<TxId>` instead of a
single `TxId`.
- The error type of `wallet::create_spend_to_address` has been changed to use
`zcash_primitives::transaction::fees::zip317::FeeError` instead of
`zcash_primitives::transaction::components::amount::BalanceError`.
- The following methods now take `&impl SpendProver, &impl OutputProver`
instead of `impl TxProver`:
- `wallet::create_proposed_transaction`
- `wallet::create_proposed_transactions`
- `wallet::create_spend_to_address`
- `wallet::shield_transparent_funds`
- `wallet::spend`
@ -162,8 +176,8 @@ and this library adheres to Rust's notion of
`data_api::InputSource` must expose.
- Changes to the `WalletRead` trait:
- `get_checkpoint_depth` has been removed without replacement. This
is no longer needed given the change to use the stored anchor height for transaction
proposal execution.
is no longer needed given the change to use the stored anchor height for
transaction proposal execution.
- `is_valid_account_extfvk` has been removed; it was unused in
the ECC mobile wallet SDKs and has been superseded by `get_account_for_ufvk`.
- `get_spendable_sapling_notes`, `select_spendable_sapling_notes`, and
@ -179,9 +193,6 @@ and this library adheres to Rust's notion of
- `wallet::{propose_shielding, shield_transparent_funds}` now takes their
`min_confirmations` arguments as `u32` rather than a `NonZeroU32` to permit
implmentations to enable zero-conf shielding.
- `wallet::create_proposed_transaction` now forces implementations to ignore
the database identifiers for its contained notes by universally quantifying
the `NoteRef` type parameter.
- `wallet::input_selection::GreedyInputSelector` now has relaxed requirements
for its `InputSource` associated type.
@ -190,6 +201,11 @@ and this library adheres to Rust's notion of
- `Proposal::min_anchor_height` has been removed in favor of storing this
value in `SaplingInputs`.
- `Proposal::sapling_inputs` has been replaced by `Proposal::shielded_inputs`
- In addition to having been moved to the `zcash_client_backend::proposal`
module, the `Proposal` type has been substantially modified in order to make
it possible to represent multi-step transactions, such as a deshielding
transaction followed by a zero-conf transfer as required by ZIP 320. Individual
transaction proposals are now represented by the `proposal::Step` type.
- `zcash_client_backend::fees`:
- `ChangeStrategy::compute_balance` arguments have changed.
@ -245,6 +261,7 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::data_api::ScannedBlock::from_parts` has been made crate-private.
- `zcash_client_backend::data_api::ScannedBlock::into_sapling_commitments` has been
replaced by `into_commitments` which returns a `ScannedBlockCommitments` value.
- `zcash_client_backend::data_api::wallet::create_proposed_transaction`
## [0.10.0] - 2023-09-25

View File

@ -5,34 +5,41 @@
syntax = "proto3";
package cash.z.wallet.sdk.ffi;
// A data structure that describes the inputs to be consumed and outputs to
// be produced in a proposed transaction.
// A data structure that describes the a series of transactions to be created.
message Proposal {
// The version of this serialization format.
uint32 protoVersion = 1;
// ZIP 321 serialized transaction request
string transactionRequest = 2;
// The vector of selected payment index / output pool mappings. Payment index
// 0 corresponds to the payment with no explicit index.
repeated PaymentOutputPool paymentOutputPools = 3;
// The anchor height to be used in creating the transaction, if any.
// Setting the anchor height to zero will disallow the use of any shielded
// inputs.
uint32 anchorHeight = 4;
// The inputs to be used in creating the transaction.
repeated ProposedInput inputs = 5;
// The total value, fee value, and change outputs of the proposed
// transaction
TransactionBalance balance = 6;
// The fee rule used in constructing this proposal
FeeRule feeRule = 7;
FeeRule feeRule = 2;
// The target height for which the proposal was constructed
//
// The chain must contain at least this many blocks in order for the proposal to
// be executed.
uint32 minTargetHeight = 8;
// A flag indicating whether the proposal is for a shielding transaction,
uint32 minTargetHeight = 3;
// The series of transactions to be created.
repeated ProposalStep steps = 4;
}
// A data structure that describes the inputs to be consumed and outputs to
// be produced in a proposed transaction.
message ProposalStep {
// ZIP 321 serialized transaction request
string transactionRequest = 1;
// The vector of selected payment index / output pool mappings. Payment index
// 0 corresponds to the payment with no explicit index.
repeated PaymentOutputPool paymentOutputPools = 2;
// The anchor height to be used in creating the transaction, if any.
// Setting the anchor height to zero will disallow the use of any shielded
// inputs.
uint32 anchorHeight = 3;
// The inputs to be used in creating the transaction.
repeated ProposedInput inputs = 4;
// The total value, fee value, and change outputs of the proposed
// transaction
TransactionBalance balance = 5;
// A flag indicating whether the step is for a shielding transaction,
// used for determining which OVK to select for wallet-internal outputs.
bool isShielding = 9;
bool isShielding = 6;
}
enum ValuePool {
@ -57,14 +64,37 @@ message PaymentOutputPool {
ValuePool valuePool = 2;
}
// The unique identifier and value for each proposed input.
message ProposedInput {
// The unique identifier and value for each proposed input that does not
// require a back-reference to a prior step of the proposal.
message ReceivedOutput {
bytes txid = 1;
ValuePool valuePool = 2;
uint32 index = 3;
uint64 value = 4;
}
// A reference a payment in a prior step of the proposal. This payment must
// belong to the wallet.
message PriorStepOutput {
uint32 stepIndex = 1;
uint32 paymentIndex = 2;
}
// A reference a change output from a prior step of the proposal.
message PriorStepChange {
uint32 stepIndex = 1;
uint32 changeIndex = 2;
}
// The unique identifier and value for an input to be used in the transaction.
message ProposedInput {
oneof value {
ReceivedOutput receivedOutput = 1;
PriorStepOutput priorStepOutput = 2;
PriorStepChange priorStepChange = 3;
}
}
// The fee rule used in constructing a Proposal
enum FeeRule {
// Protobuf requires that enums have a zero discriminant as the default
@ -82,15 +112,21 @@ enum FeeRule {
// The proposed change outputs and fee value.
message TransactionBalance {
// A list of change output values.
repeated ChangeValue proposedChange = 1;
// The fee to be paid by the proposed transaction, in zatoshis.
uint64 feeRequired = 2;
}
// A proposed change output. If the transparent value pool is selected,
// the `memo` field must be null.
message ChangeValue {
// The value of a change output to be created, in zatoshis.
uint64 value = 1;
// The value pool in which the change output should be created.
ValuePool valuePool = 2;
// The optional memo that should be associated with the newly created change output.
// Memos must not be present for transparent change outputs.
MemoBytes memo = 3;
}

View File

@ -37,6 +37,10 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
/// An error in transaction proposal construction
Proposal(ProposalError),
/// The proposal was structurally valid, but spending shielded outputs of prior multi-step
/// transaction steps is not yet supported.
ProposalNotSupported,
/// No account could be found corresponding to a provided spending key.
KeyNotRecognized,
@ -107,6 +111,12 @@ where
Error::Proposal(e) => {
write!(f, "Input selection attempted to construct an invalid proposal: {}", e)
}
Error::ProposalNotSupported => {
write!(
f,
"The proposal was valid, but spending shielded outputs of prior transaction steps is not yet supported."
)
}
Error::KeyNotRecognized => {
write!(
f,

View File

@ -1,15 +1,16 @@
use std::num::NonZeroU32;
use nonempty::NonEmpty;
use rand_core::OsRng;
use sapling::{
note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey},
prover::{OutputProver, SpendProver},
};
use zcash_primitives::{
consensus::{self, NetworkUpgrade},
consensus::{self, BlockHeight, NetworkUpgrade},
memo::MemoBytes,
transaction::{
builder::{BuildConfig, Builder},
builder::{BuildConfig, BuildResult, Builder},
components::amount::{Amount, NonNegativeAmount},
fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule},
Transaction, TxId,
@ -26,7 +27,7 @@ use crate::{
decrypt_transaction,
fees::{self, DustOutputPolicy},
keys::UnifiedSpendingKey,
proposal::Proposal,
proposal::{self, Proposal},
wallet::{Note, OvkPolicy, Recipient},
zip321::{self, Payment},
PoolType, ShieldedProtocol,
@ -207,7 +208,7 @@ pub fn create_spend_to_address<DbT, ParamsT>(
min_confirmations: NonZeroU32,
change_memo: Option<MemoBytes>,
) -> Result<
TxId,
NonEmpty<TxId>,
Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
@ -238,7 +239,7 @@ where
change_memo,
)?;
create_proposed_transaction(
create_proposed_transactions(
wallet_db,
params,
spend_prover,
@ -316,7 +317,7 @@ pub fn spend<DbT, ParamsT, InputsT>(
ovk_policy: OvkPolicy,
min_confirmations: NonZeroU32,
) -> Result<
TxId,
NonEmpty<TxId>,
Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
@ -344,7 +345,7 @@ where
min_confirmations,
)?;
create_proposed_transaction(
create_proposed_transactions(
wallet_db,
params,
spend_prover,
@ -521,7 +522,7 @@ where
/// to fall back to the transparent receiver until full Orchard support is implemented.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
pub fn create_proposed_transactions<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_prover: &impl SpendProver,
@ -530,7 +531,7 @@ pub fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
ovk_policy: OvkPolicy,
proposal: &Proposal<FeeRuleT, N>,
) -> Result<
TxId,
NonEmpty<TxId>,
Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
@ -543,6 +544,83 @@ where
ParamsT: consensus::Parameters + Clone,
FeeRuleT: FeeRule,
{
let mut step_results = Vec::with_capacity(proposal.steps().len());
for step in proposal.steps() {
let step_result = create_proposed_transaction(
wallet_db,
params,
spend_prover,
output_prover,
usk,
ovk_policy.clone(),
proposal.fee_rule(),
proposal.min_target_height(),
&step_results,
step,
)?;
step_results.push((step, step_result));
}
Ok(NonEmpty::from_vec(
step_results
.iter()
.map(|(_, r)| r.transaction().txid())
.collect(),
)
.expect("proposal.steps is NonEmpty"))
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
wallet_db: &mut DbT,
params: &ParamsT,
spend_prover: &impl SpendProver,
output_prover: &impl OutputProver,
usk: &UnifiedSpendingKey,
ovk_policy: OvkPolicy,
fee_rule: &FeeRuleT,
min_target_height: BlockHeight,
prior_step_results: &[(&proposal::Step<N>, BuildResult)],
proposal_step: &proposal::Step<N>,
) -> Result<
BuildResult,
Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
InputsErrT,
FeeRuleT::Error,
>,
>
where
DbT: WalletWrite + WalletCommitmentTrees,
ParamsT: consensus::Parameters + Clone,
FeeRuleT: FeeRule,
{
// TODO: Spending shielded outputs of prior multi-step transaction steps is not yet
// supported. Maybe support this 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.
if proposal_step.prior_step_inputs().iter().any(|s_ref| {
prior_step_results.len() <= s_ref.step_index()
|| match s_ref.output_index() {
proposal::StepOutputIndex::Payment(i) => prior_step_results[s_ref.step_index()]
.0
.payment_pools()
.get(&i)
.iter()
.all(|pool| matches!(pool, PoolType::Shielded(_))),
proposal::StepOutputIndex::Change(_) => {
// Only shielded change is supported by zcash_client_backend, so multi-step
// transactions cannot yet spend prior transactions' change outputs.
true
}
}
}) {
return Err(Error::ProposalNotSupported);
}
let account = wallet_db
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
.map_err(Error::DataSource)?
@ -559,7 +637,7 @@ where
let internal_ovk = || {
#[cfg(feature = "transparent-inputs")]
return if proposal.is_shielding() {
return if proposal_step.is_shielding() {
Some(OutgoingViewingKey(
usk.transparent()
.to_account_pubkey()
@ -574,7 +652,7 @@ where
Some(dfvk.to_ovk(Scope::Internal))
};
let (sapling_anchor, sapling_inputs) = proposal.shielded_inputs().map_or_else(
let (sapling_anchor, sapling_inputs) = proposal_step.shielded_inputs().map_or_else(
|| Ok((sapling::Anchor::empty_tree(), vec![])),
|inputs| {
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {
@ -619,7 +697,7 @@ where
// are no possible transparent inputs, so we ignore those
let mut builder = Builder::new(
params.clone(),
proposal.min_target_height(),
min_target_height,
BuildConfig::Standard {
sapling_anchor: Some(sapling_anchor),
orchard_anchor: None,
@ -637,7 +715,7 @@ where
.map_err(Error::DataSource)?;
let mut utxos: Vec<WalletTransparentOutput> = vec![];
for utxo in proposal.transparent_inputs() {
for utxo in proposal_step.transparent_inputs() {
utxos.push(utxo.clone());
let address_metadata = known_addrs
@ -662,7 +740,7 @@ where
let mut sapling_output_meta = vec![];
let mut transparent_output_meta = vec![];
for payment in proposal.transaction_request().payments().values() {
for payment in proposal_step.transaction_request().payments().values() {
match &payment.recipient_address {
Address::Unified(ua) => {
let memo = payment
@ -716,7 +794,7 @@ where
}
}
for change_value in proposal.balance().proposed_change() {
for change_value in proposal_step.balance().proposed_change() {
let memo = change_value
.memo()
.map_or_else(MemoBytes::empty, |m| m.clone());
@ -751,7 +829,7 @@ where
}
// Build the transaction with the specified fee rule
let build_result = builder.build(OsRng, spend_prover, output_prover, proposal.fee_rule())?;
let build_result = builder.build(OsRng, spend_prover, output_prover, fee_rule)?;
let internal_ivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal));
let sapling_outputs =
@ -776,10 +854,7 @@ where
try_sapling_note_decryption(
&internal_ivk,
&bundle.shielded_outputs()[output_index],
consensus::sapling_zip212_enforcement(
params,
proposal.min_target_height(),
),
consensus::sapling_zip212_enforcement(params, min_target_height),
)
.map(|(note, _, _)| (account, note))
})
@ -819,13 +894,13 @@ where
created: time::OffsetDateTime::now_utc(),
account,
outputs: sapling_outputs.chain(transparent_outputs).collect(),
fee_amount: Amount::from(proposal.balance().fee_required()),
fee_amount: Amount::from(proposal_step.balance().fee_required()),
#[cfg(feature = "transparent-inputs")]
utxos_spent: utxos.iter().map(|utxo| utxo.outpoint().clone()).collect(),
})
.map_err(Error::DataSource)?;
Ok(build_result.transaction().txid())
Ok(build_result)
}
/// Constructs a transaction that consumes available transparent UTXOs belonging to
@ -875,7 +950,7 @@ pub fn shield_transparent_funds<DbT, ParamsT, InputsT>(
from_addrs: &[TransparentAddress],
min_confirmations: u32,
) -> Result<
TxId,
NonEmpty<TxId>,
Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
@ -897,7 +972,7 @@ where
min_confirmations,
)?;
create_proposed_transaction(
create_proposed_transactions(
wallet_db,
params,
spend_prover,

View File

@ -10,7 +10,6 @@ use std::{
use nonempty::NonEmpty;
use zcash_primitives::{
consensus::{self, BlockHeight},
legacy::TransparentAddress,
transaction::{
components::{
amount::{BalanceError, NonNegativeAmount},
@ -31,11 +30,12 @@ use crate::{
PoolType, ShieldedProtocol,
};
#[cfg(any(feature = "transparent-inputs"))]
use std::convert::Infallible;
#[cfg(feature = "transparent-inputs")]
use {std::collections::BTreeSet, zcash_primitives::transaction::components::OutPoint};
use {
std::collections::BTreeSet, std::convert::Infallible,
zcash_primitives::legacy::TransparentAddress,
zcash_primitives::transaction::components::OutPoint,
};
#[cfg(feature = "orchard")]
use crate::fees::orchard as orchard_fees;
@ -433,7 +433,7 @@ where
match balance {
Ok(balance) => {
return Proposal::from_parts(
return Proposal::single_step(
transaction_request,
payment_pools,
vec![],
@ -582,7 +582,7 @@ where
};
if balance.total() >= shielding_threshold {
Proposal::from_parts(
Proposal::single_step(
TransactionRequest::empty(),
BTreeMap::new(),
transparent_inputs,

View File

@ -15,7 +15,7 @@ use crate::{
PoolType,
};
/// Errors that can occur in construction of a [`Proposal`].
/// Errors that can occur in construction of a [`Step`].
#[derive(Debug, Clone)]
pub enum ProposalError {
/// The total output value of the transaction request is not a valid Zcash amount.
@ -35,6 +35,8 @@ pub enum ProposalError {
/// * There provided transaction request is empty; i.e. the only output values specified
/// are change and fee amounts.
ShieldingInvalid,
/// A reference to the output of a prior step is invalid.
ReferenceError(StepOutput),
}
impl Display for ProposalError {
@ -61,6 +63,7 @@ impl Display for ProposalError {
f,
"The proposal violates the rules for a shielding transaction."
),
ProposalError::ReferenceError(r) => write!(f, "No prior step output found for {:?}", r),
}
}
}
@ -98,21 +101,165 @@ impl<NoteRef> ShieldedInputs<NoteRef> {
}
}
/// The inputs to be consumed and outputs to be produced in a proposed transaction.
/// A proposal for a series of transactions to be created.
///
/// Each step of the proposal represents a separate transaction to be created. At present, only
/// transparent outputs of earlier steps may be spent in later steps; the ability to chain shielded
/// transaction steps may be added in a future update.
#[derive(Clone, PartialEq, Eq)]
pub struct Proposal<FeeRuleT, NoteRef> {
fee_rule: FeeRuleT,
min_target_height: BlockHeight,
steps: NonEmpty<Step<NoteRef>>,
}
impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
/// Constructs a validated multi-step [`Proposal`].
///
/// This operation validates the proposal for agreement between outputs and inputs
/// in the case of multi-step proposals, and ensures that no double-spends are being
/// proposed.
///
/// Parameters:
/// * `fee_rule`: The fee rule observed by the proposed transaction.
/// * `min_target_height`: The minimum block height at which the transaction may be created.
/// * `steps`: A vector of steps that make up the proposal.
pub fn multi_step(
fee_rule: FeeRuleT,
min_target_height: BlockHeight,
steps: NonEmpty<Step<NoteRef>>,
) -> Result<Self, ProposalError> {
// TODO: actually perform the validation described in the documentation.
Ok(Self {
fee_rule,
min_target_height,
steps,
})
}
/// Constructs a validated [`Proposal`] having only a single step from its constituent parts.
///
/// This operation validates the proposal for balance consistency and agreement between
/// the `is_shielding` flag and the structure of the proposal.
///
/// Parameters:
/// * `transaction_request`: The ZIP 321 transaction request describing the payments to be
/// made.
/// * `payment_pools`: A map from payment index to pool type.
/// * `transparent_inputs`: The set of previous transparent outputs to be spent.
/// * `shielded_inputs`: The sets of previous shielded outputs to be spent.
/// * `balance`: The change outputs to be added the transaction and the fee to be paid.
/// * `fee_rule`: The fee rule observed by the proposed transaction.
/// * `min_target_height`: The minimum block height at which the transaction may be created.
/// * `is_shielding`: A flag that identifies whether this is a wallet-internal shielding
/// transaction.
#[allow(clippy::too_many_arguments)]
pub fn single_step(
transaction_request: TransactionRequest,
payment_pools: BTreeMap<usize, PoolType>,
transparent_inputs: Vec<WalletTransparentOutput>,
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
balance: TransactionBalance,
fee_rule: FeeRuleT,
min_target_height: BlockHeight,
is_shielding: bool,
) -> Result<Self, ProposalError> {
Ok(Self {
fee_rule,
min_target_height,
steps: NonEmpty::singleton(Step::from_parts(
&[],
transaction_request,
payment_pools,
transparent_inputs,
shielded_inputs,
vec![],
balance,
is_shielding,
)?),
})
}
/// Returns the fee rule to be used by the transaction builder.
pub fn fee_rule(&self) -> &FeeRuleT {
&self.fee_rule
}
/// Returns the target height for which the proposal was prepared.
///
/// The chain must contain at least this many blocks in order for the proposal to
/// be executed.
pub fn min_target_height(&self) -> BlockHeight {
self.min_target_height
}
/// Returns the steps of the proposal. Each step corresponds to an independent transaction to
/// be generated as a result of this proposal.
pub fn steps(&self) -> &NonEmpty<Step<NoteRef>> {
&self.steps
}
}
impl<FeeRuleT: Debug, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Proposal")
.field("fee_rule", &self.fee_rule)
.field("min_target_height", &self.min_target_height)
.field("steps", &self.steps)
.finish()
}
}
/// A reference to either a payment or change output within a step.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepOutputIndex {
Payment(usize),
Change(usize),
}
/// A reference to the output of a step in a proposal.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StepOutput {
step_index: usize,
output_index: StepOutputIndex,
}
impl StepOutput {
/// Constructs a new [`StepOutput`] from its constituent parts.
pub fn new(step_index: usize, output_index: StepOutputIndex) -> Self {
Self {
step_index,
output_index,
}
}
/// Returns the step index to which this reference refers.
pub fn step_index(&self) -> usize {
self.step_index
}
/// Returns the identifier for the payment or change output within
/// the referenced step.
pub fn output_index(&self) -> StepOutputIndex {
self.output_index
}
}
/// The inputs to be consumed and outputs to be produced in a proposed transaction.
#[derive(Clone, PartialEq, Eq)]
pub struct Step<NoteRef> {
transaction_request: TransactionRequest,
payment_pools: BTreeMap<usize, PoolType>,
transparent_inputs: Vec<WalletTransparentOutput>,
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
prior_step_inputs: Vec<StepOutput>,
balance: TransactionBalance,
fee_rule: FeeRuleT,
min_target_height: BlockHeight,
is_shielding: bool,
}
impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
/// Constructs a validated [`Proposal`] from its constituent parts.
impl<NoteRef> Step<NoteRef> {
/// Constructs a validated [`Step`] from its constituent parts.
///
/// This operation validates the proposal for balance consistency and agreement between
/// the `is_shielding` flag and the structure of the proposal.
@ -124,19 +271,17 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
/// * `transparent_inputs`: The set of previous transparent outputs to be spent.
/// * `shielded_inputs`: The sets of previous shielded outputs to be spent.
/// * `balance`: The change outputs to be added the transaction and the fee to be paid.
/// * `fee_rule`: The fee rule observed by the proposed transaction.
/// * `min_target_height`: The minimum block height at which the transaction may be created.
/// * `is_shielding`: A flag that identifies whether this is a wallet-internal shielding
/// transaction.
#[allow(clippy::too_many_arguments)]
pub fn from_parts(
prior_steps: &[Step<NoteRef>],
transaction_request: TransactionRequest,
payment_pools: BTreeMap<usize, PoolType>,
transparent_inputs: Vec<WalletTransparentOutput>,
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
prior_step_inputs: Vec<StepOutput>,
balance: TransactionBalance,
fee_rule: FeeRuleT,
min_target_height: BlockHeight,
is_shielding: bool,
) -> Result<Self, ProposalError> {
let transparent_input_total = transparent_inputs
@ -145,14 +290,43 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
(acc? + a).ok_or(ProposalError::Overflow)
})?;
let shielded_input_total = shielded_inputs
.iter()
.flat_map(|s_in| s_in.notes().iter())
.map(|out| out.note().value())
.fold(Some(NonNegativeAmount::ZERO), |acc, a| (acc? + a))
.ok_or(ProposalError::Overflow)?;
let input_total =
(transparent_input_total + shielded_input_total).ok_or(ProposalError::Overflow)?;
let prior_step_input_total = prior_step_inputs
.iter()
.map(|s_ref| {
let step = prior_steps
.get(s_ref.step_index)
.ok_or(ProposalError::ReferenceError(*s_ref))?;
Ok(match s_ref.output_index {
StepOutputIndex::Payment(i) => {
step.transaction_request
.payments()
.get(&i)
.ok_or(ProposalError::ReferenceError(*s_ref))?
.amount
}
StepOutputIndex::Change(i) => step
.balance
.proposed_change()
.get(i)
.ok_or(ProposalError::ReferenceError(*s_ref))?
.value(),
})
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.fold(Some(NonNegativeAmount::ZERO), |acc, a| (acc? + a))
.ok_or(ProposalError::Overflow)?;
let input_total = (transparent_input_total + shielded_input_total + prior_step_input_total)
.ok_or(ProposalError::Overflow)?;
let request_total = transaction_request
.total()
@ -173,9 +347,8 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
payment_pools,
transparent_inputs,
shielded_inputs,
prior_step_inputs,
balance,
fee_rule,
min_target_height,
is_shielding,
})
} else {
@ -203,21 +376,15 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
pub fn shielded_inputs(&self) -> Option<&ShieldedInputs<NoteRef>> {
self.shielded_inputs.as_ref()
}
/// Returns the inputs that should be obtained from the outputs of the transaction
/// created to satisfy a previous step of the proposal.
pub fn prior_step_inputs(&self) -> &[StepOutput] {
self.prior_step_inputs.as_ref()
}
/// Returns the change outputs to be added to the transaction and the fee to be paid.
pub fn balance(&self) -> &TransactionBalance {
&self.balance
}
/// Returns the fee rule to be used by the transaction builder.
pub fn fee_rule(&self) -> &FeeRuleT {
&self.fee_rule
}
/// Returns the target height for which the proposal was prepared.
///
/// The chain must contain at least this many blocks in order for the proposal to
/// be executed.
pub fn min_target_height(&self) -> BlockHeight {
self.min_target_height
}
/// Returns a flag indicating whether or not the proposed transaction
/// is exclusively wallet-internal (if it does not involve any external
/// recipients).
@ -226,9 +393,9 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
}
}
impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
impl<NoteRef> Debug for Step<NoteRef> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Proposal")
f.debug_struct("Step")
.field("transaction_request", &self.transaction_request)
.field("transparent_inputs", &self.transparent_inputs)
.field(
@ -240,8 +407,6 @@ impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
&self.shielded_inputs().map(|i| i.anchor_height),
)
.field("balance", &self.balance)
//.field("fee_rule", &self.fee_rule)
.field("min_target_height", &self.min_target_height)
.field("is_shielding", &self.is_shielding)
.finish_non_exhaustive()
}

View File

@ -24,7 +24,7 @@ use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
use crate::{
data_api::InputSource,
fees::{ChangeValue, TransactionBalance},
proposal::{Proposal, ProposalError, ShieldedInputs},
proposal::{Proposal, ProposalError, ShieldedInputs, Step, StepOutput, StepOutputIndex},
zip321::{TransactionRequest, Zip321Error},
PoolType, ShieldedProtocol,
};
@ -216,8 +216,12 @@ pub const PROPOSAL_SER_V1: u32 = 1;
/// representation.
#[derive(Debug, Clone)]
pub enum ProposalDecodingError<DbError> {
/// The encoded proposal contained no steps
NoSteps,
/// The ZIP 321 transaction request URI was invalid.
Zip321(Zip321Error),
/// A proposed input was null.
NullInput(usize),
/// A transaction identifier string did not decode to a valid transaction ID.
TxIdInvalid(TryFromSliceError),
/// An invalid value pool identifier was encountered.
@ -252,7 +256,11 @@ 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::NoSteps => write!(f, "The proposal had no steps."),
ProposalDecodingError::Zip321(err) => write!(f, "Transaction request invalid: {}", err),
ProposalDecodingError::NullInput(i) => {
write!(f, "Proposed input was null at index {}", i)
}
ProposalDecodingError::TxIdInvalid(err) => {
write!(f, "Invalid transaction id: {:?}", err)
}
@ -317,7 +325,7 @@ fn pool_type<T>(pool_id: i32) -> Result<PoolType, ProposalDecodingError<T>> {
}
}
impl proposal::ProposedInput {
impl proposal::ReceivedOutput {
pub fn parse_txid(&self) -> Result<TxId, TryFromSliceError> {
Ok(TxId::from_bytes(self.txid[..].try_into()?))
}
@ -359,64 +367,112 @@ impl proposal::Proposal {
params: &P,
value: &Proposal<StandardFeeRule, NoteRef>,
) -> Self {
let transaction_request = value.transaction_request().to_uri(params);
let anchor_height = value
.shielded_inputs()
.map_or_else(|| 0, |i| u32::from(i.anchor_height()));
let inputs = value
.transparent_inputs()
use proposal::proposed_input;
use proposal::{PriorStepChange, PriorStepOutput, ReceivedOutput};
let steps = value
.steps()
.iter()
.map(|utxo| proposal::ProposedInput {
txid: utxo.outpoint().hash().to_vec(),
value_pool: proposal::ValuePool::Transparent.into(),
index: utxo.outpoint().n(),
value: utxo.txout().value.into(),
})
.chain(value.shielded_inputs().iter().flat_map(|s_in| {
s_in.notes().iter().map(|rec_note| proposal::ProposedInput {
txid: rec_note.txid().as_ref().to_vec(),
value_pool: proposal::ValuePool::from(rec_note.note().protocol()).into(),
index: rec_note.output_index().into(),
value: rec_note.note().value().into(),
})
}))
.collect();
.map(|step| {
let transaction_request = step.transaction_request().to_uri(params);
let payment_output_pools = value
.payment_pools()
.iter()
.map(|(idx, pool_type)| proposal::PaymentOutputPool {
payment_index: u32::try_from(*idx).expect("Payment index fits into a u32"),
value_pool: proposal::ValuePool::from(*pool_type).into(),
let anchor_height = step
.shielded_inputs()
.map_or_else(|| 0, |i| u32::from(i.anchor_height()));
let inputs = step
.transparent_inputs()
.iter()
.map(|utxo| proposal::ProposedInput {
value: Some(proposed_input::Value::ReceivedOutput(ReceivedOutput {
txid: utxo.outpoint().hash().to_vec(),
value_pool: proposal::ValuePool::Transparent.into(),
index: utxo.outpoint().n(),
value: utxo.txout().value.into(),
})),
})
.chain(step.shielded_inputs().iter().flat_map(|s_in| {
s_in.notes().iter().map(|rec_note| proposal::ProposedInput {
value: Some(proposed_input::Value::ReceivedOutput(ReceivedOutput {
txid: rec_note.txid().as_ref().to_vec(),
value_pool: proposal::ValuePool::from(rec_note.note().protocol())
.into(),
index: rec_note.output_index().into(),
value: rec_note.note().value().into(),
})),
})
}))
.chain(step.prior_step_inputs().iter().map(|p_in| {
match p_in.output_index() {
StepOutputIndex::Payment(i) => proposal::ProposedInput {
value: Some(proposed_input::Value::PriorStepOutput(
PriorStepOutput {
step_index: p_in
.step_index()
.try_into()
.expect("Step index fits into a u32"),
payment_index: i
.try_into()
.expect("Payment index fits into a u32"),
},
)),
},
StepOutputIndex::Change(i) => proposal::ProposedInput {
value: Some(proposed_input::Value::PriorStepChange(
PriorStepChange {
step_index: p_in
.step_index()
.try_into()
.expect("Step index fits into a u32"),
change_index: i
.try_into()
.expect("Payment index fits into a u32"),
},
)),
},
}
}))
.collect();
let payment_output_pools = step
.payment_pools()
.iter()
.map(|(idx, pool_type)| proposal::PaymentOutputPool {
payment_index: u32::try_from(*idx).expect("Payment index fits into a u32"),
value_pool: proposal::ValuePool::from(*pool_type).into(),
})
.collect();
let balance = Some(proposal::TransactionBalance {
proposed_change: step
.balance()
.proposed_change()
.iter()
.map(|change| proposal::ChangeValue {
value: change.value().into(),
value_pool: proposal::ValuePool::from(change.output_pool()).into(),
memo: change.memo().map(|memo_bytes| proposal::MemoBytes {
value: memo_bytes.as_slice().to_vec(),
}),
})
.collect(),
fee_required: step.balance().fee_required().into(),
});
#[allow(deprecated)]
proposal::ProposalStep {
transaction_request,
payment_output_pools,
anchor_height,
inputs,
balance,
is_shielding: step.is_shielding(),
}
})
.collect();
let balance = Some(proposal::TransactionBalance {
proposed_change: value
.balance()
.proposed_change()
.iter()
.map(|change| proposal::ChangeValue {
value: change.value().into(),
value_pool: proposal::ValuePool::from(change.output_pool()).into(),
memo: change.memo().map(|memo_bytes| proposal::MemoBytes {
value: memo_bytes.as_slice().to_vec(),
}),
})
.collect(),
fee_required: value.balance().fee_required().into(),
});
#[allow(deprecated)]
proposal::Proposal {
proto_version: PROPOSAL_SER_V1,
transaction_request,
payment_output_pools,
anchor_height,
inputs,
balance,
fee_rule: match value.fee_rule() {
StandardFeeRule::PreZip313 => proposal::FeeRule::PreZip313,
StandardFeeRule::Zip313 => proposal::FeeRule::Zip313,
@ -424,7 +480,7 @@ impl proposal::Proposal {
}
.into(),
min_target_height: value.min_target_height().into(),
is_shielding: value.is_shielding(),
steps,
}
}
@ -438,6 +494,7 @@ impl proposal::Proposal {
where
DbT: InputSource<Error = DbError>,
{
use self::proposal::proposed_input::Value::*;
match self.proto_version {
PROPOSAL_SER_V1 => {
#[allow(deprecated)]
@ -450,116 +507,166 @@ impl proposal::Proposal {
}
};
let transaction_request =
TransactionRequest::from_uri(params, &self.transaction_request)?;
let mut steps = Vec::with_capacity(self.steps.len());
for step in &self.steps {
let transaction_request =
TransactionRequest::from_uri(params, &step.transaction_request)?;
let payment_pools = self
.payment_output_pools
.iter()
.map(|pop| {
Ok((
usize::try_from(pop.payment_index)
.expect("Payment index fits into a usize"),
pool_type(pop.value_pool)?,
))
})
.collect::<Result<BTreeMap<usize, PoolType>, ProposalDecodingError<DbError>>>(
)?;
let payment_pools = step
.payment_output_pools
.iter()
.map(|pop| {
Ok((
usize::try_from(pop.payment_index)
.expect("Payment index fits into a usize"),
pool_type(pop.value_pool)?,
))
})
.collect::<Result<BTreeMap<usize, PoolType>, ProposalDecodingError<DbError>>>()?;
#[cfg(not(feature = "transparent-inputs"))]
let transparent_inputs = vec![];
#[cfg(feature = "transparent-inputs")]
let mut transparent_inputs = vec![];
#[cfg(not(feature = "transparent-inputs"))]
let transparent_inputs = vec![];
#[cfg(feature = "transparent-inputs")]
let mut transparent_inputs = vec![];
let mut received_notes = vec![];
let mut prior_step_inputs = vec![];
for (i, input) in step.inputs.iter().enumerate() {
match input
.value
.as_ref()
.ok_or(ProposalDecodingError::NullInput(i))?
{
ReceivedOutput(out) => {
let txid = out
.parse_txid()
.map_err(ProposalDecodingError::TxIdInvalid)?;
let mut received_notes = vec![];
for input in self.inputs.iter() {
let txid = input
.parse_txid()
.map_err(ProposalDecodingError::TxIdInvalid)?;
match out.pool_type()? {
PoolType::Transparent => {
#[cfg(not(feature = "transparent-inputs"))]
return Err(ProposalDecodingError::ValuePoolNotSupported(
1,
));
match input.pool_type()? {
PoolType::Transparent => {
#[cfg(not(feature = "transparent-inputs"))]
return Err(ProposalDecodingError::ValuePoolNotSupported(1));
#[cfg(feature = "transparent-inputs")]
{
let outpoint = OutPoint::new(txid.into(), input.index);
transparent_inputs.push(
wallet_db
.get_unspent_transparent_output(&outpoint)
.map_err(ProposalDecodingError::InputRetrieval)?
.ok_or({
ProposalDecodingError::InputNotFound(
txid,
PoolType::Transparent,
input.index,
)
})?,
);
#[cfg(feature = "transparent-inputs")]
{
let outpoint = OutPoint::new(txid.into(), out.index);
transparent_inputs.push(
wallet_db
.get_unspent_transparent_output(&outpoint)
.map_err(ProposalDecodingError::InputRetrieval)?
.ok_or({
ProposalDecodingError::InputNotFound(
txid,
PoolType::Transparent,
out.index,
)
})?,
);
}
}
PoolType::Shielded(protocol) => received_notes.push(
wallet_db
.get_spendable_note(&txid, protocol, out.index)
.map_err(ProposalDecodingError::InputRetrieval)
.and_then(|opt| {
opt.ok_or({
ProposalDecodingError::InputNotFound(
txid,
PoolType::Shielded(protocol),
out.index,
)
})
})?,
),
}
}
PriorStepOutput(s_ref) => {
prior_step_inputs.push(StepOutput::new(
s_ref
.step_index
.try_into()
.expect("Step index fits into a usize"),
StepOutputIndex::Payment(
s_ref
.payment_index
.try_into()
.expect("Payment index fits into a usize"),
),
));
}
PriorStepChange(s_ref) => {
prior_step_inputs.push(StepOutput::new(
s_ref
.step_index
.try_into()
.expect("Step index fits into a usize"),
StepOutputIndex::Change(
s_ref
.change_index
.try_into()
.expect("Payment index fits into a usize"),
),
));
}
}
PoolType::Shielded(protocol) => received_notes.push(
wallet_db
.get_spendable_note(&txid, protocol, input.index)
.map_err(ProposalDecodingError::InputRetrieval)
.and_then(|opt| {
opt.ok_or({
ProposalDecodingError::InputNotFound(
txid,
PoolType::Shielded(protocol),
input.index,
)
})
})?,
),
}
let shielded_inputs = NonEmpty::from_vec(received_notes)
.map(|notes| ShieldedInputs::from_parts(step.anchor_height.into(), notes));
let proto_balance = step
.balance
.as_ref()
.ok_or(ProposalDecodingError::BalanceInvalid)?;
let balance = TransactionBalance::new(
proto_balance
.proposed_change
.iter()
.map(|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
match cv.pool_type()? {
PoolType::Shielded(ShieldedProtocol::Sapling) => {
Ok(ChangeValue::sapling(
NonNegativeAmount::from_u64(cv.value).map_err(
|_| ProposalDecodingError::BalanceInvalid,
)?,
cv.memo
.as_ref()
.map(|bytes| {
MemoBytes::from_bytes(&bytes.value)
.map_err(ProposalDecodingError::MemoInvalid)
})
.transpose()?,
))
}
t => Err(ProposalDecodingError::InvalidChangeRecipient(t)),
}
})
.collect::<Result<Vec<_>, _>>()?,
NonNegativeAmount::from_u64(proto_balance.fee_required)
.map_err(|_| ProposalDecodingError::BalanceInvalid)?,
)
.map_err(|_| ProposalDecodingError::BalanceInvalid)?;
let step = Step::from_parts(
&steps,
transaction_request,
payment_pools,
transparent_inputs,
shielded_inputs,
prior_step_inputs,
balance,
step.is_shielding,
)
.map_err(ProposalDecodingError::ProposalInvalid)?;
steps.push(step);
}
let shielded_inputs = NonEmpty::from_vec(received_notes)
.map(|notes| ShieldedInputs::from_parts(self.anchor_height.into(), notes));
let proto_balance = self
.balance
.as_ref()
.ok_or(ProposalDecodingError::BalanceInvalid)?;
let balance = TransactionBalance::new(
proto_balance
.proposed_change
.iter()
.map(|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
match cv.pool_type()? {
PoolType::Shielded(ShieldedProtocol::Sapling) => {
Ok(ChangeValue::sapling(
NonNegativeAmount::from_u64(cv.value)
.map_err(|_| ProposalDecodingError::BalanceInvalid)?,
cv.memo
.as_ref()
.map(|bytes| {
MemoBytes::from_bytes(&bytes.value)
.map_err(ProposalDecodingError::MemoInvalid)
})
.transpose()?,
))
}
t => Err(ProposalDecodingError::InvalidChangeRecipient(t)),
}
})
.collect::<Result<Vec<_>, _>>()?,
NonNegativeAmount::from_u64(proto_balance.fee_required)
.map_err(|_| ProposalDecodingError::BalanceInvalid)?,
)
.map_err(|_| ProposalDecodingError::BalanceInvalid)?;
Proposal::from_parts(
transaction_request,
payment_pools,
transparent_inputs,
shielded_inputs,
balance,
Proposal::multi_step(
fee_rule,
self.min_target_height.into(),
self.is_shielding,
NonEmpty::from_vec(steps).ok_or(ProposalDecodingError::NoSteps)?,
)
.map_err(ProposalDecodingError::ProposalInvalid)
}

View File

@ -1,41 +1,50 @@
/// A data structure that describes the inputs to be consumed and outputs to
/// be produced in a proposed transaction.
/// A data structure that describes the a series of transactions to be created.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Proposal {
/// The version of this serialization format.
#[prost(uint32, tag = "1")]
pub proto_version: u32,
/// ZIP 321 serialized transaction request
#[prost(string, tag = "2")]
pub transaction_request: ::prost::alloc::string::String,
/// The vector of selected payment index / output pool mappings. Payment index
/// 0 corresponds to the payment with no explicit index.
#[prost(message, repeated, tag = "3")]
pub payment_output_pools: ::prost::alloc::vec::Vec<PaymentOutputPool>,
/// The anchor height to be used in creating the transaction, if any.
/// Setting the anchor height to zero will disallow the use of any shielded
/// inputs.
#[prost(uint32, tag = "4")]
pub anchor_height: u32,
/// The inputs to be used in creating the transaction.
#[prost(message, repeated, tag = "5")]
pub inputs: ::prost::alloc::vec::Vec<ProposedInput>,
/// The total value, fee value, and change outputs of the proposed
/// transaction
#[prost(message, optional, tag = "6")]
pub balance: ::core::option::Option<TransactionBalance>,
/// The fee rule used in constructing this proposal
#[prost(enumeration = "FeeRule", tag = "7")]
#[prost(enumeration = "FeeRule", tag = "2")]
pub fee_rule: i32,
/// The target height for which the proposal was constructed
///
/// The chain must contain at least this many blocks in order for the proposal to
/// be executed.
#[prost(uint32, tag = "8")]
#[prost(uint32, tag = "3")]
pub min_target_height: u32,
/// A flag indicating whether the proposal is for a shielding transaction,
/// The series of transactions to be created.
#[prost(message, repeated, tag = "4")]
pub steps: ::prost::alloc::vec::Vec<ProposalStep>,
}
/// A data structure that describes the inputs to be consumed and outputs to
/// be produced in a proposed transaction.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProposalStep {
/// ZIP 321 serialized transaction request
#[prost(string, tag = "1")]
pub transaction_request: ::prost::alloc::string::String,
/// The vector of selected payment index / output pool mappings. Payment index
/// 0 corresponds to the payment with no explicit index.
#[prost(message, repeated, tag = "2")]
pub payment_output_pools: ::prost::alloc::vec::Vec<PaymentOutputPool>,
/// The anchor height to be used in creating the transaction, if any.
/// Setting the anchor height to zero will disallow the use of any shielded
/// inputs.
#[prost(uint32, tag = "3")]
pub anchor_height: u32,
/// The inputs to be used in creating the transaction.
#[prost(message, repeated, tag = "4")]
pub inputs: ::prost::alloc::vec::Vec<ProposedInput>,
/// The total value, fee value, and change outputs of the proposed
/// transaction
#[prost(message, optional, tag = "5")]
pub balance: ::core::option::Option<TransactionBalance>,
/// A flag indicating whether the step is for a shielding transaction,
/// used for determining which OVK to select for wallet-internal outputs.
#[prost(bool, tag = "9")]
#[prost(bool, tag = "6")]
pub is_shielding: bool,
}
/// A mapping from ZIP 321 payment index to the output pool that has been chosen
@ -49,10 +58,11 @@ pub struct PaymentOutputPool {
#[prost(enumeration = "ValuePool", tag = "2")]
pub value_pool: i32,
}
/// The unique identifier and value for each proposed input.
/// The unique identifier and value for each proposed input that does not
/// require a back-reference to a prior step of the proposal.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProposedInput {
pub struct ReceivedOutput {
#[prost(bytes = "vec", tag = "1")]
pub txid: ::prost::alloc::vec::Vec<u8>,
#[prost(enumeration = "ValuePool", tag = "2")]
@ -62,12 +72,53 @@ pub struct ProposedInput {
#[prost(uint64, tag = "4")]
pub value: u64,
}
/// A reference a payment in a prior step of the proposal. This payment must
/// belong to the wallet.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PriorStepOutput {
#[prost(uint32, tag = "1")]
pub step_index: u32,
#[prost(uint32, tag = "2")]
pub payment_index: u32,
}
/// A reference a change output from a prior step of the proposal.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PriorStepChange {
#[prost(uint32, tag = "1")]
pub step_index: u32,
#[prost(uint32, tag = "2")]
pub change_index: u32,
}
/// The unique identifier and value for an input to be used in the transaction.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProposedInput {
#[prost(oneof = "proposed_input::Value", tags = "1, 2, 3")]
pub value: ::core::option::Option<proposed_input::Value>,
}
/// Nested message and enum types in `ProposedInput`.
pub mod proposed_input {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum Value {
#[prost(message, tag = "1")]
ReceivedOutput(super::ReceivedOutput),
#[prost(message, tag = "2")]
PriorStepOutput(super::PriorStepOutput),
#[prost(message, tag = "3")]
PriorStepChange(super::PriorStepChange),
}
}
/// The proposed change outputs and fee value.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TransactionBalance {
/// A list of change output values.
#[prost(message, repeated, tag = "1")]
pub proposed_change: ::prost::alloc::vec::Vec<ChangeValue>,
/// The fee to be paid by the proposed transaction, in zatoshis.
#[prost(uint64, tag = "2")]
pub fee_required: u64,
}
@ -76,10 +127,14 @@ pub struct TransactionBalance {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ChangeValue {
/// The value of a change output to be created, in zatoshis.
#[prost(uint64, tag = "1")]
pub value: u64,
/// The value pool in which the change output should be created.
#[prost(enumeration = "ValuePool", tag = "2")]
pub value_pool: i32,
/// The optional memo that should be associated with the newly created change output.
/// Memos must not be present for transparent change outputs.
#[prost(message, optional, tag = "3")]
pub memo: ::core::option::Option<MemoBytes>,
}

View File

@ -378,6 +378,7 @@ impl<NoteRef> orchard_fees::InputView<NoteRef> for ReceivedNote<NoteRef, orchard
/// viewing key, refer to [ZIP 310].
///
/// [ZIP 310]: https://zips.z.cash/zip-0310
#[derive(Debug, Clone)]
pub enum OvkPolicy {
/// Use the outgoing viewing key from the sender's [`ExtendedFullViewingKey`].
///

View File

@ -70,6 +70,7 @@ maybe-rayon.workspace = true
assert_matches.workspace = true
incrementalmerkletree = { workspace = true, features = ["test-dependencies"] }
shardtree = { workspace = true, features = ["legacy-api", "test-dependencies"] }
nonempty.workspace = true
proptest.workspace = true
rand_core.workspace = true
regex = "1.4"

View File

@ -5,6 +5,7 @@ use std::num::NonZeroU32;
#[cfg(feature = "unstable")]
use std::fs::File;
use nonempty::NonEmpty;
use prost::Message;
use rand_core::{OsRng, RngCore};
use rusqlite::{params, Connection};
@ -29,7 +30,7 @@ use zcash_client_backend::{
self,
chain::{scan_cached_blocks, BlockSource, ScanSummary},
wallet::{
create_proposed_transaction, create_spend_to_address,
create_proposed_transactions, create_spend_to_address,
input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector},
propose_standard_transfer_to_address, propose_transfer, spend,
},
@ -442,7 +443,7 @@ impl<Cache> TestState<Cache> {
min_confirmations: NonZeroU32,
change_memo: Option<MemoBytes>,
) -> Result<
TxId,
NonEmpty<TxId>,
data_api::error::Error<
SqliteClientError,
commitment_tree::Error,
@ -477,7 +478,7 @@ impl<Cache> TestState<Cache> {
ovk_policy: OvkPolicy,
min_confirmations: NonZeroU32,
) -> Result<
TxId,
NonEmpty<TxId>,
data_api::error::Error<
SqliteClientError,
commitment_tree::Error,
@ -608,14 +609,14 @@ impl<Cache> TestState<Cache> {
)
}
/// Invokes [`create_proposed_transaction`] with the given arguments.
pub(crate) fn create_proposed_transaction<InputsErrT, FeeRuleT>(
/// Invokes [`create_proposed_transactions`] with the given arguments.
pub(crate) fn create_proposed_transactions<InputsErrT, FeeRuleT>(
&mut self,
usk: &UnifiedSpendingKey,
ovk_policy: OvkPolicy,
proposal: &Proposal<FeeRuleT, ReceivedNoteId>,
) -> Result<
TxId,
NonEmpty<TxId>,
data_api::error::Error<
SqliteClientError,
commitment_tree::Error,
@ -628,7 +629,7 @@ impl<Cache> TestState<Cache> {
{
let params = self.network();
let prover = test_prover();
create_proposed_transaction(
create_proposed_transactions(
&mut self.db_data,
&params,
&prover,
@ -650,7 +651,7 @@ impl<Cache> TestState<Cache> {
from_addrs: &[TransparentAddress],
min_confirmations: u32,
) -> Result<
TxId,
NonEmpty<TxId>,
data_api::error::Error<
SqliteClientError,
commitment_tree::Error,

View File

@ -2316,7 +2316,7 @@ mod tests {
);
let txid = st
.shield_transparent_funds(&input_selector, value, &usk, &[*taddr], 1)
.unwrap();
.unwrap()[0];
// The wallet should have zero transparent balance, because the shielding
// transaction can be mined.

View File

@ -589,10 +589,10 @@ pub(crate) mod tests {
.unwrap();
let create_proposed_result =
st.create_proposed_transaction::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal);
assert_matches!(create_proposed_result, Ok(_));
st.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal);
assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1);
let sent_tx_id = create_proposed_result.unwrap();
let sent_tx_id = create_proposed_result.unwrap()[0];
// Verify that the sent transaction was stored and that we can decrypt the memos
let tx = st
@ -864,8 +864,8 @@ pub(crate) mod tests {
// Executing the proposal should succeed
let txid = st
.create_proposed_transaction::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal)
.unwrap();
.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal)
.unwrap()[0];
let (h, _) = st.generate_next_block_including(txid);
st.scan_cached_blocks(h, 1);
@ -921,8 +921,8 @@ pub(crate) mod tests {
// Executing the proposal should succeed
assert_matches!(
st.create_proposed_transaction::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal,),
Ok(_)
st.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal,),
Ok(txids) if txids.len() == 1
);
// A second proposal fails because there are no usable notes
@ -1000,8 +1000,8 @@ pub(crate) mod tests {
.unwrap();
let txid2 = st
.create_proposed_transaction::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal)
.unwrap();
.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal)
.unwrap()[0];
let (h, _) = st.generate_next_block_including(txid2);
st.scan_cached_blocks(h, 1);
@ -1066,7 +1066,7 @@ pub(crate) mod tests {
)?;
// Executing the proposal should succeed
let txid = st.create_proposed_transaction(&usk, ovk_policy, &proposal)?;
let txid = st.create_proposed_transactions(&usk, ovk_policy, &proposal)?[0];
// Fetch the transaction from the database
let raw_tx: Vec<_> = st
@ -1170,8 +1170,8 @@ pub(crate) mod tests {
// Executing the proposal should succeed
assert_matches!(
st.create_proposed_transaction::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal),
Ok(_)
st.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal),
Ok(txids) if txids.len() == 1
);
}
@ -1232,8 +1232,8 @@ pub(crate) mod tests {
// Executing the proposal should succeed
assert_matches!(
st.create_proposed_transaction::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal),
Ok(_)
st.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal),
Ok(txids) if txids.len() == 1
);
}
@ -1310,7 +1310,7 @@ pub(crate) mod tests {
OvkPolicy::Sender,
NonZeroU32::new(1).unwrap(),
)
.unwrap();
.unwrap()[0];
let amount_left = (value - (amount_sent + fee_rule.fixed_fee()).unwrap()).unwrap();
let pending_change = (amount_left - amount_legacy_change).unwrap();
@ -1441,7 +1441,7 @@ pub(crate) mod tests {
OvkPolicy::Sender,
NonZeroU32::new(1).unwrap(),
)
.unwrap();
.unwrap()[0];
let (h, _) = st.generate_next_block_including(txid);
st.scan_cached_blocks(h, 1);