Merge pull request #1187 from nuttycom/proposal_resolved_receivers

`zcash_client_backend`: Updates to the `Proposal` data structure
This commit is contained in:
Kris Nuttycombe 2024-02-15 21:28:20 -07:00 committed by GitHub
commit c6656c108b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1874 additions and 844 deletions

5
Cargo.lock generated
View File

@ -2104,9 +2104,9 @@ dependencies = [
[[package]]
name = "sapling-crypto"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f5de898a7cdb7f6d9c8fb888341b6ae6e2aeae88227b7f435f1dda49ecf9e62"
checksum = "d183012062dfdde85f7e3e758328fcf6e9846d8dd3fce35b04d0efcb6677b0e0"
dependencies = [
"aes",
"bellman",
@ -3062,6 +3062,7 @@ dependencies = [
"incrementalmerkletree",
"jubjub",
"maybe-rayon",
"nonempty",
"orchard",
"proptest",
"prost",

View File

@ -49,7 +49,7 @@ bitvec = "1"
blake2s_simd = "1"
bls12_381 = "0.8"
jubjub = "0.10"
sapling = { package = "sapling-crypto", version = "0.1" }
sapling = { package = "sapling-crypto", version = "0.1.1" }
# - Orchard
nonempty = "0.7"

View File

@ -35,6 +35,7 @@ and this library adheres to Rust's notion of
## [0.11.0-pre-release] Unreleased
### Added
- `zcash_client_backend::PoolType::is_receiver`
- `zcash_client_backend::data_api`:
- `InputSource`
- `ScannedBlock::{into_commitments, sapling}`
@ -47,36 +48,42 @@ and this library adheres to Rust's notion of
}`
- `WalletSummary::next_sapling_subtree_index`
- `wallet::propose_standard_transfer_to_address`
- `wallet::input_selection::Proposal::{from_parts, shielded_inputs}`
- `wallet::create_proposed_transactions`
- `wallet::input_selection::ShieldedInputs`
- `wallet::input_selection::ShieldingSelector` has been
factored out from the `InputSelector` trait to separate out transparent
functionality and move it behind the `transparent-inputs` feature flag.
- `TransparentAddressMetadata` (which replaces `zcash_keys::address::AddressMetadata`).
- `impl std::error::Error for wallet::input_selection::InputSelectorError`
- `zcash_client_backend::fees::{standard, sapling}`
- `zcash_client_backend::fees::ChangeValue::new`
- `zcash_client_backend::wallet`:
- `Note`
- `ReceivedNote`
- `WalletSaplingOutput::recipient_key_scope`
- `TransparentAddressMetadata` (which replaces `zcash_keys::address::AddressMetadata`).
- `impl {Debug, Clone} for OvkPolicy`
- `zcash_client_backend::zip321::TransactionRequest::total`
- `zcash_client_backend::zip321::parse::Param::name`
- `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.
- `impl Clone for zcash_client_backend::{
zip321::{Payment, TransactionRequest, Zip321Error, parse::Param, parse::IndexedParam},
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
wallet::input_selection::{Proposal, SaplingInputs},
proposal::{Proposal, SaplingInputs},
}`
- `impl {PartialEq, Eq} for zcash_client_backend::{
zip321::{Zip321Error, parse::Param, parse::IndexedParam},
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
wallet::input_selection::{Proposal, SaplingInputs},
proposal::{Proposal, SaplingInputs},
}`
- `zcash_client_backend::zip321::to_uri` now returns a `String` rather than an
`Option<String>` and provides canonical serialization for the empty proposal.
- `zcash_client_backend::zip321`:
- `TransactionRequest::{total, from_indexed}`
- `parse::Param::name`
### Moved
- `zcash_client_backend::data_api::{PoolType, ShieldedProtocol}` have
@ -113,31 +120,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.
- 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`
@ -149,6 +169,7 @@ and this library adheres to Rust's notion of
`min_confirmations` argument as `u32` instead of `NonZeroU32`.
- The `wallet::input_selection::InputSelector::DataSource`
associated type has been renamed to `InputSource`.
- `wallet::input_selection::InputSelectorError` has added variant `Proposal`
- The signature of `wallet:input_selection::InputSelector::propose_transaction`
has been altered such that it longer takes `min_confirmations` as an
argument, instead taking explicit `target_height` and `anchor_height`
@ -156,29 +177,42 @@ 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
`get_unspent_transparent_outputs` have been removed; use
`data_api::InputSource` instead.
- Added `get_account_ids`.
- `get_transparent_receivers` and `get_transparent_balances` are now
guarded by the `transparent-inputs` feature flag, with noop default
implementations provided.
- `get_transparent_receivers` now returns
`zcash_client_backend::data_api::TransparentAddressMetadata` instead of
`zcash_keys::address::AddressMetadata`.
`Option<zcash_client_backend::wallet::TransparentAddressMetadata>` as part of
its result where previously it returned `zcash_keys::address::AddressMetadata`.
- `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.
- Arguments to `wallet::input_selection::Proposal::from_parts` have changed.
- `wallet::input_selection::Proposal::min_anchor_height` has been removed in
favor of storing this value in `SaplingInputs`.
- `wallet::input_selection::GreedyInputSelector` now has relaxed requirements
for its `InputSource` associated type.
- `zcash_client_backend::proposal`:
- Arguments to `Proposal::from_parts` have changed.
- `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.
- `ProposalError` has new variants:
- `ReferenceError`
- `StepDoubleSpend`
- `ChainDoubleSpend`
- `PaymentPoolsMismatch`
- `zcash_client_backend::fees`:
- `ChangeStrategy::compute_balance` arguments have changed.
- `ChangeValue` is now a struct. In addition to the existing change value, it
@ -193,6 +227,14 @@ and this library adheres to Rust's notion of
`ReceivedSaplingNote::from_parts` for construction instead. Accessor methods
are provided for each previously public field.
- `zcash_client_backend::scanning::ScanError` has a new variant, `TreeSizeInvalid`.
- `zcash_client_backend::zip321::TransactionRequest::payments` now returns a
`BTreeMap<usize, Payment>` instead of `&[Payment]` so that parameter
indices may be preserved.
- `zcash_client_backend::zip321::to_uri` now returns a `String` rather than an
`Option<String>` and provides canonical serialization for the empty proposal.
- `zcash_client_backend::zip321::from_uri` previously stripped payment indices,
meaning that round-trip serialization was not supported. Payment indices are
now retained.
- The following fields now have type `NonNegativeAmount` instead of `Amount`:
- `zcash_client_backend::data_api`:
- `error::Error::InsufficientFunds.{available, required}`
@ -223,12 +265,13 @@ and this library adheres to Rust's notion of
### Removed
- `zcash_client_backend::wallet::ReceivedSaplingNote` has been replaced by
`zcash_client_backend::ReceivedNote`.
- `zcash_client_backend::wallet::input_selection::Proposal::sapling_inputs` has
been replaced by `Proposal::shielded_inputs`
- `zcash_client_backend::::wallet::input_selection::{Proposal, ShieldedInputs, ProposalError}`
have been moved to `zcash_client_backend::proposal`.
- `zcash_client_backend::data_api`
- `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,12 +5,29 @@
syntax = "proto3";
package cash.z.wallet.sdk.ffi;
// A data structure that describes a series of transactions to be created.
message Proposal {
// The version of this serialization format.
uint32 protoVersion = 1;
// The fee rule used in constructing this proposal
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 = 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 Proposal {
uint32 protoVersion = 1;
message ProposalStep {
// ZIP 321 serialized transaction request
string transactionRequest = 2;
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.
@ -20,16 +37,9 @@ message Proposal {
// The total value, fee value, and change outputs of the proposed
// transaction
TransactionBalance balance = 5;
// The fee rule used in constructing this proposal
FeeRule feeRule = 6;
// 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 = 7;
// A flag indicating whether the proposal is for a shielding transaction,
// A flag indicating whether the step is for a shielding transaction,
// used for determining which OVK to select for wallet-internal outputs.
bool isShielding = 8;
bool isShielding = 6;
}
enum ValuePool {
@ -46,14 +56,45 @@ enum ValuePool {
Orchard = 3;
}
// The unique identifier and value for each proposed input.
message ProposedInput {
// A mapping from ZIP 321 payment index to the output pool that has been chosen
// for that payment, based upon the payment address and the selected inputs to
// the transaction.
message PaymentOutputPool {
uint32 paymentIndex = 1;
ValuePool valuePool = 2;
}
// 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
@ -71,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

@ -14,7 +14,6 @@ use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
use zcash_primitives::{
block::BlockHash,
consensus::BlockHeight,
legacy::{NonHardenedChildIndex, TransparentAddress},
memo::{Memo, MemoBytes},
transaction::{
components::amount::{Amount, BalanceError, NonNegativeAmount},
@ -36,7 +35,10 @@ use self::chain::CommitmentTreeRoot;
use self::scanning::ScanRange;
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::transaction::components::OutPoint;
use {
crate::wallet::TransparentAddressMetadata,
zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint},
};
pub mod chain;
pub mod error;
@ -56,30 +58,6 @@ pub enum NullifierQuery {
All,
}
/// Describes the derivation of a transparent address.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransparentAddressMetadata {
scope: zip32::Scope,
address_index: NonHardenedChildIndex,
}
impl TransparentAddressMetadata {
pub fn new(scope: zip32::Scope, address_index: NonHardenedChildIndex) -> Self {
Self {
scope,
address_index,
}
}
pub fn scope(&self) -> zip32::Scope {
self.scope
}
pub fn address_index(&self) -> NonHardenedChildIndex {
self.address_index
}
}
/// Balance information for a value within a single pool in an account.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Balance {
@ -581,18 +559,24 @@ pub trait WalletRead {
/// The set contains all transparent receivers that are known to have been derived
/// under this account. Wallets should scan the chain for UTXOs sent to these
/// receivers.
#[cfg(feature = "transparent-inputs")]
fn get_transparent_receivers(
&self,
account: AccountId,
) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, Self::Error>;
_account: AccountId,
) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, Self::Error> {
Ok(HashMap::new())
}
/// Returns a mapping from transparent receiver to not-yet-shielded UTXO balance,
/// for each address associated with a nonzero balance.
#[cfg(feature = "transparent-inputs")]
fn get_transparent_balances(
&self,
account: AccountId,
max_height: BlockHeight,
) -> Result<HashMap<TransparentAddress, Amount>, Self::Error>;
_account: AccountId,
_max_height: BlockHeight,
) -> Result<HashMap<TransparentAddress, Amount>, Self::Error> {
Ok(HashMap::new())
}
/// Returns a vector with the IDs of all accounts known to this wallet.
fn get_account_ids(&self) -> Result<Vec<AccountId>, Self::Error>;
@ -1133,7 +1117,6 @@ pub mod testing {
use zcash_primitives::{
block::BlockHash,
consensus::{BlockHeight, Network},
legacy::TransparentAddress,
memo::Memo,
transaction::{components::Amount, Transaction, TxId},
zip32::{AccountId, Scope},
@ -1149,10 +1132,12 @@ pub mod testing {
use super::{
chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata,
DecryptedTransaction, InputSource, NullifierQuery, ScannedBlock, SentTransaction,
TransparentAddressMetadata, WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite,
SAPLING_SHARD_HEIGHT,
WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
};
#[cfg(feature = "transparent-inputs")]
use {crate::wallet::TransparentAddressMetadata, zcash_primitives::legacy::TransparentAddress};
pub struct MockWalletDb {
pub network: Network,
pub sapling_tree: ShardTree<
@ -1306,6 +1291,7 @@ pub mod testing {
Ok(Vec::new())
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_receivers(
&self,
_account: AccountId,
@ -1314,6 +1300,7 @@ pub mod testing {
Ok(HashMap::new())
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_balances(
&self,
_account: AccountId,

View File

@ -14,6 +14,7 @@ use zcash_primitives::{
};
use crate::data_api::wallet::input_selection::InputSelectorError;
use crate::proposal::ProposalError;
use crate::PoolType;
#[cfg(feature = "transparent-inputs")]
@ -33,6 +34,13 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
/// An error in note selection
NoteSelection(SelectionError),
/// 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,
@ -100,6 +108,15 @@ where
Error::NoteSelection(e) => {
write!(f, "Note selection encountered the following error: {}", e)
}
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,
@ -148,6 +165,7 @@ where
Error::DataSource(e) => Some(e),
Error::CommitmentTree(e) => Some(e),
Error::NoteSelection(e) => Some(e),
Error::Proposal(e) => Some(e),
Error::Builder(e) => Some(e),
_ => None,
}
@ -171,6 +189,7 @@ 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),
InputSelectorError::InsufficientFunds {
available,
required,

View File

@ -1,16 +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_keys::encoding::AddressCodec;
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,
@ -21,12 +21,14 @@ use zcash_primitives::{
use crate::{
address::Address,
data_api::{
error::Error, wallet::input_selection::Proposal, DecryptedTransaction, SentTransaction,
SentTransactionOutput, WalletCommitmentTrees, WalletRead, WalletWrite,
error::Error, DecryptedTransaction, SentTransaction, SentTransactionOutput,
WalletCommitmentTrees, WalletRead, WalletWrite,
},
decrypt_transaction,
fees::{self, DustOutputPolicy},
keys::UnifiedSpendingKey,
proposal::ProposalError,
proposal::{self, Proposal},
wallet::{Note, OvkPolicy, Recipient},
zip321::{self, Payment},
PoolType, ShieldedProtocol,
@ -41,9 +43,12 @@ use super::InputSource;
#[cfg(feature = "transparent-inputs")]
use {
crate::wallet::WalletTransparentOutput, input_selection::ShieldingSelector,
sapling::keys::OutgoingViewingKey, std::convert::Infallible,
input_selection::ShieldingSelector,
sapling::keys::OutgoingViewingKey,
std::convert::Infallible,
zcash_keys::encoding::AddressCodec,
zcash_primitives::legacy::TransparentAddress,
zcash_primitives::transaction::components::{OutPoint, TxOut},
};
/// Scans a [`Transaction`] for any information that can be decrypted by the accounts in
@ -77,11 +82,11 @@ where
}
#[allow(clippy::needless_doctest_main)]
/// Creates a transaction paying the specified address from the given account.
/// Creates a transaction or series of transactions paying the specified address from
/// the given account, and the [`TxId`] corresponding to each newly-created transaction.
///
/// Returns the row index of the newly-created transaction in the `transactions` table
/// within the data database. The caller can read the raw transaction bytes from the `raw`
/// column in order to broadcast the transaction to the network.
/// These transactions can be retrieved from the underlying data store using the
/// [`WalletRead::get_transaction`] method.
///
/// Do not call this multiple times in parallel, or you will generate transactions that
/// double-spend the same notes.
@ -207,7 +212,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 +243,7 @@ where
change_memo,
)?;
create_proposed_transaction(
create_proposed_transactions(
wallet_db,
params,
spend_prover,
@ -249,15 +254,12 @@ where
)
}
/// Constructs a transaction that sends funds as specified by the `request` argument
/// and stores it to the wallet's "sent transactions" data store, and returns a
/// unique identifier for the transaction; this identifier is used only for internal
/// reference purposes and is not the same as the transaction's txid, although after v4
/// transactions have been made invalid in a future network upgrade, the txid could
/// potentially be used for this type (as it is non-malleable for v5+ transactions).
/// Constructs a transaction or series of transactions that send funds as specified
/// by the `request` argument, stores them to the wallet's "sent transactions" data
/// store, and returns the [`TxId`] for each transaction constructed.
///
/// This procedure uses the wallet's underlying note selection algorithm to choose
/// inputs of sufficient value to satisfy the request, if possible.
/// The newly-created transactions can be retrieved from the underlying data store using the
/// [`WalletRead::get_transaction`] method.
///
/// Do not call this multiple times in parallel, or you will generate transactions that
/// double-spend the same notes.
@ -316,7 +318,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 +346,7 @@ where
min_confirmations,
)?;
create_proposed_transaction(
create_proposed_transactions(
wallet_db,
params,
spend_prover,
@ -355,8 +357,8 @@ where
)
}
/// Select transaction inputs, compute fees, and construct a proposal for a transaction
/// that can then be authorized and made ready for submission to the network with
/// Select transaction inputs, compute fees, and construct a proposal for a transaction or series
/// of transactions that can then be authorized and made ready for submission to the network with
/// [`create_proposed_transaction`].
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
@ -399,9 +401,13 @@ where
.map_err(Error::from)
}
/// Proposes a transaction paying the specified address from the given account.
/// Proposes making a payment to the specified address from the given account.
///
/// Returns the proposal, which may then be executed using [`create_proposed_transaction`]
/// Returns the proposal, which may then be executed using [`create_proposed_transaction`].
/// Depending upon the recipient address, more than one transaction may be constructed
/// in the execution of the returned proposal.
///
/// This method uses the basic [`GreedyInputSelector`] for input selection.
///
/// Parameters:
/// * `wallet_db`: A read/write reference to the wallet database.
@ -470,6 +476,8 @@ where
)
}
/// Constructs a proposal to shield all of the funds belonging to the provided set of
/// addresses.
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
@ -511,17 +519,20 @@ where
.map_err(Error::from)
}
/// Construct, prove, and sign a transaction using the inputs supplied by the given proposal,
/// and persist it to the wallet database.
/// Construct, prove, and sign a transaction or series of transactions using the inputs supplied by
/// the given proposal, and persist it to the wallet database.
///
/// Returns the database identifier for the newly constructed transaction, or an error if
/// Returns the database identifier for each newly constructed transaction, or an error if
/// an error occurs in transaction construction, proving, or signing.
///
/// Note: If the payment includes a recipient with an Orchard-only UA, this will attempt
/// to fall back to the transparent receiver until full Orchard support is implemented.
/// When evaluating multi-step proposals, only transparent outputs of any given step may be spent
/// in later steps; attempting to spend a shielded note (including change) output by an earlier
/// step is not supported, because the ultimate positions of those notes in the global note
/// commitment tree cannot be known until the transaction that produces those notes is mined,
/// and therefore the required spend proofs for such notes cannot be constructed.
#[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 +541,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 +554,92 @@ 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.
for s_ref in proposal_step.prior_step_inputs() {
prior_step_results.get(s_ref.step_index()).map_or_else(
|| {
// Return an error in case the step index doesn't match up with a step
Err(Error::Proposal(ProposalError::ReferenceError(*s_ref)))
},
|step| match s_ref.output_index() {
proposal::StepOutputIndex::Payment(i) => {
let prior_pool = step
.0
.payment_pools()
.get(&i)
.ok_or(Error::Proposal(ProposalError::ReferenceError(*s_ref)))?;
if matches!(prior_pool, PoolType::Shielded(_)) {
Err(Error::ProposalNotSupported)
} else {
Ok(())
}
}
proposal::StepOutputIndex::Change(_) => {
// Only shielded change is supported by zcash_client_backend, so multi-step
// transactions cannot yet spend prior transactions' change outputs.
Err(Error::ProposalNotSupported)
}
},
)?;
}
let account = wallet_db
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
.map_err(Error::DataSource)?
@ -559,7 +656,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 +671,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 +716,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,
@ -631,40 +728,98 @@ where
}
#[cfg(feature = "transparent-inputs")]
let utxos = {
let utxos_spent = {
let known_addrs = wallet_db
.get_transparent_receivers(account)
.map_err(Error::DataSource)?;
let mut utxos: Vec<WalletTransparentOutput> = vec![];
for utxo in proposal.transparent_inputs() {
utxos.push(utxo.clone());
let mut utxos_spent: Vec<OutPoint> = vec![];
let mut add_transparent_input = |addr: &TransparentAddress,
outpoint: OutPoint,
utxo: TxOut|
-> Result<
(),
Error<
<DbT as WalletRead>::Error,
<DbT as WalletCommitmentTrees>::Error,
InputsErrT,
FeeRuleT::Error,
>,
> {
let address_metadata = known_addrs
.get(utxo.recipient_address())
.ok_or_else(|| Error::AddressNotRecognized(*utxo.recipient_address()))?
.get(addr)
.ok_or(Error::AddressNotRecognized(*addr))?
.clone()
.ok_or_else(|| {
Error::NoSpendingKey(utxo.recipient_address().encode(params))
})?;
.ok_or_else(|| Error::NoSpendingKey(addr.encode(params)))?;
let secret_key = usk
.transparent()
.derive_external_secret_key(address_metadata.address_index())
.derive_secret_key(address_metadata.scope(), address_metadata.address_index())
.unwrap();
builder.add_transparent_input(
secret_key,
utxos_spent.push(outpoint.clone());
builder.add_transparent_input(secret_key, outpoint, utxo)?;
Ok(())
};
for utxo in proposal_step.transparent_inputs() {
add_transparent_input(
utxo.recipient_address(),
utxo.outpoint().clone(),
utxo.txout().clone(),
)?;
}
utxos
for input_ref in proposal_step.prior_step_inputs() {
match input_ref.output_index() {
proposal::StepOutputIndex::Payment(i) => {
// We know based upon the earlier check that this must be a transparent input,
// We also know that transparent outputs for that previous step were added to
// the transaction in payment index order, so we can use dead reckoning to
// figure out which output it ended up being.
let (prior_step, result) = &prior_step_results[input_ref.step_index()];
let recipient_address = match &prior_step
.transaction_request()
.payments()
.get(&i)
.expect("Payment step references are checked at construction")
.recipient_address
{
Address::Transparent(t) => Some(t),
Address::Unified(uaddr) => uaddr.transparent(),
_ => None,
}
.ok_or(Error::ProposalNotSupported)?;
let outpoint = OutPoint::new(
result.transaction().txid().into(),
u32::try_from(
prior_step
.payment_pools()
.iter()
.filter(|(_, pool)| pool == &&PoolType::Transparent)
.take_while(|(j, _)| j <= &&i)
.count()
- 1,
)
.expect("Transparent output index fits into a u32"),
);
let utxo = &result
.transaction()
.transparent_bundle()
.ok_or(Error::Proposal(ProposalError::ReferenceError(*input_ref)))?
.vout[outpoint.n() as usize];
add_transparent_input(recipient_address, outpoint, utxo.clone())?;
}
proposal::StepOutputIndex::Change(_) => unreachable!(),
}
}
utxos_spent
};
let mut sapling_output_meta = vec![];
let mut transparent_output_meta = vec![];
for payment in proposal.transaction_request().payments() {
for payment in proposal_step.transaction_request().payments().values() {
match &payment.recipient_address {
Address::Unified(ua) => {
let memo = payment
@ -713,12 +868,12 @@ where
} else {
builder.add_transparent_output(to, payment.amount)?;
}
transparent_output_meta.push((*to, payment.amount));
transparent_output_meta.push((to, payment.amount));
}
}
}
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());
@ -753,7 +908,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 =
@ -778,10 +933,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))
})
@ -808,7 +960,7 @@ where
SentTransactionOutput::from_parts(
output_index,
Recipient::Transparent(addr),
Recipient::Transparent(*addr),
value,
None,
None,
@ -821,22 +973,22 @@ 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(),
utxos_spent,
})
.map_err(Error::DataSource)?;
Ok(build_result.transaction().txid())
Ok(build_result)
}
/// Constructs a transaction that consumes available transparent UTXOs belonging to
/// the specified secret key, and sends them to the default address for the provided Sapling
/// extended full viewing key.
/// Constructs a transaction that consumes available transparent UTXOs belonging to the specified
/// secret key, and sends them to the most-preferred receiver of the default internal address for
/// the provided Unified Spending Key.
///
/// This procedure will not attempt to shield transparent funds if the total amount being shielded
/// is less than the default fee to send the transaction. Fees will be paid only from the transparent
/// UTXOs being consumed.
/// is less than the default fee to send the transaction. Fees will be paid only from the
/// transparent UTXOs being consumed.
///
/// Parameters:
/// * `wallet_db`: A read/write reference to the wallet database
@ -877,7 +1029,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,
@ -899,7 +1051,7 @@ where
min_confirmations,
)?;
create_proposed_transaction(
create_proposed_transactions(
wallet_db,
params,
spend_prover,

View File

@ -1,12 +1,15 @@
//! Types related to the process of selecting inputs to be spent given a transaction request.
use core::marker::PhantomData;
use std::fmt::{self, Debug, Display};
use std::{
collections::BTreeMap,
error,
fmt::{self, Debug, Display},
};
use nonempty::NonEmpty;
use zcash_primitives::{
consensus::{self, BlockHeight},
legacy::TransparentAddress,
transaction::{
components::{
amount::{BalanceError, NonNegativeAmount},
@ -20,27 +23,32 @@ use zcash_primitives::{
use crate::{
address::{Address, UnifiedAddress},
data_api::InputSource,
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance},
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy},
proposal::{Proposal, ProposalError, ShieldedInputs},
wallet::{Note, ReceivedNote, WalletTransparentOutput},
zip321::TransactionRequest,
ShieldedProtocol,
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;
/// The type of errors that may be produced in input selection.
#[derive(Debug)]
pub enum InputSelectorError<DbErrT, SelectorErrT> {
/// An error occurred accessing the underlying data store.
DataSource(DbErrT),
/// An error occurred specific to the provided input selector's selection rules.
Selection(SelectorErrT),
/// Input selection attempted to generate an invalid transaction proposal.
Proposal(ProposalError),
/// Insufficient funds were available to satisfy the payment request that inputs were being
/// selected to attempt to satisfy.
InsufficientFunds {
@ -65,6 +73,13 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
InputSelectorError::Selection(e) => {
write!(f, "Note selection encountered the following error: {}", e)
}
InputSelectorError::Proposal(e) => {
write!(
f,
"Input selection attempted to generate an invalid proposal: {}",
e
)
}
InputSelectorError::InsufficientFunds {
available,
required,
@ -81,218 +96,21 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
}
}
/// The inputs to be consumed and outputs to be produced in a proposed transaction.
#[derive(Clone, PartialEq, Eq)]
pub struct Proposal<FeeRuleT, NoteRef> {
transaction_request: TransactionRequest,
transparent_inputs: Vec<WalletTransparentOutput>,
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
balance: TransactionBalance,
fee_rule: FeeRuleT,
min_target_height: BlockHeight,
is_shielding: bool,
}
/// Errors that can occur in construction of a [`Proposal`].
#[derive(Debug, Clone)]
pub enum ProposalError {
/// 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,
},
/// 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<DE, SE> error::Error for InputSelectorError<DE, SE>
where
DE: Debug + Display + error::Error + 'static,
SE: Debug + Display + error::Error + 'static,
{
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self {
Self::DataSource(e) => Some(e),
Self::Selection(e) => Some(e),
Self::Proposal(e) => Some(e),
_ => None,
}
}
}
impl std::error::Error for ProposalError {}
impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
/// Constructs a validated [`Proposal`] 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.
#[allow(clippy::too_many_arguments)]
pub fn from_parts(
transaction_request: TransactionRequest,
transparent_inputs: Vec<WalletTransparentOutput>,
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
balance: TransactionBalance,
fee_rule: FeeRuleT,
min_target_height: BlockHeight,
is_shielding: bool,
) -> 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(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 request_total = transaction_request
.total()
.map_err(|_| ProposalError::RequestTotalInvalid)?;
let output_total = (request_total + balance.total()).ok_or(ProposalError::Overflow)?;
if is_shielding
&& (transparent_input_total == NonNegativeAmount::ZERO
|| shielded_input_total > NonNegativeAmount::ZERO
|| request_total > NonNegativeAmount::ZERO)
{
return Err(ProposalError::ShieldingInvalid);
}
if input_total == output_total {
Ok(Self {
transaction_request,
transparent_inputs,
shielded_inputs,
balance,
fee_rule,
min_target_height,
is_shielding,
})
} else {
Err(ProposalError::BalanceError {
input_total,
output_total,
})
}
}
/// Returns the transaction request that describes the payments to be made.
pub fn transaction_request(&self) -> &TransactionRequest {
&self.transaction_request
}
/// Returns the transparent inputs that have been selected to fund the transaction.
pub fn transparent_inputs(&self) -> &[WalletTransparentOutput] {
&self.transparent_inputs
}
/// Returns the Sapling inputs that have been selected to fund the transaction.
pub fn shielded_inputs(&self) -> Option<&ShieldedInputs<NoteRef>> {
self.shielded_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).
pub fn is_shielding(&self) -> bool {
self.is_shielding
}
}
impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Proposal")
.field("transaction_request", &self.transaction_request)
.field("transparent_inputs", &self.transparent_inputs)
.field(
"shielded_inputs",
&self.shielded_inputs().map(|i| i.notes.len()),
)
.field(
"anchor_height",
&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()
}
}
/// The Sapling inputs to a proposed transaction.
#[derive(Clone, PartialEq, Eq)]
pub struct ShieldedInputs<NoteRef> {
anchor_height: BlockHeight,
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
}
impl<NoteRef> ShieldedInputs<NoteRef> {
/// Constructs a [`ShieldedInputs`] from its constituent parts.
pub fn from_parts(
anchor_height: BlockHeight,
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
) -> Self {
Self {
anchor_height,
notes,
}
}
/// Returns the anchor height for Sapling inputs that should be used when constructing the
/// proposed transaction.
pub fn anchor_height(&self) -> BlockHeight {
self.anchor_height
}
/// Returns the list of Sapling notes to be used as inputs to the proposed transaction.
pub fn notes(&self) -> &NonEmpty<ReceivedNote<NoteRef, Note>> {
&self.notes
}
}
/// A strategy for selecting transaction inputs and proposing transaction outputs.
///
/// Proposals should include only economically useful inputs, as determined by `Self::FeeRule`;
@ -525,46 +343,46 @@ where
let mut sapling_outputs = vec![];
#[cfg(feature = "orchard")]
let mut orchard_outputs = vec![];
for payment in transaction_request.payments() {
let mut push_transparent = |taddr: TransparentAddress| {
transparent_outputs.push(TxOut {
value: payment.amount,
script_pubkey: taddr.script(),
});
};
let mut push_sapling = || {
sapling_outputs.push(SaplingPayment(payment.amount));
};
#[cfg(feature = "orchard")]
let mut push_orchard = || {
orchard_outputs.push(OrchardPayment(payment.amount));
};
let mut payment_pools = BTreeMap::new();
for (idx, payment) in transaction_request.payments() {
match &payment.recipient_address {
Address::Transparent(addr) => {
push_transparent(*addr);
payment_pools.insert(*idx, PoolType::Transparent);
transparent_outputs.push(TxOut {
value: payment.amount,
script_pubkey: addr.script(),
});
}
Address::Sapling(_) => {
push_sapling();
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
sapling_outputs.push(SaplingPayment(payment.amount));
}
Address::Unified(addr) => {
#[cfg(feature = "orchard")]
let has_orchard = addr.orchard().is_some();
#[cfg(not(feature = "orchard"))]
let has_orchard = false;
if has_orchard {
#[cfg(feature = "orchard")]
push_orchard();
} else if addr.sapling().is_some() {
push_sapling();
} else if let Some(addr) = addr.transparent() {
push_transparent(*addr);
} else {
return Err(InputSelectorError::Selection(
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr.clone())),
));
if addr.orchard().is_some() {
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Orchard));
orchard_outputs.push(OrchardPayment(payment.amount));
continue;
}
if addr.sapling().is_some() {
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
sapling_outputs.push(SaplingPayment(payment.amount));
continue;
}
if let Some(addr) = addr.transparent() {
payment_pools.insert(*idx, PoolType::Transparent);
transparent_outputs.push(TxOut {
value: payment.amount,
script_pubkey: addr.script(),
});
continue;
}
return Err(InputSelectorError::Selection(
GreedyInputSelectorError::UnsupportedAddress(Box::new(addr.clone())),
));
}
}
}
@ -615,20 +433,18 @@ where
match balance {
Ok(balance) => {
return Ok(Proposal {
return Proposal::single_step(
transaction_request,
transparent_inputs: vec![],
shielded_inputs: NonEmpty::from_vec(shielded_inputs).map(|notes| {
ShieldedInputs {
anchor_height,
notes,
}
}),
payment_pools,
vec![],
NonEmpty::from_vec(shielded_inputs)
.map(|notes| ShieldedInputs::from_parts(anchor_height, notes)),
balance,
fee_rule: (*self.change_strategy.fee_rule()).clone(),
min_target_height: target_height,
is_shielding: false,
});
(*self.change_strategy.fee_rule()).clone(),
target_height,
false,
)
.map_err(InputSelectorError::Proposal);
}
Err(ChangeError::DustInputs { mut sapling, .. }) => {
exclude.append(&mut sapling);
@ -766,15 +582,17 @@ where
};
if balance.total() >= shielding_threshold {
Ok(Proposal {
transaction_request: TransactionRequest::empty(),
Proposal::single_step(
TransactionRequest::empty(),
BTreeMap::new(),
transparent_inputs,
shielded_inputs: None,
None,
balance,
fee_rule: (*self.change_strategy.fee_rule()).clone(),
min_target_height: target_height,
is_shielding: true,
})
(*self.change_strategy.fee_rule()).clone(),
target_height,
true,
)
.map_err(InputSelectorError::Proposal)
} else {
Err(InputSelectorError::InsufficientFunds {
available: balance.total(),

View File

@ -64,9 +64,11 @@
pub use zcash_keys::address;
pub mod data_api;
mod decrypt;
use zcash_keys::address::Address;
pub use zcash_keys::encoding;
pub mod fees;
pub use zcash_keys::keys;
pub mod proposal;
pub mod proto;
pub mod scan;
pub mod scanning;
@ -100,7 +102,7 @@ pub enum ShieldedProtocol {
}
/// A value pool to which the wallet supports sending transaction outputs.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum PoolType {
/// The transparent value pool
Transparent,
@ -108,6 +110,27 @@ pub enum PoolType {
Shielded(ShieldedProtocol),
}
impl PoolType {
pub fn is_receiver(&self, addr: &Address) -> bool {
match addr {
Address::Sapling(_) => matches!(self, PoolType::Shielded(ShieldedProtocol::Sapling)),
Address::Transparent(_) => matches!(self, PoolType::Transparent),
Address::Unified(ua) => match self {
PoolType::Transparent => ua.transparent().is_some(),
PoolType::Shielded(ShieldedProtocol::Sapling) => ua.sapling().is_some(),
#[cfg(zcash_unstable = "orchard")]
PoolType::Shielded(ShieldedProtocol::Orchard) => {
#[cfg(feature = "orchard")]
return ua.orchard().is_some();
#[cfg(not(feature = "orchard"))]
return false;
}
},
}
}
}
impl fmt::Display for PoolType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {

View File

@ -0,0 +1,512 @@
//! Types related to the construction and evaluation of transaction proposals.
use std::{
collections::{BTreeMap, BTreeSet},
fmt::{self, Debug, Display},
};
use nonempty::NonEmpty;
use zcash_primitives::{
consensus::BlockHeight,
transaction::{components::amount::NonNegativeAmount, TxId},
};
use crate::{
fees::TransactionBalance,
wallet::{Note, ReceivedNote, WalletTransparentOutput},
zip321::TransactionRequest,
PoolType, ShieldedProtocol,
};
/// 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.
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,
},
/// 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,
/// A reference to the output of a prior step is invalid.
ReferenceError(StepOutput),
/// An attempted double-spend of a prior step output was detected.
StepDoubleSpend(StepOutput),
/// An attempted double-spend of an output belonging to the wallet was detected.
ChainDoubleSpend(PoolType, TxId, u32),
/// There was a mismatch between the payments in the proposal's transaction request
/// and the payment pool selection values.
PaymentPoolsMismatch,
}
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."
),
ProposalError::ReferenceError(r) => {
write!(f, "No prior step output found for reference {:?}", r)
}
ProposalError::StepDoubleSpend(r) => write!(
f,
"The proposal uses the output of step {:?} in more than one place.",
r
),
ProposalError::ChainDoubleSpend(pool, txid, index) => write!(
f,
"The proposal attempts to spend the same output twice: {}, {}, {}",
pool, txid, index
),
ProposalError::PaymentPoolsMismatch => write!(
f,
"The chosen payment pools did not match the payments of the transaction request."
),
}
}
}
impl std::error::Error for ProposalError {}
/// The Sapling inputs to a proposed transaction.
#[derive(Clone, PartialEq, Eq)]
pub struct ShieldedInputs<NoteRef> {
anchor_height: BlockHeight,
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
}
impl<NoteRef> ShieldedInputs<NoteRef> {
/// Constructs a [`ShieldedInputs`] from its constituent parts.
pub fn from_parts(
anchor_height: BlockHeight,
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
) -> Self {
Self {
anchor_height,
notes,
}
}
/// Returns the anchor height for Sapling inputs that should be used when constructing the
/// proposed transaction.
pub fn anchor_height(&self) -> BlockHeight {
self.anchor_height
}
/// Returns the list of Sapling notes to be used as inputs to the proposed transaction.
pub fn notes(&self) -> &NonEmpty<ReceivedNote<NoteRef, Note>> {
&self.notes
}
}
/// 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> {
let mut consumed_chain_inputs: BTreeSet<(PoolType, TxId, u32)> = BTreeSet::new();
let mut consumed_prior_inputs: BTreeSet<StepOutput> = BTreeSet::new();
for (i, step) in steps.iter().enumerate() {
for prior_ref in step.prior_step_inputs() {
// check that there are no forward references
if prior_ref.step_index() >= i {
return Err(ProposalError::ReferenceError(*prior_ref));
}
// check that the reference is valid
let prior_step = &steps[prior_ref.step_index()];
match prior_ref.output_index() {
StepOutputIndex::Payment(idx) => {
if prior_step.transaction_request().payments().len() <= idx {
return Err(ProposalError::ReferenceError(*prior_ref));
}
}
StepOutputIndex::Change(idx) => {
if prior_step.balance().proposed_change().len() <= idx {
return Err(ProposalError::ReferenceError(*prior_ref));
}
}
}
// check that there are no double-spends
if !consumed_prior_inputs.insert(*prior_ref) {
return Err(ProposalError::StepDoubleSpend(*prior_ref));
}
}
for t_out in step.transparent_inputs() {
let key = (
PoolType::Transparent,
TxId::from_bytes(*t_out.outpoint().hash()),
t_out.outpoint().n(),
);
if !consumed_chain_inputs.insert(key) {
return Err(ProposalError::ChainDoubleSpend(key.0, key.1, key.2));
}
}
for s_out in step.shielded_inputs().iter().flat_map(|i| i.notes().iter()) {
let key = (
match &s_out.note() {
Note::Sapling(_) => PoolType::Shielded(ShieldedProtocol::Sapling),
#[cfg(feature = "orchard")]
Note::Orchard(_) => PoolType::Shielded(ShieldedProtocol::Orchard),
},
*s_out.txid(),
s_out.output_index().into(),
);
if !consumed_chain_inputs.insert(key) {
return Err(ProposalError::ChainDoubleSpend(key.0, key.1, key.2));
}
}
}
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, PartialOrd, Ord)]
pub enum StepOutputIndex {
Payment(usize),
Change(usize),
}
/// A reference to the output of a step in a proposal.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
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,
is_shielding: bool,
}
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.
///
/// Parameters:
/// * `transaction_request`: The ZIP 321 transaction request describing the payments
/// to be made.
/// * `payment_pools`: A map from payment index to pool type. The set of payment indices
/// provided here must exactly match the set of payment indices in the [`TransactionRequest`],
/// and the selected pool for an index must correspond to a valid receiver of the
/// address at that index (or the address itself in the case of bare transparent or Sapling
/// addresses).
/// * `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.
/// * `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,
is_shielding: bool,
) -> Result<Self, ProposalError> {
// Verify that the set of payment pools matches exactly a set of valid payment recipients
if transaction_request.payments().len() != payment_pools.len() {
return Err(ProposalError::PaymentPoolsMismatch);
}
for (idx, pool) in &payment_pools {
if !transaction_request
.payments()
.get(idx)
.iter()
.any(|payment| pool.is_receiver(&payment.recipient_address))
{
return Err(ProposalError::PaymentPoolsMismatch);
}
}
let transparent_input_total = transparent_inputs
.iter()
.map(|out| out.txout().value)
.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 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()
.map_err(|_| ProposalError::RequestTotalInvalid)?;
let output_total = (request_total + balance.total()).ok_or(ProposalError::Overflow)?;
if is_shielding
&& (transparent_input_total == NonNegativeAmount::ZERO
|| shielded_input_total > NonNegativeAmount::ZERO
|| request_total > NonNegativeAmount::ZERO)
{
return Err(ProposalError::ShieldingInvalid);
}
if input_total == output_total {
Ok(Self {
transaction_request,
payment_pools,
transparent_inputs,
shielded_inputs,
prior_step_inputs,
balance,
is_shielding,
})
} else {
Err(ProposalError::BalanceError {
input_total,
output_total,
})
}
}
/// Returns the transaction request that describes the payments to be made.
pub fn transaction_request(&self) -> &TransactionRequest {
&self.transaction_request
}
/// Returns the map from payment index to the pool that has been selected
/// for the output that will fulfill that payment.
pub fn payment_pools(&self) -> &BTreeMap<usize, PoolType> {
&self.payment_pools
}
/// Returns the transparent inputs that have been selected to fund the transaction.
pub fn transparent_inputs(&self) -> &[WalletTransparentOutput] {
&self.transparent_inputs
}
/// Returns the shielded inputs that have been selected to fund the transaction.
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 a flag indicating whether or not the proposed transaction
/// is exclusively wallet-internal (if it does not involve any external
/// recipients).
pub fn is_shielding(&self) -> bool {
self.is_shielding
}
}
impl<NoteRef> Debug for Step<NoteRef> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Step")
.field("transaction_request", &self.transaction_request)
.field("transparent_inputs", &self.transparent_inputs)
.field(
"shielded_inputs",
&self.shielded_inputs().map(|i| i.notes.len()),
)
.field(
"anchor_height",
&self.shielded_inputs().map(|i| i.anchor_height),
)
.field("balance", &self.balance)
.field("is_shielding", &self.is_shielding)
.finish_non_exhaustive()
}
}

View File

@ -2,6 +2,7 @@
use std::{
array::TryFromSliceError,
collections::BTreeMap,
fmt::{self, Display},
io,
};
@ -21,11 +22,9 @@ use zcash_primitives::{
use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
use crate::{
data_api::{
wallet::input_selection::{Proposal, ProposalError, ShieldedInputs},
InputSource,
},
data_api::InputSource,
fees::{ChangeValue, TransactionBalance},
proposal::{Proposal, ProposalError, ShieldedInputs, Step, StepOutput, StepOutputIndex},
zip321::{TransactionRequest, Zip321Error},
PoolType, ShieldedProtocol,
};
@ -217,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.
@ -253,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)
}
@ -318,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()?))
}
@ -334,6 +341,15 @@ impl proposal::ChangeValue {
}
}
impl From<PoolType> for proposal::ValuePool {
fn from(value: PoolType) -> Self {
match value {
PoolType::Transparent => proposal::ValuePool::Transparent,
PoolType::Shielded(p) => p.into(),
}
}
}
impl From<ShieldedProtocol> for proposal::ValuePool {
fn from(value: ShieldedProtocol) -> Self {
match value {
@ -351,54 +367,111 @@ 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 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(),
});
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(),
});
proposal::ProposalStep {
transaction_request,
payment_output_pools,
anchor_height,
inputs,
balance,
is_shielding: step.is_shielding(),
}
})
.collect();
#[allow(deprecated)]
proposal::Proposal {
proto_version: PROPOSAL_SER_V1,
transaction_request,
anchor_height,
inputs,
balance,
fee_rule: match value.fee_rule() {
StandardFeeRule::PreZip313 => proposal::FeeRule::PreZip313,
StandardFeeRule::Zip313 => proposal::FeeRule::Zip313,
@ -406,7 +479,7 @@ impl proposal::Proposal {
}
.into(),
min_target_height: value.min_target_height().into(),
is_shielding: value.is_shielding(),
steps,
}
}
@ -420,6 +493,7 @@ impl proposal::Proposal {
where
DbT: InputSource<Error = DbError>,
{
use self::proposal::proposed_input::Value::*;
match self.proto_version {
PROPOSAL_SER_V1 => {
#[allow(deprecated)]
@ -432,102 +506,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)?;
#[cfg(not(feature = "transparent-inputs"))]
let transparent_inputs = vec![];
#[cfg(feature = "transparent-inputs")]
let mut transparent_inputs = vec![];
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>>>()?;
let mut received_notes = vec![];
for input in self.inputs.iter() {
let txid = input
.parse_txid()
.map_err(ProposalDecodingError::TxIdInvalid)?;
#[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)?;
match input.pool_type()? {
PoolType::Transparent => {
#[cfg(not(feature = "transparent-inputs"))]
return Err(ProposalDecodingError::ValuePoolNotSupported(1));
match out.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,
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,13 +1,35 @@
/// A data structure that describes 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,
/// The fee rule used in constructing this proposal
#[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 = "3")]
pub min_target_height: u32,
/// 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 Proposal {
#[prost(uint32, tag = "1")]
pub proto_version: u32,
pub struct ProposalStep {
/// ZIP 321 serialized transaction request
#[prost(string, tag = "2")]
#[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.
@ -20,24 +42,27 @@ pub struct Proposal {
/// transaction
#[prost(message, optional, tag = "5")]
pub balance: ::core::option::Option<TransactionBalance>,
/// The fee rule used in constructing this proposal
#[prost(enumeration = "FeeRule", tag = "6")]
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 = "7")]
pub min_target_height: u32,
/// A flag indicating whether the proposal is for a shielding transaction,
/// 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 = "8")]
#[prost(bool, tag = "6")]
pub is_shielding: bool,
}
/// The unique identifier and value for each proposed input.
/// A mapping from ZIP 321 payment index to the output pool that has been chosen
/// for that payment, based upon the payment address and the selected inputs to
/// the transaction.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProposedInput {
pub struct PaymentOutputPool {
#[prost(uint32, tag = "1")]
pub payment_index: u32,
#[prost(enumeration = "ValuePool", tag = "2")]
pub value_pool: i32,
}
/// 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 ReceivedOutput {
#[prost(bytes = "vec", tag = "1")]
pub txid: ::prost::alloc::vec::Vec<u8>,
#[prost(enumeration = "ValuePool", tag = "2")]
@ -47,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,
}
@ -61,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

@ -22,6 +22,9 @@ use crate::{address::UnifiedAddress, fees::sapling as sapling_fees, PoolType, Sh
#[cfg(feature = "orchard")]
use crate::fees::orchard as orchard_fees;
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::legacy::keys::{NonHardenedChildIndex, TransparentKeyScope};
/// A unique identifier for a shielded transaction output
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NoteId {
@ -375,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`].
///
@ -395,3 +399,29 @@ pub enum OvkPolicy {
/// recipients, but not by the sender.
Discard,
}
/// Metadata related to the ZIP 32 derivation of a transparent address.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(feature = "transparent-inputs")]
pub struct TransparentAddressMetadata {
scope: TransparentKeyScope,
address_index: NonHardenedChildIndex,
}
#[cfg(feature = "transparent-inputs")]
impl TransparentAddressMetadata {
pub fn new(scope: TransparentKeyScope, address_index: NonHardenedChildIndex) -> Self {
Self {
scope,
address_index,
}
}
pub fn scope(&self) -> TransparentKeyScope {
self.scope
}
pub fn address_index(&self) -> NonHardenedChildIndex {
self.address_index
}
}

View File

@ -6,7 +6,7 @@
//! The specification for ZIP 321 URIs may be found at <https://zips.z.cash/zip-0321>
use core::fmt::Debug;
use std::{
collections::HashMap,
collections::BTreeMap,
fmt::{self, Display},
};
@ -21,9 +21,6 @@ use zcash_primitives::{
transaction::components::amount::NonNegativeAmount,
};
#[cfg(any(test, feature = "test-dependencies"))]
use std::cmp::Ordering;
use crate::address::Address;
/// Errors that may be produced in decoding of payment requests.
@ -139,28 +136,6 @@ impl Payment {
pub(in crate::zip321) fn normalize(&mut self) {
self.other_params.sort();
}
/// Returns a function which compares two normalized payments, with addresses sorted by their
/// string representation given the specified network. This does not perform normalization
/// internally, so payments must be normalized prior to being passed to the comparison function
/// returned from this method.
#[cfg(any(test, feature = "test-dependencies"))]
pub(in crate::zip321) fn compare_normalized<P: consensus::Parameters>(
params: &P,
) -> impl Fn(&Payment, &Payment) -> Ordering + '_ {
move |a: &Payment, b: &Payment| {
let a_addr = a.recipient_address.encode(params);
let b_addr = b.recipient_address.encode(params);
a_addr
.cmp(&b_addr)
.then(a.amount.cmp(&b.amount))
.then(a.memo.cmp(&b.memo))
.then(a.label.cmp(&b.label))
.then(a.message.cmp(&b.message))
.then(a.other_params.cmp(&b.other_params))
}
}
}
/// A ZIP321 transaction request.
@ -171,18 +146,27 @@ impl Payment {
/// payment value in the request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransactionRequest {
payments: Vec<Payment>,
payments: BTreeMap<usize, Payment>,
}
impl TransactionRequest {
/// Constructs a new empty transaction request.
pub fn empty() -> Self {
Self { payments: vec![] }
Self {
payments: BTreeMap::new(),
}
}
/// Constructs a new transaction request that obeys the ZIP-321 invariants
/// Constructs a new transaction request that obeys the ZIP-321 invariants.
pub fn new(payments: Vec<Payment>) -> Result<TransactionRequest, Zip321Error> {
let request = TransactionRequest { payments };
// Payment indices are limited to 4 digits
if payments.len() > 9999 {
return Err(Zip321Error::TooManyPayments(payments.len()));
}
let request = TransactionRequest {
payments: payments.into_iter().enumerate().collect(),
};
// Enforce validity requirements.
if !request.payments.is_empty() {
@ -195,9 +179,27 @@ impl TransactionRequest {
Ok(request)
}
/// Returns the slice of payments that make up this request.
pub fn payments(&self) -> &[Payment] {
&self.payments[..]
/// Constructs a new transaction request from the provided map from payment
/// index to payment.
///
/// Payment index 0 will be mapped to the empty payment index.
pub fn from_indexed(
payments: BTreeMap<usize, Payment>,
) -> Result<TransactionRequest, Zip321Error> {
if let Some(k) = payments.keys().find(|k| **k > 9999) {
// This is not quite the correct error, but close enough.
return Err(Zip321Error::TooManyPayments(*k));
}
Ok(TransactionRequest { payments })
}
/// Returns the map of payments that make up this request.
///
/// This is a map from payment index to payment. Payment index `0` is used to denote
/// the empty payment index in the returned values.
pub fn payments(&self) -> &BTreeMap<usize, Payment> {
&self.payments
}
/// Returns the total value of payments to be made.
@ -205,36 +207,29 @@ impl TransactionRequest {
/// Returns `Err` in the case of overflow, or if the value is
/// outside the range `0..=MAX_MONEY` zatoshis.
pub fn total(&self) -> Result<NonNegativeAmount, ()> {
if self.payments.is_empty() {
Ok(NonNegativeAmount::ZERO)
} else {
self.payments
.iter()
.map(|p| p.amount)
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| (acc? + a).ok_or(()))
}
self.payments
.values()
.map(|p| p.amount)
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| (acc? + a).ok_or(()))
}
/// A utility for use in tests to help check round-trip serialization properties.
#[cfg(any(test, feature = "test-dependencies"))]
pub(in crate::zip321) fn normalize<P: consensus::Parameters>(&mut self, params: &P) {
for p in &mut self.payments {
pub(in crate::zip321) fn normalize(&mut self) {
for p in self.payments.values_mut() {
p.normalize();
}
self.payments.sort_by(Payment::compare_normalized(params));
}
/// A utility for use in tests to help check round-trip serialization properties.
/// by comparing a two transaction requests for equality after normalization.
#[cfg(all(test, feature = "test-dependencies"))]
pub(in crate::zip321) fn normalize_and_eq<P: consensus::Parameters>(
params: &P,
pub(in crate::zip321) fn normalize_and_eq(
a: &mut TransactionRequest,
b: &mut TransactionRequest,
) -> bool {
a.normalize(params);
b.normalize(params);
a.normalize();
b.normalize();
a == b
}
@ -275,9 +270,10 @@ impl TransactionRequest {
)
}
match &self.payments[..] {
[] => "zcash:".to_string(),
[payment] => {
match self.payments.len() {
0 => "zcash:".to_string(),
1 if *self.payments.iter().next().unwrap().0 == 0 => {
let (_, payment) = self.payments.iter().next().unwrap();
let query_params = payment_params(payment, None)
.into_iter()
.collect::<Vec<String>>();
@ -293,12 +289,12 @@ impl TransactionRequest {
let query_params = self
.payments
.iter()
.enumerate()
.flat_map(|(i, payment)| {
let idx = if *i == 0 { None } else { Some(*i) };
let primary_address = payment.recipient_address.clone();
std::iter::empty()
.chain(Some(render::addr_param(params, &primary_address, Some(i))))
.chain(payment_params(payment, Some(i)))
.chain(Some(render::addr_param(params, &primary_address, idx)))
.chain(payment_params(payment, idx))
})
.collect::<Vec<String>>();
@ -325,7 +321,7 @@ impl TransactionRequest {
};
// Construct sets of payment parameters, keyed by the payment index.
let mut params_by_index: HashMap<usize, Vec<parse::Param>> = HashMap::new();
let mut params_by_index: BTreeMap<usize, Vec<parse::Param>> = BTreeMap::new();
// Add the primary address, if any, to the index.
if let Some(p) = primary_addr_param {
@ -352,8 +348,8 @@ impl TransactionRequest {
// Build the actual payment values from the index.
params_by_index
.into_iter()
.map(|(i, params)| parse::to_payment(params, i))
.collect::<Result<Vec<_>, _>>()
.map(|(i, params)| parse::to_payment(params, i).map(|payment| (i, payment)))
.collect::<Result<BTreeMap<usize, Payment>, _>>()
.map(|payments| TransactionRequest { payments })
}
}
@ -797,9 +793,17 @@ pub mod testing {
}
prop_compose! {
pub fn arb_zip321_request()(payments in vec(arb_zip321_payment(), 1..10)) -> TransactionRequest {
let mut req = TransactionRequest { payments };
req.normalize(&TEST_NETWORK); // just to make test comparisons easier
pub fn arb_zip321_request()(payments in btree_map(0usize..10000, arb_zip321_payment(), 1..10)) -> TransactionRequest {
let mut req = TransactionRequest::from_indexed(payments).unwrap();
req.normalize(); // just to make test comparisons easier
req
}
}
prop_compose! {
pub fn arb_zip321_request_sequential()(payments in vec(arb_zip321_payment(), 1..10)) -> TransactionRequest {
let mut req = TransactionRequest::new(payments).unwrap();
req.normalize(); // just to make test comparisons easier
req
}
}
@ -883,8 +887,8 @@ mod tests {
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k?amount=3768769.02796286&message=";
let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, uri).unwrap();
let expected = TransactionRequest {
payments: vec![
let expected = TransactionRequest::new(
vec![
Payment {
recipient_address: Address::Sapling(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: NonNegativeAmount::const_from_u64(376876902796286),
@ -894,7 +898,7 @@ mod tests {
other_params: vec![],
}
]
};
).unwrap();
assert_eq!(parse_result, expected);
}
@ -904,8 +908,8 @@ mod tests {
let uri = "zcash:ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k";
let parse_result = TransactionRequest::from_uri(&TEST_NETWORK, uri).unwrap();
let expected = TransactionRequest {
payments: vec![
let expected = TransactionRequest::new(
vec![
Payment {
recipient_address: Address::Sapling(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: NonNegativeAmount::ZERO,
@ -915,15 +919,15 @@ mod tests {
other_params: vec![],
}
]
};
).unwrap();
assert_eq!(parse_result, expected);
}
#[test]
fn test_zip321_roundtrip_empty_message() {
let req = TransactionRequest {
payments: vec![
let req = TransactionRequest::new(
vec![
Payment {
recipient_address: Address::Sapling(decode_payment_address(TEST_NETWORK.hrp_sapling_payment_address(), "ztestsapling1n65uaftvs2g7075q2x2a04shfk066u3lldzxsrprfrqtzxnhc9ps73v4lhx4l9yfxj46sl0q90k").unwrap()),
amount: NonNegativeAmount::ZERO,
@ -933,7 +937,7 @@ mod tests {
other_params: vec![]
}
]
};
).unwrap();
check_roundtrip(req);
}
@ -970,19 +974,19 @@ mod tests {
let valid_1 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase";
let v1r = TransactionRequest::from_uri(&TEST_NETWORK, valid_1).unwrap();
assert_eq!(
v1r.payments.get(0).map(|p| p.amount),
v1r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(100000000))
);
let valid_2 = "zcash:?address=tmEZhbWHTpdKMw5it8YDspUXSMGQyFwovpU&amount=123.456&address.1=ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez&amount.1=0.789&memo.1=VGhpcyBpcyBhIHVuaWNvZGUgbWVtbyDinKjwn6aE8J-PhvCfjok";
let mut v2r = TransactionRequest::from_uri(&TEST_NETWORK, valid_2).unwrap();
v2r.normalize(&TEST_NETWORK);
v2r.normalize();
assert_eq!(
v2r.payments.get(0).map(|p| p.amount),
v2r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(12345600000))
);
assert_eq!(
v2r.payments.get(1).map(|p| p.amount),
v2r.payments.get(&1).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(78900000))
);
@ -991,7 +995,7 @@ mod tests {
let valid_3 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=20999999.99999999";
let v3r = TransactionRequest::from_uri(&TEST_NETWORK, valid_3).unwrap();
assert_eq!(
v3r.payments.get(0).map(|p| p.amount),
v3r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(2099999999999999u64))
);
@ -1000,7 +1004,7 @@ mod tests {
let valid_4 = "zcash:ztestsapling10yy2ex5dcqkclhc7z7yrnjq2z6feyjad56ptwlfgmy77dmaqqrl9gyhprdx59qgmsnyfska2kez?amount=21000000";
let v4r = TransactionRequest::from_uri(&TEST_NETWORK, valid_4).unwrap();
assert_eq!(
v4r.payments.get(0).map(|p| p.amount),
v4r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(2100000000000000u64))
);
}
@ -1021,7 +1025,7 @@ mod tests {
let valid_1 = "zcash:zregtestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle7505hlz3?amount=1&memo=VGhpcyBpcyBhIHNpbXBsZSBtZW1vLg&message=Thank%20you%20for%20your%20purchase";
let v1r = TransactionRequest::from_uri(&params, valid_1).unwrap();
assert_eq!(
v1r.payments.get(0).map(|p| p.amount),
v1r.payments.get(&0).map(|p| p.amount),
Some(NonNegativeAmount::const_from_u64(100000000))
);
}
@ -1140,13 +1144,13 @@ mod tests {
fn prop_zip321_roundtrip_request(mut req in arb_zip321_request()) {
let req_uri = req.to_uri(&TEST_NETWORK);
let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &req_uri).unwrap();
assert!(TransactionRequest::normalize_and_eq(&TEST_NETWORK, &mut parsed, &mut req));
assert!(TransactionRequest::normalize_and_eq(&mut parsed, &mut req));
}
#[test]
fn prop_zip321_roundtrip_uri(uri in arb_zip321_uri()) {
let mut parsed = TransactionRequest::from_uri(&TEST_NETWORK, &uri).unwrap();
parsed.normalize(&TEST_NETWORK);
parsed.normalize();
let serialized = parsed.to_uri(&TEST_NETWORK);
assert_eq!(serialized, uri)
}

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

@ -48,7 +48,6 @@ use shardtree::{error::ShardTreeError, ShardTree};
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
legacy::TransparentAddress,
memo::{Memo, MemoBytes},
transaction::{
components::amount::{Amount, NonNegativeAmount},
@ -64,8 +63,8 @@ use zcash_client_backend::{
chain::{BlockSource, CommitmentTreeRoot},
scanning::{ScanPriority, ScanRange},
AccountBirthday, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery,
ScannedBlock, SentTransaction, TransparentAddressMetadata, WalletCommitmentTrees,
WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
ScannedBlock, SentTransaction, WalletCommitmentTrees, WalletRead, WalletSummary,
WalletWrite, SAPLING_SHARD_HEIGHT,
},
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock,
@ -76,7 +75,10 @@ use zcash_client_backend::{
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::transaction::components::OutPoint;
use {
zcash_client_backend::wallet::TransparentAddressMetadata,
zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint},
};
#[cfg(feature = "unstable")]
use {
@ -343,36 +345,21 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
}
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_receivers(
&self,
_account: AccountId,
) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, Self::Error> {
#[cfg(feature = "transparent-inputs")]
return wallet::get_transparent_receivers(self.conn.borrow(), &self.params, _account);
#[cfg(not(feature = "transparent-inputs"))]
panic!(
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
wallet::get_transparent_receivers(self.conn.borrow(), &self.params, _account)
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_balances(
&self,
_account: AccountId,
_max_height: BlockHeight,
) -> Result<HashMap<TransparentAddress, Amount>, Self::Error> {
#[cfg(feature = "transparent-inputs")]
return wallet::get_transparent_balances(
self.conn.borrow(),
&self.params,
_account,
_max_height,
);
#[cfg(not(feature = "transparent-inputs"))]
panic!(
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
wallet::get_transparent_balances(self.conn.borrow(), &self.params, _account, _max_height)
}
#[cfg(feature = "orchard")]

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,15 +30,14 @@ use zcash_client_backend::{
self,
chain::{scan_cached_blocks, BlockSource, ScanSummary},
wallet::{
create_proposed_transaction, create_spend_to_address,
input_selection::{
GreedyInputSelector, GreedyInputSelectorError, InputSelector, Proposal,
},
create_proposed_transactions, create_spend_to_address,
input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector},
propose_standard_transfer_to_address, propose_transfer, spend,
},
AccountBalance, AccountBirthday, WalletRead, WalletSummary, WalletWrite,
},
keys::UnifiedSpendingKey,
proposal::Proposal,
proto::compact_formats::{
self as compact, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
},
@ -443,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,
@ -478,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,
@ -609,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,
@ -629,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,
@ -651,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

@ -112,9 +112,12 @@ use {
crate::UtxoId,
rusqlite::Row,
std::collections::BTreeSet,
zcash_client_backend::{data_api::TransparentAddressMetadata, wallet::WalletTransparentOutput},
zcash_client_backend::wallet::{TransparentAddressMetadata, WalletTransparentOutput},
zcash_primitives::{
legacy::{keys::IncomingViewingKey, NonHardenedChildIndex, Script, TransparentAddress},
legacy::{
keys::{IncomingViewingKey, NonHardenedChildIndex},
Script, TransparentAddress,
},
transaction::components::{OutPoint, TxOut},
},
};
@ -395,7 +398,10 @@ pub(crate) fn get_transparent_receivers<P: consensus::Parameters>(
ret.insert(
*taddr,
Some(TransparentAddressMetadata::new(Scope::External, index)),
Some(TransparentAddressMetadata::new(
Scope::External.into(),
index,
)),
);
}
}
@ -404,7 +410,7 @@ pub(crate) fn get_transparent_receivers<P: consensus::Parameters>(
ret.insert(
taddr,
Some(TransparentAddressMetadata::new(
Scope::External,
Scope::External.into(),
child_index,
)),
);
@ -2310,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

@ -2,7 +2,6 @@
use std::collections::HashSet;
use rusqlite::{self, types::ToSql, OptionalExtension};
use schemer::{self};
use schemer_rusqlite::RusqliteMigration;
use uuid::Uuid;
@ -397,7 +396,7 @@ mod tests {
fn migrate_from_wm2() {
use zcash_client_backend::keys::UnifiedAddressRequest;
use zcash_primitives::{
legacy::NonHardenedChildIndex, transaction::components::amount::NonNegativeAmount,
legacy::keys::NonHardenedChildIndex, transaction::components::amount::NonNegativeAmount,
};
use crate::UA_TRANSPARENT;

View File

@ -292,7 +292,7 @@ mod tests {
use zcash_primitives::{
block::BlockHash,
consensus::{BlockHeight, Network, NetworkUpgrade, Parameters},
legacy::{keys::IncomingViewingKey, NonHardenedChildIndex},
legacy::keys::{IncomingViewingKey, NonHardenedChildIndex},
memo::MemoBytes,
transaction::{
builder::{BuildConfig, BuildResult, Builder},

View File

@ -521,8 +521,13 @@ pub(crate) mod tests {
#[cfg(feature = "transparent-inputs")]
use {
zcash_client_backend::wallet::WalletTransparentOutput,
zcash_primitives::transaction::components::{OutPoint, TxOut},
zcash_client_backend::{
fees::TransactionBalance, proposal::Step, wallet::WalletTransparentOutput, PoolType,
},
zcash_primitives::{
legacy::keys::IncomingViewingKey,
transaction::components::{OutPoint, TxOut},
},
};
pub(crate) fn test_prover() -> impl SpendProver + OutputProver {
@ -530,7 +535,7 @@ pub(crate) mod tests {
}
#[test]
fn send_proposed_transfer() {
fn send_single_step_proposed_transfer() {
let mut st = TestBuilder::new()
.with_block_cache()
.with_test_account(AccountBirthday::from_sapling_activation)
@ -589,10 +594,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
@ -677,6 +682,158 @@ pub(crate) mod tests {
);
}
#[test]
#[cfg(feature = "transparent-inputs")]
fn send_multi_step_proposed_transfer() {
use nonempty::NonEmpty;
use zcash_client_backend::proposal::{Proposal, StepOutput, StepOutputIndex};
let mut st = TestBuilder::new()
.with_block_cache()
.with_test_account(AccountBirthday::from_sapling_activation)
.build();
let (account, usk, _) = st.test_account().unwrap();
let dfvk = st.test_account_sapling().unwrap();
// Add funds to the wallet in a single note
let value = NonNegativeAmount::const_from_u64(65000);
let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
st.scan_cached_blocks(h, 1);
// Spendable balance matches total balance
assert_eq!(st.get_total_balance(account), value);
assert_eq!(st.get_spendable_balance(account, 1), value);
assert_eq!(
block_max_scanned(&st.wallet().conn, &st.wallet().params)
.unwrap()
.unwrap()
.block_height(),
h
);
// Generate a single-step proposal. Then, instead of executing that proposal,
// we will use its only step as the first step in a multi-step proposal that
// spends the first step's output.
// The first step will deshield to the wallet's default transparent address
let to0 = Address::Transparent(usk.default_transparent_address().0);
let request0 = zip321::TransactionRequest::new(vec![Payment {
recipient_address: to0,
amount: NonNegativeAmount::const_from_u64(50000),
memo: None,
label: None,
message: None,
other_params: vec![],
}])
.unwrap();
let fee_rule = StandardFeeRule::Zip317;
let input_selector = GreedyInputSelector::new(
standard::SingleOutputChangeStrategy::new(fee_rule, None),
DustOutputPolicy::default(),
);
let proposal0 = st
.propose_transfer(
account,
&input_selector,
request0,
NonZeroU32::new(1).unwrap(),
)
.unwrap();
let min_target_height = proposal0.min_target_height();
let step0 = &proposal0.steps().head;
assert!(step0.balance().proposed_change().is_empty());
assert_eq!(
step0.balance().fee_required(),
NonNegativeAmount::const_from_u64(15000)
);
// We'll use an internal transparent address that hasn't been added to the wallet
// to simulate an external transparent recipient.
let to1 = Address::Transparent(
usk.transparent()
.to_account_pubkey()
.derive_internal_ivk()
.unwrap()
.default_address()
.0,
);
let request1 = zip321::TransactionRequest::new(vec![Payment {
recipient_address: to1,
amount: NonNegativeAmount::const_from_u64(40000),
memo: None,
label: None,
message: None,
other_params: vec![],
}])
.unwrap();
let step1 = Step::from_parts(
&[step0.clone()],
request1,
[(0, PoolType::Transparent)].into_iter().collect(),
vec![],
None,
vec![StepOutput::new(0, StepOutputIndex::Payment(0))],
TransactionBalance::new(vec![], NonNegativeAmount::const_from_u64(10000)).unwrap(),
false,
)
.unwrap();
let proposal = Proposal::multi_step(
fee_rule,
min_target_height,
NonEmpty::from_vec(vec![step0.clone(), step1]).unwrap(),
)
.unwrap();
let create_proposed_result =
st.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal);
assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 2);
let txids = create_proposed_result.unwrap();
// Verify that the stored sent outputs match what we're expecting
let mut stmt_sent = st
.wallet()
.conn
.prepare(
"SELECT value
FROM sent_notes
JOIN transactions ON transactions.id_tx = sent_notes.tx
WHERE transactions.txid = ?",
)
.unwrap();
let confirmed_sent = txids
.iter()
.map(|sent_txid| {
// check that there's a sent output with the correct value corresponding to
stmt_sent
.query(rusqlite::params![sent_txid.as_ref()])
.unwrap()
.mapped(|row| {
let value: u32 = row.get(0)?;
Ok((sent_txid, value))
})
.collect::<Result<Vec<_>, _>>()
.unwrap()
})
.collect::<Vec<_>>();
assert_eq!(
confirmed_sent.get(0).and_then(|v| v.get(0)),
Some(&(&txids[0], 50000))
);
assert_eq!(
confirmed_sent.get(1).and_then(|v| v.get(0)),
Some(&(&txids[1], 40000))
);
}
#[test]
#[allow(deprecated)]
fn create_to_address_fails_on_incorrect_usk() {
@ -864,8 +1021,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 +1078,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 +1157,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 +1223,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 +1327,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 +1389,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 +1467,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 +1598,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);

View File

@ -8,7 +8,7 @@ use zcash_primitives::{
use crate::address::UnifiedAddress;
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::legacy::NonHardenedChildIndex;
use zcash_primitives::legacy::keys::NonHardenedChildIndex;
#[cfg(feature = "transparent-inputs")]
use {
@ -815,7 +815,7 @@ mod tests {
#[cfg(feature = "transparent-inputs")]
#[test]
fn pk_to_taddr() {
use zcash_primitives::legacy::NonHardenedChildIndex;
use zcash_primitives::legacy::keys::NonHardenedChildIndex;
let taddr =
legacy::keys::AccountPrivKey::from_seed(&MAIN_NETWORK, &seed(), AccountId::ZERO)

View File

@ -7,7 +7,8 @@ and this library adheres to Rust's notion of
## [Unreleased]
### Added
- `zcash_primitives::legacy::keys::NonHardenedChildIndex`
- `zcash_primitives::legacy::keys::{NonHardenedChildIndex, TransparentKeyScope}`
- `zcash_primitives::legacy::keys::AccountPrivKey::derive_secret_key`
- Dependency on `bellman 0.14`.
- `zcash_primitives::consensus::sapling_zip212_enforcement`
- `zcash_primitives::transaction`:
@ -127,7 +128,8 @@ and this library adheres to Rust's notion of
- `zcash_client_backend` changes related to `local-consensus` feature:
- added tests that verify `zip321` supports Payment URIs with `Local(P)`
network parameters.
- `zcash_primitives::legacy::keys::derive_external_secret_key` parameter type changed from `u32` to `NonHardenedChildIndex`.
- `zcash_primitives::legacy::keys::{derive_external_secret_key, derive_internal_secret_key}`
arguments changed from `u32` to `NonHardenedChildIndex`.
### Removed
- `zcash_primitives::constants`:

View File

@ -6,11 +6,6 @@ use std::fmt;
use std::io::{self, Read, Write};
use std::ops::Shl;
#[cfg(feature = "transparent-inputs")]
use hdwallet::KeyIndex;
use subtle::{Choice, ConstantTimeEq};
use zcash_encoding::Vector;
#[cfg(feature = "transparent-inputs")]
@ -407,63 +402,6 @@ impl TransparentAddress {
}
}
/// A child index for a derived transparent address.
///
/// Only NON-hardened derivation is supported.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NonHardenedChildIndex(u32);
impl ConstantTimeEq for NonHardenedChildIndex {
fn ct_eq(&self, other: &Self) -> Choice {
self.0.ct_eq(&other.0)
}
}
impl NonHardenedChildIndex {
pub const ZERO: NonHardenedChildIndex = NonHardenedChildIndex(0);
/// Parses the given ZIP 32 child index.
///
/// Returns `None` if the hardened bit is set.
pub fn from_index(i: u32) -> Option<Self> {
if i < (1 << 31) {
Some(NonHardenedChildIndex(i))
} else {
None
}
}
/// Returns the index as a 32-bit integer.
pub fn index(&self) -> u32 {
self.0
}
pub fn next(&self) -> Option<Self> {
// overflow cannot happen because self.0 is 31 bits, and the next index is at most 32 bits
// which in that case would lead from_index to return None.
Self::from_index(self.0 + 1)
}
}
#[cfg(feature = "transparent-inputs")]
impl TryFrom<KeyIndex> for NonHardenedChildIndex {
type Error = ();
fn try_from(value: KeyIndex) -> Result<Self, Self::Error> {
match value {
KeyIndex::Normal(i) => NonHardenedChildIndex::from_index(i).ok_or(()),
KeyIndex::Hardened(_) => Err(()),
}
}
}
#[cfg(feature = "transparent-inputs")]
impl From<NonHardenedChildIndex> for KeyIndex {
fn from(value: NonHardenedChildIndex) -> Self {
Self::Normal(value.index())
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use proptest::prelude::{any, prop_compose};
@ -479,9 +417,7 @@ pub mod testing {
#[cfg(test)]
mod tests {
use super::{NonHardenedChildIndex, OpCode, Script, TransparentAddress};
use hdwallet::KeyIndex;
use subtle::ConstantTimeEq;
use super::{OpCode, Script, TransparentAddress};
#[test]
fn script_opcode() {
@ -548,55 +484,4 @@ mod tests {
);
assert_eq!(addr.script().address(), Some(addr));
}
#[test]
fn nonhardened_indexes_accepted() {
assert_eq!(0, NonHardenedChildIndex::from_index(0).unwrap().index());
assert_eq!(
0x7fffffff,
NonHardenedChildIndex::from_index(0x7fffffff)
.unwrap()
.index()
);
}
#[test]
fn hardened_indexes_rejected() {
assert!(NonHardenedChildIndex::from_index(0x80000000).is_none());
assert!(NonHardenedChildIndex::from_index(0xffffffff).is_none());
}
#[test]
fn nonhardened_index_next() {
assert_eq!(1, NonHardenedChildIndex::ZERO.next().unwrap().index());
assert!(NonHardenedChildIndex::from_index(0x7fffffff)
.unwrap()
.next()
.is_none());
}
#[test]
fn nonhardened_index_ct_eq() {
assert!(check(
NonHardenedChildIndex::ZERO,
NonHardenedChildIndex::ZERO
));
assert!(!check(
NonHardenedChildIndex::ZERO,
NonHardenedChildIndex::ZERO.next().unwrap()
));
fn check<T: ConstantTimeEq>(v1: T, v2: T) -> bool {
v1.ct_eq(&v2).into()
}
}
#[test]
#[cfg(feature = "transparent-inputs")]
fn nonhardened_index_tryfrom_keyindex() {
let nh: NonHardenedChildIndex = KeyIndex::Normal(0).try_into().unwrap();
assert_eq!(nh.index(), 0);
assert!(NonHardenedChildIndex::try_from(KeyIndex::Hardened(0)).is_err());
}
}

View File

@ -6,11 +6,99 @@ use hdwallet::{
};
use secp256k1::PublicKey;
use sha2::{Digest, Sha256};
use subtle::{Choice, ConstantTimeEq};
use zcash_spec::PrfExpand;
use crate::{consensus, zip32::AccountId};
use super::{NonHardenedChildIndex, TransparentAddress};
use super::TransparentAddress;
/// The scope of a transparent key.
///
/// This type can represent [`zip32`] internal and external scopes, as well as custom scopes that
/// may be used in non-hardened derivation at the `change` level of the BIP 44 key path.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TransparentKeyScope(u32);
impl TransparentKeyScope {
pub fn custom(i: u32) -> Option<Self> {
if i < (1 << 31) {
Some(TransparentKeyScope(i))
} else {
None
}
}
}
impl From<zip32::Scope> for TransparentKeyScope {
fn from(value: zip32::Scope) -> Self {
match value {
zip32::Scope::External => TransparentKeyScope(0),
zip32::Scope::Internal => TransparentKeyScope(1),
}
}
}
impl From<TransparentKeyScope> for KeyIndex {
fn from(value: TransparentKeyScope) -> Self {
KeyIndex::Normal(value.0)
}
}
/// A child index for a derived transparent address.
///
/// Only NON-hardened derivation is supported.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NonHardenedChildIndex(u32);
impl ConstantTimeEq for NonHardenedChildIndex {
fn ct_eq(&self, other: &Self) -> Choice {
self.0.ct_eq(&other.0)
}
}
impl NonHardenedChildIndex {
pub const ZERO: NonHardenedChildIndex = NonHardenedChildIndex(0);
/// Parses the given ZIP 32 child index.
///
/// Returns `None` if the hardened bit is set.
pub fn from_index(i: u32) -> Option<Self> {
if i < (1 << 31) {
Some(NonHardenedChildIndex(i))
} else {
None
}
}
/// Returns the index as a 32-bit integer.
pub fn index(&self) -> u32 {
self.0
}
pub fn next(&self) -> Option<Self> {
// overflow cannot happen because self.0 is 31 bits, and the next index is at most 32 bits
// which in that case would lead from_index to return None.
Self::from_index(self.0 + 1)
}
}
impl TryFrom<KeyIndex> for NonHardenedChildIndex {
type Error = ();
fn try_from(value: KeyIndex) -> Result<Self, Self::Error> {
match value {
KeyIndex::Normal(i) => NonHardenedChildIndex::from_index(i).ok_or(()),
KeyIndex::Hardened(_) => Err(()),
}
}
}
impl From<NonHardenedChildIndex> for KeyIndex {
fn from(value: NonHardenedChildIndex) -> Self {
Self::Normal(value.index())
}
}
/// A [BIP44] private key at the account path level `m/44'/<coin_type>'/<account>'`.
///
@ -44,28 +132,35 @@ impl AccountPrivKey {
AccountPubKey(ExtendedPubKey::from_private_key(&self.0))
}
/// Derives the BIP44 private spending key for the child path
/// `m/44'/<coin_type>'/<account>'/<scope>/<child_index>`.
pub fn derive_secret_key(
&self,
scope: TransparentKeyScope,
child_index: NonHardenedChildIndex,
) -> Result<secp256k1::SecretKey, hdwallet::error::Error> {
self.0
.derive_private_key(scope.into())?
.derive_private_key(child_index.into())
.map(|k| k.private_key)
}
/// Derives the BIP44 private spending key for the external (incoming payment) child path
/// `m/44'/<coin_type>'/<account>'/0/<child_index>`.
pub fn derive_external_secret_key(
&self,
child_index: NonHardenedChildIndex,
) -> Result<secp256k1::SecretKey, hdwallet::error::Error> {
self.0
.derive_private_key(KeyIndex::Normal(0))?
.derive_private_key(child_index.into())
.map(|k| k.private_key)
self.derive_secret_key(zip32::Scope::External.into(), child_index)
}
/// Derives the BIP44 private spending key for the internal (change) child path
/// `m/44'/<coin_type>'/<account>'/1/<child_index>`.
pub fn derive_internal_secret_key(
&self,
child_index: u32,
child_index: NonHardenedChildIndex,
) -> Result<secp256k1::SecretKey, hdwallet::error::Error> {
self.0
.derive_private_key(KeyIndex::Normal(1))?
.derive_private_key(KeyIndex::Normal(child_index))
.map(|k| k.private_key)
self.derive_secret_key(zip32::Scope::Internal.into(), child_index)
}
/// Returns the `AccountPrivKey` serialized using the encoding for a
@ -292,7 +387,11 @@ impl ExternalOvk {
#[cfg(test)]
mod tests {
use hdwallet::KeyIndex;
use subtle::ConstantTimeEq;
use super::AccountPubKey;
use super::NonHardenedChildIndex;
#[test]
fn check_ovk_test_vectors() {
@ -539,4 +638,54 @@ mod tests {
assert_eq!(tv.external_ovk, external.as_bytes());
}
}
#[test]
fn nonhardened_indexes_accepted() {
assert_eq!(0, NonHardenedChildIndex::from_index(0).unwrap().index());
assert_eq!(
0x7fffffff,
NonHardenedChildIndex::from_index(0x7fffffff)
.unwrap()
.index()
);
}
#[test]
fn hardened_indexes_rejected() {
assert!(NonHardenedChildIndex::from_index(0x80000000).is_none());
assert!(NonHardenedChildIndex::from_index(0xffffffff).is_none());
}
#[test]
fn nonhardened_index_next() {
assert_eq!(1, NonHardenedChildIndex::ZERO.next().unwrap().index());
assert!(NonHardenedChildIndex::from_index(0x7fffffff)
.unwrap()
.next()
.is_none());
}
#[test]
fn nonhardened_index_ct_eq() {
assert!(check(
NonHardenedChildIndex::ZERO,
NonHardenedChildIndex::ZERO
));
assert!(!check(
NonHardenedChildIndex::ZERO,
NonHardenedChildIndex::ZERO.next().unwrap()
));
fn check<T: ConstantTimeEq>(v1: T, v2: T) -> bool {
v1.ct_eq(&v2).into()
}
}
#[test]
fn nonhardened_index_tryfrom_keyindex() {
let nh: NonHardenedChildIndex = KeyIndex::Normal(0).try_into().unwrap();
assert_eq!(nh.index(), 0);
assert!(NonHardenedChildIndex::try_from(KeyIndex::Hardened(0)).is_err());
}
}

View File

@ -950,7 +950,7 @@ mod tests {
#[cfg(feature = "transparent-inputs")]
fn binding_sig_absent_if_no_shielded_spend_or_output() {
use crate::consensus::NetworkUpgrade;
use crate::legacy::NonHardenedChildIndex;
use crate::legacy::keys::NonHardenedChildIndex;
use crate::transaction::builder::{self, TransparentBuilder};
let sapling_activation_height = TEST_NETWORK