Merge pull request #1187 from nuttycom/proposal_resolved_receivers
`zcash_client_backend`: Updates to the `Proposal` data structure
This commit is contained in:
commit
c6656c108b
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(¶ms, 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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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,
|
||||
¶ms,
|
||||
&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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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`:
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue