Merge pull request #1105 from nuttycom/wallet/orchard_spends_and_outputs

zcash_client_backend: add Orchard spends and outputs to transaction construction
This commit is contained in:
str4d 2024-02-22 01:12:18 +00:00 committed by GitHub
commit 04343e16f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 825 additions and 238 deletions

View File

@ -17,6 +17,8 @@ and this library adheres to Rust's notion of
- `BlockMetadata::orchard_tree_size` - `BlockMetadata::orchard_tree_size`
- `ScannedBlock::orchard` - `ScannedBlock::orchard`
- `ScannedBlockCommitments::orchard` - `ScannedBlockCommitments::orchard`
- `ORCHARD_SHARD_HEIGHT`
- `BlockMetadata::orchard_tree_size`
- `zcash_client_backend::fees::orchard` - `zcash_client_backend::fees::orchard`
- `zcash_client_backend::fees::ChangeValue::orchard` - `zcash_client_backend::fees::ChangeValue::orchard`
- `zcash_client_backend::wallet`: - `zcash_client_backend::wallet`:
@ -29,6 +31,10 @@ and this library adheres to Rust's notion of
- Changes to the `WalletRead` trait: - Changes to the `WalletRead` trait:
- Added `get_orchard_nullifiers` - Added `get_orchard_nullifiers`
- `ShieldedProtocol` has a new `Orchard` variant. - `ShieldedProtocol` has a new `Orchard` variant.
- `WalletCommitmentTrees` has new members when the `orchard` feature is enabled:
- `type OrchardShardStore`
- `fn with_orchard_tree_mut`
- `fn put_orchard_subtree_roots`
- `zcash_client_backend::fees`: - `zcash_client_backend::fees`:
- Arguments to `ChangeStrategy::compute_balance` have changed. - Arguments to `ChangeStrategy::compute_balance` have changed.
@ -59,6 +65,7 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::wallet`: - `zcash_client_backend::wallet`:
- `Note` - `Note`
- `ReceivedNote` - `ReceivedNote`
- `Recipient::{map_internal_account, internal_account_transpose_option}`
- `WalletSaplingOutput::recipient_key_scope` - `WalletSaplingOutput::recipient_key_scope`
- `TransparentAddressMetadata` (which replaces `zcash_keys::address::AddressMetadata`). - `TransparentAddressMetadata` (which replaces `zcash_keys::address::AddressMetadata`).
- `impl {Debug, Clone} for OvkPolicy` - `impl {Debug, Clone} for OvkPolicy`
@ -121,7 +128,7 @@ and this library adheres to Rust's notion of
backend-specific note identifier. The related `NoteRef` type parameter has backend-specific note identifier. The related `NoteRef` type parameter has
been removed from `error::Error`. been removed from `error::Error`.
- New variants have been added: - New variants have been added:
- `Error::UnsupportedPoolType` - `Error::UnsupportedChangeType`
- `Error::NoSupportedReceivers` - `Error::NoSupportedReceivers`
- `Error::NoSpendingKey` - `Error::NoSpendingKey`
- `Error::Proposal` - `Error::Proposal`
@ -147,9 +154,9 @@ and this library adheres to Rust's notion of
the database identifiers for its contained notes by universally quantifying the database identifiers for its contained notes by universally quantifying
the `NoteRef` type parameter. the `NoteRef` type parameter.
- It returns a `NonEmpty<TxId>` instead of a single `TxId` value. - It returns a `NonEmpty<TxId>` instead of a single `TxId` value.
- `wallet::create_spend_to_address` now takes an additional `change_memo` - `wallet::create_spend_to_address` now takes additional `change_memo` and
argument. It also returns its result as a `NonEmpty<TxId>` instead of a `fallback_change_pool` arguments. It also returns its result as a
single `TxId`. `NonEmpty<TxId>` instead of a single `TxId`.
- `wallet::spend` returns its result as a `NonEmpty<TxId>` instead of a - `wallet::spend` returns its result as a `NonEmpty<TxId>` instead of a
single `TxId`. single `TxId`.
- The error type of `wallet::create_spend_to_address` has been changed to use - The error type of `wallet::create_spend_to_address` has been changed to use
@ -219,13 +226,20 @@ and this library adheres to Rust's notion of
now also provides the output pool to which change should be sent and an now also provides the output pool to which change should be sent and an
optional memo to be associated with the change output. optional memo to be associated with the change output.
- `ChangeError` has a new `BundleError` variant. - `ChangeError` has a new `BundleError` variant.
- `fixed::SingleOutputChangeStrategy::new` and - `fixed::SingleOutputChangeStrategy::new`,
`zip317::SingleOutputChangeStrategy::new` each now accept an additional `zip317::SingleOutputChangeStrategy::new`, and
`change_memo` argument. `standard::SingleOutputChangeStrategy::new` each now accept additional
`change_memo` and `fallback_change_pool` arguments.
- `zcash_client_backend::wallet`: - `zcash_client_backend::wallet`:
- The fields of `ReceivedSaplingNote` are now private. Use - The fields of `ReceivedSaplingNote` are now private. Use
`ReceivedSaplingNote::from_parts` for construction instead. Accessor methods `ReceivedSaplingNote::from_parts` for construction instead. Accessor methods
are provided for each previously public field. are provided for each previously public field.
- `Recipient` is now polymorphic in the type of the payload for wallet-internal
recipients. This simplifies the handling of wallet-internal outputs.
- `SentTransactionOutput::from_parts` now takes a `Recipient<Note>`.
- `SentTransactionOutput::recipient` now returns a `Recipient<Note>`.
- `OvkPolicy::Custom` is now a structured variant that can contain independent
Sapling and Orchard `OutgoingViewingKey`s.
- `zcash_client_backend::scanning::ScanError` has a new variant, `TreeSizeInvalid`. - `zcash_client_backend::scanning::ScanError` has a new variant, `TreeSizeInvalid`.
- `zcash_client_backend::zip321::TransactionRequest::payments` now returns a - `zcash_client_backend::zip321::TransactionRequest::payments` now returns a
`BTreeMap<usize, Payment>` instead of `&[Payment]` so that parameter `BTreeMap<usize, Payment>` instead of `&[Payment]` so that parameter
@ -267,6 +281,8 @@ and this library adheres to Rust's notion of
`zcash_client_backend::ReceivedNote`. `zcash_client_backend::ReceivedNote`.
- `zcash_client_backend::::wallet::input_selection::{Proposal, ShieldedInputs, ProposalError}` - `zcash_client_backend::::wallet::input_selection::{Proposal, ShieldedInputs, ProposalError}`
have been moved to `zcash_client_backend::proposal`. have been moved to `zcash_client_backend::proposal`.
- `zcash_client_backend::wallet::SentTransactionOutput::sapling_change_to` - the
note created by an internal transfer is now conveyed in the `recipient` field.
- `zcash_client_backend::data_api` - `zcash_client_backend::data_api`
- `zcash_client_backend::data_api::ScannedBlock::from_parts` has been made crate-private. - `zcash_client_backend::data_api::ScannedBlock::from_parts` has been made crate-private.
- `zcash_client_backend::data_api::ScannedBlock::into_sapling_commitments` has been - `zcash_client_backend::data_api::ScannedBlock::into_sapling_commitments` has been

View File

@ -8,7 +8,6 @@ use std::{
}; };
use incrementalmerkletree::{frontier::Frontier, Retention}; use incrementalmerkletree::{frontier::Frontier, Retention};
use sapling::{Node, NOTE_COMMITMENT_TREE_DEPTH};
use secrecy::SecretVec; use secrecy::SecretVec;
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree}; use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
use zcash_primitives::{ use zcash_primitives::{
@ -51,6 +50,13 @@ pub mod wallet;
/// `lightwalletd` when using the `GetSubtreeRoots` GRPC call. /// `lightwalletd` when using the `GetSubtreeRoots` GRPC call.
pub const SAPLING_SHARD_HEIGHT: u8 = sapling::NOTE_COMMITMENT_TREE_DEPTH / 2; pub const SAPLING_SHARD_HEIGHT: u8 = sapling::NOTE_COMMITMENT_TREE_DEPTH / 2;
/// The height of subtree roots in the Orchard note commitment tree.
///
/// This conforms to the structure of subtree data returned by
/// `lightwalletd` when using the `GetSubtreeRoots` GRPC call.
#[cfg(feature = "orchard")]
pub const ORCHARD_SHARD_HEIGHT: u8 = { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 } / 2;
/// An enumeration of constraints that can be applied when querying for nullifiers for notes /// An enumeration of constraints that can be applied when querying for nullifiers for notes
/// belonging to the wallet. /// belonging to the wallet.
pub enum NullifierQuery { pub enum NullifierQuery {
@ -808,29 +814,37 @@ pub struct SentTransaction<'a> {
pub utxos_spent: Vec<OutPoint>, pub utxos_spent: Vec<OutPoint>,
} }
/// A type that represents an output (either Sapling or transparent) that was sent by the wallet. /// An output of a transaction generated by the wallet.
///
/// This type is capable of representing both shielded and transparent outputs.
pub struct SentTransactionOutput { pub struct SentTransactionOutput {
output_index: usize, output_index: usize,
recipient: Recipient, recipient: Recipient<Note>,
value: NonNegativeAmount, value: NonNegativeAmount,
memo: Option<MemoBytes>, memo: Option<MemoBytes>,
sapling_change_to: Option<(AccountId, sapling::Note)>,
} }
impl SentTransactionOutput { impl SentTransactionOutput {
/// Constructs a new [`SentTransactionOutput`] from its constituent parts.
///
/// ### Fields:
/// * `output_index` - the index of the output or action in the sent transaction
/// * `recipient` - the recipient of the output, either a Zcash address or a
/// wallet-internal account and the note belonging to the wallet created by
/// the output
/// * `value` - the value of the output, in zatoshis
/// * `memo` - the memo that was sent with this output
pub fn from_parts( pub fn from_parts(
output_index: usize, output_index: usize,
recipient: Recipient, recipient: Recipient<Note>,
value: NonNegativeAmount, value: NonNegativeAmount,
memo: Option<MemoBytes>, memo: Option<MemoBytes>,
sapling_change_to: Option<(AccountId, sapling::Note)>,
) -> Self { ) -> Self {
Self { Self {
output_index, output_index,
recipient, recipient,
value, value,
memo, memo,
sapling_change_to,
} }
} }
@ -843,9 +857,9 @@ impl SentTransactionOutput {
pub fn output_index(&self) -> usize { pub fn output_index(&self) -> usize {
self.output_index self.output_index
} }
/// Returns the recipient address of the transaction, or the account id for wallet-internal /// Returns the recipient address of the transaction, or the account id and
/// transactions. /// resulting note for wallet-internal outputs.
pub fn recipient(&self) -> &Recipient { pub fn recipient(&self) -> &Recipient<Note> {
&self.recipient &self.recipient
} }
/// Returns the value of the newly created output. /// Returns the value of the newly created output.
@ -857,12 +871,6 @@ impl SentTransactionOutput {
pub fn memo(&self) -> Option<&MemoBytes> { pub fn memo(&self) -> Option<&MemoBytes> {
self.memo.as_ref() self.memo.as_ref()
} }
/// Returns the account to which change (or wallet-internal value in the case of a shielding
/// transaction) was sent, along with the change note.
pub fn sapling_change_to(&self) -> Option<&(AccountId, sapling::Note)> {
self.sapling_change_to.as_ref()
}
} }
/// A data structure used to set the birthday height for an account, and ensure that the initial /// A data structure used to set the birthday height for an account, and ensure that the initial
@ -870,7 +878,7 @@ impl SentTransactionOutput {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AccountBirthday { pub struct AccountBirthday {
height: BlockHeight, height: BlockHeight,
sapling_frontier: Frontier<Node, NOTE_COMMITMENT_TREE_DEPTH>, sapling_frontier: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
recover_until: Option<BlockHeight>, recover_until: Option<BlockHeight>,
} }
@ -911,7 +919,7 @@ impl AccountBirthday {
#[cfg(feature = "test-dependencies")] #[cfg(feature = "test-dependencies")]
pub fn from_parts( pub fn from_parts(
height: BlockHeight, height: BlockHeight,
sapling_frontier: Frontier<Node, NOTE_COMMITMENT_TREE_DEPTH>, sapling_frontier: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
recover_until: Option<BlockHeight>, recover_until: Option<BlockHeight>,
) -> Self { ) -> Self {
Self { Self {
@ -944,7 +952,9 @@ impl AccountBirthday {
/// Returns the Sapling note commitment tree frontier as of the end of the block at /// Returns the Sapling note commitment tree frontier as of the end of the block at
/// [`Self::height`]. /// [`Self::height`].
pub fn sapling_frontier(&self) -> &Frontier<Node, NOTE_COMMITMENT_TREE_DEPTH> { pub fn sapling_frontier(
&self,
) -> &Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }> {
&self.sapling_frontier &self.sapling_frontier
} }
@ -1081,13 +1091,15 @@ pub trait WalletWrite: WalletRead {
/// also provide operations related to Orchard note commitment trees in the future. /// also provide operations related to Orchard note commitment trees in the future.
pub trait WalletCommitmentTrees { pub trait WalletCommitmentTrees {
type Error; type Error;
/// The type of the backing [`ShardStore`] for the Sapling note commitment tree.
type SaplingShardStore<'a>: ShardStore< type SaplingShardStore<'a>: ShardStore<
H = sapling::Node, H = sapling::Node,
CheckpointId = BlockHeight, CheckpointId = BlockHeight,
Error = Self::Error, Error = Self::Error,
>; >;
/// /// Evaluates the given callback function with a reference to the Sapling
/// note commitment tree maintained by the wallet.
fn with_sapling_tree_mut<F, A, E>(&mut self, callback: F) -> Result<A, E> fn with_sapling_tree_mut<F, A, E>(&mut self, callback: F) -> Result<A, E>
where where
for<'a> F: FnMut( for<'a> F: FnMut(
@ -1099,12 +1111,48 @@ pub trait WalletCommitmentTrees {
) -> Result<A, E>, ) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>; E: From<ShardTreeError<Self::Error>>;
/// Adds a sequence of note commitment tree subtree roots to the data store. /// Adds a sequence of Sapling note commitment tree subtree roots to the data store.
///
/// Each such value should be the Merkle root of a subtree of the Sapling note commitment tree
/// containing 2^[`SAPLING_SHARD_HEIGHT`] note commitments.
fn put_sapling_subtree_roots( fn put_sapling_subtree_roots(
&mut self, &mut self,
start_index: u64, start_index: u64,
roots: &[CommitmentTreeRoot<sapling::Node>], roots: &[CommitmentTreeRoot<sapling::Node>],
) -> Result<(), ShardTreeError<Self::Error>>; ) -> Result<(), ShardTreeError<Self::Error>>;
/// The type of the backing [`ShardStore`] for the Orchard note commitment tree.
#[cfg(feature = "orchard")]
type OrchardShardStore<'a>: ShardStore<
H = orchard::tree::MerkleHashOrchard,
CheckpointId = BlockHeight,
Error = Self::Error,
>;
/// Evaluates the given callback function with a reference to the Orchard
/// note commitment tree maintained by the wallet.
#[cfg(feature = "orchard")]
fn with_orchard_tree_mut<F, A, E>(&mut self, callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::OrchardShardStore<'a>,
{ ORCHARD_SHARD_HEIGHT * 2 },
ORCHARD_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>;
/// Adds a sequence of Orchard note commitment tree subtree roots to the data store.
///
/// Each such value should be the Merkle root of a subtree of the Orchard note commitment tree
/// containing 2^[`ORCHARD_SHARD_HEIGHT`] note commitments.
#[cfg(feature = "orchard")]
fn put_orchard_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>>;
} }
#[cfg(feature = "test-dependencies")] #[cfg(feature = "test-dependencies")]
@ -1138,6 +1186,9 @@ pub mod testing {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use {crate::wallet::TransparentAddressMetadata, zcash_primitives::legacy::TransparentAddress}; use {crate::wallet::TransparentAddressMetadata, zcash_primitives::legacy::TransparentAddress};
#[cfg(feature = "orchard")]
use super::ORCHARD_SHARD_HEIGHT;
pub struct MockWalletDb { pub struct MockWalletDb {
pub network: Network, pub network: Network,
pub sapling_tree: ShardTree< pub sapling_tree: ShardTree<
@ -1145,6 +1196,12 @@ pub mod testing {
{ SAPLING_SHARD_HEIGHT * 2 }, { SAPLING_SHARD_HEIGHT * 2 },
SAPLING_SHARD_HEIGHT, SAPLING_SHARD_HEIGHT,
>, >,
#[cfg(feature = "orchard")]
pub orchard_tree: ShardTree<
MemoryShardStore<orchard::tree::MerkleHashOrchard, BlockHeight>,
{ ORCHARD_SHARD_HEIGHT * 2 },
ORCHARD_SHARD_HEIGHT,
>,
} }
impl MockWalletDb { impl MockWalletDb {
@ -1152,6 +1209,8 @@ pub mod testing {
Self { Self {
network, network,
sapling_tree: ShardTree::new(MemoryShardStore::empty(), 100), sapling_tree: ShardTree::new(MemoryShardStore::empty(), 100),
#[cfg(feature = "orchard")]
orchard_tree: ShardTree::new(MemoryShardStore::empty(), 100),
} }
} }
} }
@ -1406,5 +1465,43 @@ pub mod testing {
Ok(()) Ok(())
} }
#[cfg(feature = "orchard")]
type OrchardShardStore<'a> =
MemoryShardStore<orchard::tree::MerkleHashOrchard, BlockHeight>;
#[cfg(feature = "orchard")]
fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::OrchardShardStore<'a>,
{ ORCHARD_SHARD_HEIGHT * 2 },
ORCHARD_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>,
{
callback(&mut self.orchard_tree)
}
/// Adds a sequence of note commitment tree subtree roots to the data store.
#[cfg(feature = "orchard")]
fn put_orchard_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>> {
self.with_orchard_tree_mut(|t| {
for (root, i) in roots.iter().zip(0u64..) {
let root_addr =
Address::from_parts(ORCHARD_SHARD_HEIGHT.into(), start_index + i);
t.insert(root_addr, *root.root_hash())?;
}
Ok::<_, ShardTreeError<Self::Error>>(())
})?;
Ok(())
}
} }
} }

View File

@ -13,6 +13,7 @@ use zcash_primitives::{
zip32::AccountId, zip32::AccountId,
}; };
use crate::address::UnifiedAddress;
use crate::data_api::wallet::input_selection::InputSelectorError; use crate::data_api::wallet::input_selection::InputSelectorError;
use crate::proposal::ProposalError; use crate::proposal::ProposalError;
use crate::PoolType; use crate::PoolType;
@ -66,11 +67,15 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
/// It is forbidden to provide a memo when constructing a transparent output. /// It is forbidden to provide a memo when constructing a transparent output.
MemoForbidden, MemoForbidden,
/// Attempted to create a spend to an unsupported pool type (currently, Orchard). /// Attempted to send change to an unsupported pool.
UnsupportedPoolType(PoolType), ///
/// This is indicative of a programming error; execution of a transaction proposal that
/// presumes support for the specified pool was performed using an application that does not
/// provide such support.
UnsupportedChangeType(PoolType),
/// Attempted to create a spend to an unsupported Unified Address receiver /// Attempted to create a spend to an unsupported Unified Address receiver
NoSupportedReceivers(Vec<u32>), NoSupportedReceivers(Box<UnifiedAddress>),
/// A proposed transaction cannot be built because it requires spending an input /// A proposed transaction cannot be built because it requires spending an input
/// for which no spending key is available. /// for which no spending key is available.
@ -140,8 +145,12 @@ where
Error::ScanRequired => write!(f, "Must scan blocks first"), Error::ScanRequired => write!(f, "Must scan blocks first"),
Error::Builder(e) => write!(f, "An error occurred building the transaction: {}", e), Error::Builder(e) => write!(f, "An error occurred building the transaction: {}", e),
Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."), Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."),
Error::UnsupportedPoolType(t) => write!(f, "Attempted to send to an unsupported pool: {}", t), Error::UnsupportedChangeType(t) => write!(f, "Attempted to send change to an unsupported pool type: {}", t),
Error::NoSupportedReceivers(t) => write!(f, "Unified address contained only unsupported receiver types: {:?}", &t[..]), Error::NoSupportedReceivers(ua) => write!(
f,
"A recipient's unified address does not contain any receivers to which the wallet can send funds; required one of {}",
ua.receiver_types().iter().enumerate().map(|(i, tc)| format!("{}{:?}", if i > 0 { ", " } else { "" }, tc)).collect::<String>()
),
Error::NoSpendingKey(addr) => write!(f, "No spending key available for address: {}", addr), Error::NoSpendingKey(addr) => write!(f, "No spending key available for address: {}", addr),
Error::NoteMismatch(n) => write!(f, "A note being spent ({:?}) does not correspond to either the internal or external full viewing key for the provided spending key.", n), Error::NoteMismatch(n) => write!(f, "A note being spent ({:?}) does not correspond to either the internal or external full viewing key for the provided spending key.", n),

View File

@ -27,8 +27,7 @@ use crate::{
decrypt_transaction, decrypt_transaction,
fees::{self, DustOutputPolicy}, fees::{self, DustOutputPolicy},
keys::UnifiedSpendingKey, keys::UnifiedSpendingKey,
proposal::ProposalError, proposal::{self, Proposal, ProposalError},
proposal::{self, Proposal},
wallet::{Note, OvkPolicy, Recipient}, wallet::{Note, OvkPolicy, Recipient},
zip321::{self, Payment}, zip321::{self, Payment},
PoolType, ShieldedProtocol, PoolType, ShieldedProtocol,
@ -44,7 +43,6 @@ use super::InputSource;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use {
input_selection::ShieldingSelector, input_selection::ShieldingSelector,
sapling::keys::OutgoingViewingKey,
std::convert::Infallible, std::convert::Infallible,
zcash_keys::encoding::AddressCodec, zcash_keys::encoding::AddressCodec,
zcash_primitives::legacy::TransparentAddress, zcash_primitives::legacy::TransparentAddress,
@ -211,6 +209,7 @@ pub fn create_spend_to_address<DbT, ParamsT>(
ovk_policy: OvkPolicy, ovk_policy: OvkPolicy,
min_confirmations: NonZeroU32, min_confirmations: NonZeroU32,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Result< ) -> Result<
NonEmpty<TxId>, NonEmpty<TxId>,
Error< Error<
@ -241,6 +240,7 @@ where
amount, amount,
memo, memo,
change_memo, change_memo,
fallback_change_pool,
)?; )?;
create_proposed_transactions( create_proposed_transactions(
@ -359,7 +359,7 @@ where
/// Select transaction inputs, compute fees, and construct a proposal for a transaction or series /// 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 /// of transactions that can then be authorized and made ready for submission to the network with
/// [`create_proposed_transaction`]. /// [`create_proposed_transactions`].
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub fn propose_transfer<DbT, ParamsT, InputsT, CommitmentTreeErrT>( pub fn propose_transfer<DbT, ParamsT, InputsT, CommitmentTreeErrT>(
@ -403,7 +403,7 @@ where
/// Proposes making a payment to 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_transactions`].
/// Depending upon the recipient address, more than one transaction may be constructed /// Depending upon the recipient address, more than one transaction may be constructed
/// in the execution of the returned proposal. /// in the execution of the returned proposal.
/// ///
@ -424,6 +424,8 @@ where
/// * `amount`: The amount to send. /// * `amount`: The amount to send.
/// * `memo`: A memo to be included in the output to the recipient. /// * `memo`: A memo to be included in the output to the recipient.
/// * `change_memo`: A memo to be included in any change output that is created. /// * `change_memo`: A memo to be included in any change output that is created.
/// * `fallback_change_pool`: The shielded pool to which change should be sent if
/// automatic change pool determination fails.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>( pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
@ -436,6 +438,7 @@ pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
amount: NonNegativeAmount, amount: NonNegativeAmount,
memo: Option<MemoBytes>, memo: Option<MemoBytes>,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Result< ) -> Result<
Proposal<StandardFeeRule, DbT::NoteRef>, Proposal<StandardFeeRule, DbT::NoteRef>,
Error< Error<
@ -462,7 +465,11 @@ where
"It should not be possible for this to violate ZIP 321 request construction invariants.", "It should not be possible for this to violate ZIP 321 request construction invariants.",
); );
let change_strategy = fees::standard::SingleOutputChangeStrategy::new(fee_rule, change_memo); let change_strategy = fees::standard::SingleOutputChangeStrategy::new(
fee_rule,
change_memo,
fallback_change_pool,
);
let input_selector = let input_selector =
GreedyInputSelector::<DbT, _>::new(change_strategy, DustOutputPolicy::default()); GreedyInputSelector::<DbT, _>::new(change_strategy, DustOutputPolicy::default());
@ -645,72 +652,85 @@ where
.map_err(Error::DataSource)? .map_err(Error::DataSource)?
.ok_or(Error::KeyNotRecognized)?; .ok_or(Error::KeyNotRecognized)?;
let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); let (sapling_anchor, sapling_inputs) =
if proposal_step.involves(PoolType::Shielded(ShieldedProtocol::Sapling)) {
proposal_step.shielded_inputs().map_or_else(
|| Ok((Some(sapling::Anchor::empty_tree()), vec![])),
|inputs| {
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {
let anchor = sapling_tree
.root_at_checkpoint_id(&inputs.anchor_height())?
.into();
// Apply the outgoing viewing key policy. let sapling_inputs = inputs
let external_ovk = match ovk_policy { .notes()
OvkPolicy::Sender => Some(dfvk.to_ovk(Scope::External)), .iter()
OvkPolicy::Custom(ovk) => Some(ovk), .filter_map(|selected| match selected.note() {
OvkPolicy::Discard => None, Note::Sapling(note) => {
}; let key = match selected.spending_key_scope() {
Scope::External => usk.sapling().clone(),
Scope::Internal => usk.sapling().derive_internal(),
};
let internal_ovk = || { sapling_tree
#[cfg(feature = "transparent-inputs")] .witness_at_checkpoint_id_caching(
return if proposal_step.is_shielding() { selected.note_commitment_tree_position(),
Some(OutgoingViewingKey( &inputs.anchor_height(),
usk.transparent() )
.to_account_pubkey() .map(|merkle_path| Some((key, note, merkle_path)))
.internal_ovk() .map_err(Error::from)
.as_bytes(), .transpose()
)) }
#[cfg(feature = "orchard")]
Note::Orchard(_) => None,
})
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?;
Ok((Some(anchor), sapling_inputs))
})
},
)?
} else { } else {
Some(dfvk.to_ovk(Scope::Internal)) (None, vec![])
}; };
#[cfg(not(feature = "transparent-inputs"))] #[cfg(feature = "orchard")]
Some(dfvk.to_ovk(Scope::Internal)) let (orchard_anchor, orchard_inputs) =
}; if proposal_step.involves(PoolType::Shielded(ShieldedProtocol::Orchard)) {
proposal_step.shielded_inputs().map_or_else(
|| Ok((Some(orchard::Anchor::empty_tree()), vec![])),
|inputs| {
wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _>>(|orchard_tree| {
let anchor = orchard_tree
.root_at_checkpoint_id(&inputs.anchor_height())?
.into();
let (sapling_anchor, sapling_inputs) = proposal_step.shielded_inputs().map_or_else( let orchard_inputs = inputs
|| Ok((sapling::Anchor::empty_tree(), vec![])), .notes()
|inputs| { .iter()
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| { .filter_map(|selected| match selected.note() {
let anchor = sapling_tree #[cfg(feature = "orchard")]
.root_at_checkpoint_id(&inputs.anchor_height())? Note::Orchard(note) => orchard_tree
.into(); .witness_at_checkpoint_id_caching(
selected.note_commitment_tree_position(),
&inputs.anchor_height(),
)
.map(|merkle_path| Some((note, merkle_path)))
.map_err(Error::from)
.transpose(),
Note::Sapling(_) => None,
})
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?;
let sapling_inputs = inputs Ok((Some(anchor), orchard_inputs))
.notes()
.iter()
.map(|selected| {
match selected.note() {
Note::Sapling(note) => {
let key = match selected.spending_key_scope() {
Scope::External => usk.sapling().clone(),
Scope::Internal => usk.sapling().derive_internal(),
};
let merkle_path = sapling_tree.witness_at_checkpoint_id_caching(
selected.note_commitment_tree_position(),
&inputs.anchor_height(),
)?;
Ok((key, note, merkle_path))
}
#[cfg(feature = "orchard")]
Note::Orchard(_) => {
// FIXME: Implement this once `Proposal` has been refactored to
// include Orchard notes.
panic!("Orchard spends are not yet supported");
}
}
}) })
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?; },
)?
Ok((anchor, sapling_inputs)) } else {
}) (None, vec![])
}, };
)?; #[cfg(not(feature = "orchard"))]
let orchard_anchor = None;
// Create the transaction. The type of the proposal ensures that there // Create the transaction. The type of the proposal ensures that there
// are no possible transparent inputs, so we ignore those // are no possible transparent inputs, so we ignore those
@ -718,13 +738,18 @@ where
params.clone(), params.clone(),
min_target_height, min_target_height,
BuildConfig::Standard { BuildConfig::Standard {
sapling_anchor: Some(sapling_anchor), sapling_anchor,
orchard_anchor: None, orchard_anchor,
}, },
); );
for (key, note, merkle_path) in sapling_inputs.into_iter() { for (sapling_key, sapling_note, merkle_path) in sapling_inputs.into_iter() {
builder.add_sapling_spend(&key, note.clone(), merkle_path)?; builder.add_sapling_spend(&sapling_key, sapling_note.clone(), merkle_path)?;
}
#[cfg(feature = "orchard")]
for (orchard_note, merkle_path) in orchard_inputs.into_iter() {
builder.add_orchard_spend(usk.orchard(), *orchard_note, merkle_path.into())?;
} }
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
@ -817,9 +842,72 @@ where
utxos_spent utxos_spent
}; };
#[cfg(feature = "orchard")]
let orchard_fvk: orchard::keys::FullViewingKey = usk.orchard().into();
#[cfg(feature = "orchard")]
let orchard_external_ovk = match &ovk_policy {
OvkPolicy::Sender => Some(orchard_fvk.to_ovk(orchard::keys::Scope::External)),
OvkPolicy::Custom { orchard, .. } => Some(orchard.clone()),
OvkPolicy::Discard => None,
};
#[cfg(feature = "orchard")]
let orchard_internal_ovk = || {
#[cfg(feature = "transparent-inputs")]
if proposal_step.is_shielding() {
return Some(orchard::keys::OutgoingViewingKey::from(
usk.transparent()
.to_account_pubkey()
.internal_ovk()
.as_bytes(),
));
}
Some(orchard_fvk.to_ovk(Scope::Internal))
};
let sapling_dfvk = usk.sapling().to_diversifiable_full_viewing_key();
// Apply the outgoing viewing key policy.
let sapling_external_ovk = match &ovk_policy {
OvkPolicy::Sender => Some(sapling_dfvk.to_ovk(Scope::External)),
OvkPolicy::Custom { sapling, .. } => Some(*sapling),
OvkPolicy::Discard => None,
};
let sapling_internal_ovk = || {
#[cfg(feature = "transparent-inputs")]
if proposal_step.is_shielding() {
return Some(sapling::keys::OutgoingViewingKey(
usk.transparent()
.to_account_pubkey()
.internal_ovk()
.as_bytes(),
));
}
Some(sapling_dfvk.to_ovk(Scope::Internal))
};
#[cfg(feature = "orchard")]
let mut orchard_output_meta = vec![];
let mut sapling_output_meta = vec![]; let mut sapling_output_meta = vec![];
let mut transparent_output_meta = vec![]; let mut transparent_output_meta = vec![];
for payment in proposal_step.transaction_request().payments().values() { for (payment, output_pool) in proposal_step
.payment_pools()
.iter()
.map(|(idx, output_pool)| {
let payment = proposal_step
.transaction_request()
.payments()
.get(idx)
.expect(
"The mapping between payment index and payment is checked in step construction",
);
(payment, output_pool)
})
{
match &payment.recipient_address { match &payment.recipient_address {
Address::Unified(ua) => { Address::Unified(ua) => {
let memo = payment let memo = payment
@ -827,31 +915,56 @@ where
.as_ref() .as_ref()
.map_or_else(MemoBytes::empty, |m| m.clone()); .map_or_else(MemoBytes::empty, |m| m.clone());
if let Some(sapling_receiver) = ua.sapling() { match output_pool {
builder.add_sapling_output( #[cfg(not(feature = "orchard"))]
external_ovk, PoolType::Shielded(ShieldedProtocol::Orchard) => {
*sapling_receiver, return Err(Error::ProposalNotSupported);
payment.amount, }
memo.clone(), #[cfg(feature = "orchard")]
)?; PoolType::Shielded(ShieldedProtocol::Orchard) => {
sapling_output_meta.push(( builder.add_orchard_output(
Recipient::Unified( orchard_external_ovk.clone(),
ua.clone(), *ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction"),
PoolType::Shielded(ShieldedProtocol::Sapling), payment.amount.into(),
), memo.clone(),
payment.amount, )?;
Some(memo), orchard_output_meta.push((
)); Recipient::Unified(
} else if let Some(taddr) = ua.transparent() { ua.clone(),
if payment.memo.is_some() { PoolType::Shielded(ShieldedProtocol::Orchard),
return Err(Error::MemoForbidden); ),
} else { payment.amount,
builder.add_transparent_output(taddr, payment.amount)?; Some(memo),
));
}
PoolType::Shielded(ShieldedProtocol::Sapling) => {
builder.add_sapling_output(
sapling_external_ovk,
*ua.sapling().expect("The mapping between payment pool and receiver is checked in step construction"),
payment.amount,
memo.clone(),
)?;
sapling_output_meta.push((
Recipient::Unified(
ua.clone(),
PoolType::Shielded(ShieldedProtocol::Sapling),
),
payment.amount,
Some(memo),
));
}
PoolType::Transparent => {
if payment.memo.is_some() {
return Err(Error::MemoForbidden);
} else {
builder.add_transparent_output(
ua.transparent().expect("The mapping between payment pool and receiver is checked in step construction."),
payment.amount
)?;
}
} }
} else {
return Err(Error::NoSupportedReceivers(
ua.unknown().iter().map(|(tc, _)| *tc).collect(),
));
} }
} }
Address::Sapling(addr) => { Address::Sapling(addr) => {
@ -859,7 +972,12 @@ where
.memo .memo
.as_ref() .as_ref()
.map_or_else(MemoBytes::empty, |m| m.clone()); .map_or_else(MemoBytes::empty, |m| m.clone());
builder.add_sapling_output(external_ovk, *addr, payment.amount, memo.clone())?; builder.add_sapling_output(
sapling_external_ovk,
*addr,
payment.amount,
memo.clone(),
)?;
sapling_output_meta.push((Recipient::Sapling(*addr), payment.amount, Some(memo))); sapling_output_meta.push((Recipient::Sapling(*addr), payment.amount, Some(memo)));
} }
Address::Transparent(to) => { Address::Transparent(to) => {
@ -880,8 +998,8 @@ where
match change_value.output_pool() { match change_value.output_pool() {
ShieldedProtocol::Sapling => { ShieldedProtocol::Sapling => {
builder.add_sapling_output( builder.add_sapling_output(
internal_ovk(), sapling_internal_ovk(),
dfvk.change_address().1, sapling_dfvk.change_address().1,
change_value.value(), change_value.value(),
memo.clone(), memo.clone(),
)?; )?;
@ -894,15 +1012,29 @@ where
Some(memo), Some(memo),
)) ))
} }
#[cfg(zcash_unstable = "orchard")]
ShieldedProtocol::Orchard => { ShieldedProtocol::Orchard => {
#[cfg(not(feature = "orchard"))] #[cfg(not(feature = "orchard"))]
return Err(Error::UnsupportedPoolType(PoolType::Shielded( return Err(Error::UnsupportedChangeType(PoolType::Shielded(
ShieldedProtocol::Orchard, ShieldedProtocol::Orchard,
))); )));
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
unimplemented!("FIXME: implement Orchard change output creation.") {
builder.add_orchard_output(
orchard_internal_ovk(),
orchard_fvk.address_at(0u32, orchard::keys::Scope::Internal),
change_value.value().into(),
memo.clone(),
)?;
orchard_output_meta.push((
Recipient::InternalAccount(
account,
PoolType::Shielded(ShieldedProtocol::Orchard),
),
change_value.value(),
Some(memo),
))
}
} }
} }
} }
@ -910,7 +1042,39 @@ where
// Build the transaction with the specified fee rule // Build the transaction with the specified fee rule
let build_result = builder.build(OsRng, spend_prover, output_prover, fee_rule)?; let build_result = builder.build(OsRng, spend_prover, output_prover, fee_rule)?;
let internal_ivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)); #[cfg(feature = "orchard")]
let orchard_internal_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::Internal);
#[cfg(feature = "orchard")]
let orchard_outputs =
orchard_output_meta
.into_iter()
.enumerate()
.map(|(i, (recipient, value, memo))| {
let output_index = build_result
.orchard_meta()
.output_action_index(i)
.expect("An action should exist in the transaction for each Orchard output.");
let recipient = recipient
.map_internal_account(|pool| {
assert!(pool == PoolType::Shielded(ShieldedProtocol::Orchard));
build_result
.transaction()
.orchard_bundle()
.and_then(|bundle| {
bundle
.decrypt_output_with_key(output_index, &orchard_internal_ivk)
.map(|(note, _, _)| Note::Orchard(note))
})
})
.internal_account_transpose_option()
.expect("Wallet-internal outputs must be decryptable with the wallet's IVK");
SentTransactionOutput::from_parts(output_index, recipient, value, memo)
});
let sapling_internal_ivk =
PreparedIncomingViewingKey::new(&sapling_dfvk.to_ivk(Scope::Internal));
let sapling_outputs = let sapling_outputs =
sapling_output_meta sapling_output_meta
.into_iter() .into_iter()
@ -921,27 +1085,28 @@ where
.output_index(i) .output_index(i)
.expect("An output should exist in the transaction for each Sapling payment."); .expect("An output should exist in the transaction for each Sapling payment.");
let received_as = if let Recipient::InternalAccount( let recipient = recipient
account, .map_internal_account(|pool| {
PoolType::Shielded(ShieldedProtocol::Sapling), assert!(pool == PoolType::Shielded(ShieldedProtocol::Sapling));
) = recipient build_result
{ .transaction()
build_result .sapling_bundle()
.transaction() .and_then(|bundle| {
.sapling_bundle() try_sapling_note_decryption(
.and_then(|bundle| { &sapling_internal_ivk,
try_sapling_note_decryption( &bundle.shielded_outputs()[output_index],
&internal_ivk, consensus::sapling_zip212_enforcement(
&bundle.shielded_outputs()[output_index], params,
consensus::sapling_zip212_enforcement(params, min_target_height), min_target_height,
) ),
.map(|(note, _, _)| (account, note)) )
}) .map(|(note, _, _)| Note::Sapling(note))
} else { })
None })
}; .internal_account_transpose_option()
.expect("Wallet-internal outputs must be decryptable with the wallet's IVK");
SentTransactionOutput::from_parts(output_index, recipient, value, memo, received_as) SentTransactionOutput::from_parts(output_index, recipient, value, memo)
}); });
let transparent_outputs = transparent_output_meta.into_iter().map(|(addr, value)| { let transparent_outputs = transparent_output_meta.into_iter().map(|(addr, value)| {
@ -958,21 +1123,21 @@ where
.map(|(index, _)| index) .map(|(index, _)| index)
.expect("An output should exist in the transaction for each transparent payment."); .expect("An output should exist in the transaction for each transparent payment.");
SentTransactionOutput::from_parts( SentTransactionOutput::from_parts(output_index, Recipient::Transparent(*addr), value, None)
output_index,
Recipient::Transparent(*addr),
value,
None,
None,
)
}); });
let mut outputs = vec![];
#[cfg(feature = "orchard")]
outputs.extend(orchard_outputs);
outputs.extend(sapling_outputs);
outputs.extend(transparent_outputs);
wallet_db wallet_db
.store_sent_tx(&SentTransaction { .store_sent_tx(&SentTransaction {
tx: build_result.transaction(), tx: build_result.transaction(),
created: time::OffsetDateTime::now_utc(), created: time::OffsetDateTime::now_utc(),
account, account,
outputs: sapling_outputs.chain(transparent_outputs).collect(), outputs,
fee_amount: Amount::from(proposal_step.balance().fee_required()), fee_amount: Amount::from(proposal_step.balance().fee_required()),
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
utxos_spent, utxos_spent,

View File

@ -455,9 +455,9 @@ where
Err(other) => return Err(other.into()), Err(other) => return Err(other.into()),
} }
#[cfg(not(zcash_unstable = "orchard"))] #[cfg(not(feature = "orchard"))]
let selectable_pools = &[ShieldedProtocol::Sapling]; let selectable_pools = &[ShieldedProtocol::Sapling];
#[cfg(zcash_unstable = "orchard")] #[cfg(feature = "orchard")]
let selectable_pools = &[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard]; let selectable_pools = &[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard];
shielded_inputs = wallet_db shielded_inputs = wallet_db

View File

@ -34,6 +34,7 @@ pub(crate) fn single_change_output_balance<
dust_output_policy: &DustOutputPolicy, dust_output_policy: &DustOutputPolicy,
default_dust_threshold: NonNegativeAmount, default_dust_threshold: NonNegativeAmount,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
_fallback_change_pool: ShieldedProtocol,
) -> Result<TransactionBalance, ChangeError<E, NoteRefT>> ) -> Result<TransactionBalance, ChangeError<E, NoteRefT>>
where where
E: From<F::Error> + From<BalanceError>, E: From<F::Error> + From<BalanceError>,
@ -86,7 +87,6 @@ where
// TODO: implement a less naive strategy for selecting the pool to which change will be sent. // TODO: implement a less naive strategy for selecting the pool to which change will be sent.
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
#[allow(clippy::if_same_then_else)]
let (change_pool, sapling_change, orchard_change) = let (change_pool, sapling_change, orchard_change) =
if orchard_in.is_positive() || orchard_out.is_positive() { if orchard_in.is_positive() || orchard_out.is_positive() {
// Send change to Orchard if we're spending any Orchard inputs or creating any Orchard outputs // Send change to Orchard if we're spending any Orchard inputs or creating any Orchard outputs
@ -96,9 +96,12 @@ where
// Sapling outputs, so that we avoid pool-crossing. // Sapling outputs, so that we avoid pool-crossing.
(ShieldedProtocol::Sapling, 1, 0) (ShieldedProtocol::Sapling, 1, 0)
} else { } else {
// For all other transactions, send change to Sapling. // This is a fully-transparent transaction, so the caller gets to decide
// FIXME: Change this to Orchard once Orchard outputs are enabled. // where to shield change.
(ShieldedProtocol::Sapling, 1, 0) match _fallback_change_pool {
ShieldedProtocol::Orchard => (_fallback_change_pool, 0, 1),
ShieldedProtocol::Sapling => (_fallback_change_pool, 1, 0),
}
}; };
#[cfg(not(feature = "orchard"))] #[cfg(not(feature = "orchard"))]
let (change_pool, sapling_change) = (ShieldedProtocol::Sapling, 1); let (change_pool, sapling_change) = (ShieldedProtocol::Sapling, 1);

View File

@ -9,6 +9,8 @@ use zcash_primitives::{
}, },
}; };
use crate::ShieldedProtocol;
use super::{ use super::{
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy, common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
DustOutputPolicy, TransactionBalance, DustOutputPolicy, TransactionBalance,
@ -22,15 +24,24 @@ use super::orchard as orchard_fees;
pub struct SingleOutputChangeStrategy { pub struct SingleOutputChangeStrategy {
fee_rule: FixedFeeRule, fee_rule: FixedFeeRule,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
} }
impl SingleOutputChangeStrategy { impl SingleOutputChangeStrategy {
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified fee rule /// Constructs a new [`SingleOutputChangeStrategy`] with the specified fee rule
/// and change memo. /// and change memo.
pub fn new(fee_rule: FixedFeeRule, change_memo: Option<MemoBytes>) -> Self { ///
/// `fallback_change_pool` is used when more than one shielded pool is enabled via
/// feature flags, and the transaction has no shielded inputs.
pub fn new(
fee_rule: FixedFeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Self {
Self { Self {
fee_rule, fee_rule,
change_memo, change_memo,
fallback_change_pool,
} }
} }
} }
@ -65,6 +76,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
dust_output_policy, dust_output_policy,
self.fee_rule().fixed_fee(), self.fee_rule().fixed_fee(),
self.change_memo.clone(), self.change_memo.clone(),
self.fallback_change_pool,
) )
} }
} }
@ -89,13 +101,15 @@ mod tests {
tests::{TestSaplingInput, TestTransparentInput}, tests::{TestSaplingInput, TestTransparentInput},
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy, ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
}, },
ShieldedProtocol,
}; };
#[test] #[test]
fn change_without_dust() { fn change_without_dust() {
#[allow(deprecated)] #[allow(deprecated)]
let fee_rule = FixedFeeRule::standard(); let fee_rule = FixedFeeRule::standard();
let change_strategy = SingleOutputChangeStrategy::new(fee_rule, None); let change_strategy =
SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling);
// spend a single Sapling note that is sufficient to pay the fee // spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance( let result = change_strategy.compute_balance(
@ -136,7 +150,8 @@ mod tests {
fn dust_change() { fn dust_change() {
#[allow(deprecated)] #[allow(deprecated)]
let fee_rule = FixedFeeRule::standard(); let fee_rule = FixedFeeRule::standard();
let change_strategy = SingleOutputChangeStrategy::new(fee_rule, None); let change_strategy =
SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling);
// spend a single Sapling note that is sufficient to pay the fee // spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance( let result = change_strategy.compute_balance(

View File

@ -14,6 +14,8 @@ use zcash_primitives::{
}, },
}; };
use crate::ShieldedProtocol;
use super::{ use super::{
fixed, sapling as sapling_fees, zip317, ChangeError, ChangeStrategy, DustOutputPolicy, fixed, sapling as sapling_fees, zip317, ChangeError, ChangeStrategy, DustOutputPolicy,
TransactionBalance, TransactionBalance,
@ -27,15 +29,24 @@ use super::orchard as orchard_fees;
pub struct SingleOutputChangeStrategy { pub struct SingleOutputChangeStrategy {
fee_rule: StandardFeeRule, fee_rule: StandardFeeRule,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
} }
impl SingleOutputChangeStrategy { impl SingleOutputChangeStrategy {
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317 /// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
/// fee parameters. /// fee parameters.
pub fn new(fee_rule: StandardFeeRule, change_memo: Option<MemoBytes>) -> Self { ///
/// `fallback_change_pool` is used when more than one shielded pool is enabled via
/// feature flags, and the transaction has no shielded inputs.
pub fn new(
fee_rule: StandardFeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Self {
Self { Self {
fee_rule, fee_rule,
change_memo, change_memo,
fallback_change_pool,
} }
} }
} }
@ -63,6 +74,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
StandardFeeRule::PreZip313 => fixed::SingleOutputChangeStrategy::new( StandardFeeRule::PreZip313 => fixed::SingleOutputChangeStrategy::new(
FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(10000)), FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(10000)),
self.change_memo.clone(), self.change_memo.clone(),
self.fallback_change_pool,
) )
.compute_balance( .compute_balance(
params, params,
@ -78,6 +90,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
StandardFeeRule::Zip313 => fixed::SingleOutputChangeStrategy::new( StandardFeeRule::Zip313 => fixed::SingleOutputChangeStrategy::new(
FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(1000)), FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(1000)),
self.change_memo.clone(), self.change_memo.clone(),
self.fallback_change_pool,
) )
.compute_balance( .compute_balance(
params, params,
@ -93,6 +106,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
StandardFeeRule::Zip317 => zip317::SingleOutputChangeStrategy::new( StandardFeeRule::Zip317 => zip317::SingleOutputChangeStrategy::new(
Zip317FeeRule::standard(), Zip317FeeRule::standard(),
self.change_memo.clone(), self.change_memo.clone(),
self.fallback_change_pool,
) )
.compute_balance( .compute_balance(
params, params,

View File

@ -13,6 +13,8 @@ use zcash_primitives::{
}, },
}; };
use crate::ShieldedProtocol;
use super::{ use super::{
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy, common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
DustOutputPolicy, TransactionBalance, DustOutputPolicy, TransactionBalance,
@ -26,15 +28,24 @@ use super::orchard as orchard_fees;
pub struct SingleOutputChangeStrategy { pub struct SingleOutputChangeStrategy {
fee_rule: Zip317FeeRule, fee_rule: Zip317FeeRule,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
} }
impl SingleOutputChangeStrategy { impl SingleOutputChangeStrategy {
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317 /// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
/// fee parameters and change memo. /// fee parameters and change memo.
pub fn new(fee_rule: Zip317FeeRule, change_memo: Option<MemoBytes>) -> Self { ///
/// `fallback_change_pool` is used when more than one shielded pool is enabled via
/// feature flags, and the transaction has no shielded inputs.
pub fn new(
fee_rule: Zip317FeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Self {
Self { Self {
fee_rule, fee_rule,
change_memo, change_memo,
fallback_change_pool,
} }
} }
} }
@ -145,6 +156,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
dust_output_policy, dust_output_policy,
self.fee_rule.marginal_fee(), self.fee_rule.marginal_fee(),
self.change_memo.clone(), self.change_memo.clone(),
self.fallback_change_pool,
) )
} }
} }
@ -170,11 +182,16 @@ mod tests {
tests::{TestSaplingInput, TestTransparentInput}, tests::{TestSaplingInput, TestTransparentInput},
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy, ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
}, },
ShieldedProtocol,
}; };
#[test] #[test]
fn change_without_dust() { fn change_without_dust() {
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None); let change_strategy = SingleOutputChangeStrategy::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
);
// spend a single Sapling note that is sufficient to pay the fee // spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance( let result = change_strategy.compute_balance(
@ -213,7 +230,11 @@ mod tests {
#[test] #[test]
fn change_with_transparent_payments() { fn change_with_transparent_payments() {
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None); let change_strategy = SingleOutputChangeStrategy::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
);
// spend a single Sapling note that is sufficient to pay the fee // spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance( let result = change_strategy.compute_balance(
@ -252,7 +273,11 @@ mod tests {
#[test] #[test]
fn change_with_allowable_dust() { fn change_with_allowable_dust() {
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None); let change_strategy = SingleOutputChangeStrategy::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
);
// spend a single Sapling note that is sufficient to pay the fee // spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance( let result = change_strategy.compute_balance(
@ -296,7 +321,11 @@ mod tests {
#[test] #[test]
fn change_with_disallowed_dust() { fn change_with_disallowed_dust() {
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None); let change_strategy = SingleOutputChangeStrategy::new(
Zip317FeeRule::standard(),
None,
ShieldedProtocol::Sapling,
);
// spend a single Sapling note that is sufficient to pay the fee // spend a single Sapling note that is sufficient to pay the fee
let result = change_strategy.compute_balance( let result = change_strategy.compute_balance(

View File

@ -97,7 +97,6 @@ pub enum ShieldedProtocol {
/// The Sapling protocol /// The Sapling protocol
Sapling, Sapling,
/// The Orchard protocol /// The Orchard protocol
#[cfg(zcash_unstable = "orchard")]
Orchard, Orchard,
} }
@ -118,7 +117,6 @@ impl PoolType {
Address::Unified(ua) => match self { Address::Unified(ua) => match self {
PoolType::Transparent => ua.transparent().is_some(), PoolType::Transparent => ua.transparent().is_some(),
PoolType::Shielded(ShieldedProtocol::Sapling) => ua.sapling().is_some(), PoolType::Shielded(ShieldedProtocol::Sapling) => ua.sapling().is_some(),
#[cfg(zcash_unstable = "orchard")]
PoolType::Shielded(ShieldedProtocol::Orchard) => { PoolType::Shielded(ShieldedProtocol::Orchard) => {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
return ua.orchard().is_some(); return ua.orchard().is_some();
@ -136,7 +134,6 @@ impl fmt::Display for PoolType {
match self { match self {
PoolType::Transparent => f.write_str("Transparent"), PoolType::Transparent => f.write_str("Transparent"),
PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"), PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"),
#[cfg(zcash_unstable = "orchard")]
PoolType::Shielded(ShieldedProtocol::Orchard) => f.write_str("Orchard"), PoolType::Shielded(ShieldedProtocol::Orchard) => f.write_str("Orchard"),
} }
} }

View File

@ -490,6 +490,59 @@ impl<NoteRef> Step<NoteRef> {
pub fn is_shielding(&self) -> bool { pub fn is_shielding(&self) -> bool {
self.is_shielding self.is_shielding
} }
/// Returns whether or not this proposal requires interaction with the specified pool
pub fn involves(&self, pool_type: PoolType) -> bool {
match pool_type {
PoolType::Transparent => {
self.is_shielding
|| !self.transparent_inputs.is_empty()
|| self
.payment_pools()
.values()
.any(|pool| matches!(pool, PoolType::Transparent))
}
PoolType::Shielded(ShieldedProtocol::Sapling) => {
let sapling_in = self.shielded_inputs.iter().any(|s_in| {
s_in.notes()
.iter()
.any(|note| matches!(note.note(), Note::Sapling(_)))
});
let sapling_out = self
.payment_pools()
.values()
.any(|pool| matches!(pool, PoolType::Shielded(ShieldedProtocol::Sapling)));
let sapling_change = self
.balance
.proposed_change()
.iter()
.any(|c| c.output_pool() == ShieldedProtocol::Sapling);
sapling_in || sapling_out || sapling_change
}
PoolType::Shielded(ShieldedProtocol::Orchard) => {
#[cfg(not(feature = "orchard"))]
let orchard_in = false;
#[cfg(feature = "orchard")]
let orchard_in = self.shielded_inputs.iter().any(|s_in| {
s_in.notes()
.iter()
.any(|note| matches!(note.note(), Note::Orchard(_)))
});
let orchard_out = self
.payment_pools()
.values()
.any(|pool| matches!(pool, PoolType::Shielded(ShieldedProtocol::Orchard)));
let orchard_change = self
.balance
.proposed_change()
.iter()
.any(|c| c.output_pool() == ShieldedProtocol::Orchard);
orchard_in || orchard_out || orchard_change
}
}
}
} }
impl<NoteRef> Debug for Step<NoteRef> { impl<NoteRef> Debug for Step<NoteRef> {
@ -501,6 +554,7 @@ impl<NoteRef> Debug for Step<NoteRef> {
"shielded_inputs", "shielded_inputs",
&self.shielded_inputs().map(|i| i.notes.len()), &self.shielded_inputs().map(|i| i.notes.len()),
) )
.field("prior_step_inputs", &self.prior_step_inputs)
.field( .field(
"anchor_height", "anchor_height",
&self.shielded_inputs().map(|i| i.anchor_height), &self.shielded_inputs().map(|i| i.anchor_height),

View File

@ -319,7 +319,6 @@ fn pool_type<T>(pool_id: i32) -> Result<PoolType, ProposalDecodingError<T>> {
match proposal::ValuePool::try_from(pool_id) { match proposal::ValuePool::try_from(pool_id) {
Ok(proposal::ValuePool::Transparent) => Ok(PoolType::Transparent), Ok(proposal::ValuePool::Transparent) => Ok(PoolType::Transparent),
Ok(proposal::ValuePool::Sapling) => Ok(PoolType::Shielded(ShieldedProtocol::Sapling)), Ok(proposal::ValuePool::Sapling) => Ok(PoolType::Shielded(ShieldedProtocol::Sapling)),
#[cfg(zcash_unstable = "orchard")]
Ok(proposal::ValuePool::Orchard) => Ok(PoolType::Shielded(ShieldedProtocol::Orchard)), Ok(proposal::ValuePool::Orchard) => Ok(PoolType::Shielded(ShieldedProtocol::Orchard)),
_ => Err(ProposalDecodingError::ValuePoolNotSupported(pool_id)), _ => Err(ProposalDecodingError::ValuePoolNotSupported(pool_id)),
} }
@ -354,7 +353,6 @@ impl From<ShieldedProtocol> for proposal::ValuePool {
fn from(value: ShieldedProtocol) -> Self { fn from(value: ShieldedProtocol) -> Self {
match value { match value {
ShieldedProtocol::Sapling => proposal::ValuePool::Sapling, ShieldedProtocol::Sapling => proposal::ValuePool::Sapling,
#[cfg(zcash_unstable = "orchard")]
ShieldedProtocol::Orchard => proposal::ValuePool::Orchard, ShieldedProtocol::Orchard => proposal::ValuePool::Orchard,
} }
} }

View File

@ -65,11 +65,33 @@ impl NoteId {
/// internal account ID and the pool to which funds were sent in the case of a wallet-internal /// internal account ID and the pool to which funds were sent in the case of a wallet-internal
/// output. /// output.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Recipient { pub enum Recipient<N> {
Transparent(TransparentAddress), Transparent(TransparentAddress),
Sapling(sapling::PaymentAddress), Sapling(sapling::PaymentAddress),
Unified(UnifiedAddress, PoolType), Unified(UnifiedAddress, PoolType),
InternalAccount(AccountId, PoolType), InternalAccount(AccountId, N),
}
impl<N> Recipient<N> {
pub fn map_internal_account<B, F: FnOnce(N) -> B>(self, f: F) -> Recipient<B> {
match self {
Recipient::Transparent(t) => Recipient::Transparent(t),
Recipient::Sapling(s) => Recipient::Sapling(s),
Recipient::Unified(u, p) => Recipient::Unified(u, p),
Recipient::InternalAccount(a, n) => Recipient::InternalAccount(a, f(n)),
}
}
}
impl<N> Recipient<Option<N>> {
pub fn internal_account_transpose_option(self) -> Option<Recipient<N>> {
match self {
Recipient::Transparent(t) => Some(Recipient::Transparent(t)),
Recipient::Sapling(s) => Some(Recipient::Sapling(s)),
Recipient::Unified(u, p) => Some(Recipient::Unified(u, p)),
Recipient::InternalAccount(a, n) => n.map(|n0| Recipient::InternalAccount(a, n0)),
}
}
} }
/// A subset of a [`Transaction`] relevant to wallets and light clients. /// A subset of a [`Transaction`] relevant to wallets and light clients.
@ -380,26 +402,43 @@ impl<NoteRef> orchard_fees::InputView<NoteRef> for ReceivedNote<NoteRef, orchard
/// [ZIP 310]: https://zips.z.cash/zip-0310 /// [ZIP 310]: https://zips.z.cash/zip-0310
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum OvkPolicy { pub enum OvkPolicy {
/// Use the outgoing viewing key from the sender's [`ExtendedFullViewingKey`]. /// Use the outgoing viewing key from the sender's [`UnifiedFullViewingKey`].
/// ///
/// Transaction outputs will be decryptable by the sender, in addition to the /// Transaction outputs will be decryptable by the sender, in addition to the
/// recipients. /// recipients.
/// ///
/// [`ExtendedFullViewingKey`]: sapling::zip32::ExtendedFullViewingKey /// [`UnifiedFullViewingKey`]: zcash_keys::keys::UnifiedFullViewingKey
Sender, Sender,
/// Use a custom outgoing viewing key. This might for instance be derived from a /// Use custom outgoing viewing keys. These might for instance be derived from a
/// separate seed than the wallet's spending keys. /// different seed than the wallet's spending keys.
/// ///
/// Transaction outputs will be decryptable by the recipients, and whoever controls /// Transaction outputs will be decryptable by the recipients, and whoever controls
/// the provided outgoing viewing key. /// the provided outgoing viewing keys.
Custom(sapling::keys::OutgoingViewingKey), Custom {
sapling: sapling::keys::OutgoingViewingKey,
/// Use no outgoing viewing key. Transaction outputs will be decryptable by their #[cfg(feature = "orchard")]
orchard: orchard::keys::OutgoingViewingKey,
},
/// Use no outgoing viewing keys. Transaction outputs will be decryptable by their
/// recipients, but not by the sender. /// recipients, but not by the sender.
Discard, Discard,
} }
impl OvkPolicy {
/// Constructs an [`OvkPolicy::Custom`] value from a single arbitrary 32-byte key.
///
/// Outputs of transactions created with this OVK policy will be recoverable using
/// this key irrespective of the output pool.
pub fn custom_from_common_bytes(key: &[u8; 32]) -> Self {
OvkPolicy::Custom {
sapling: sapling::keys::OutgoingViewingKey(*key),
#[cfg(feature = "orchard")]
orchard: orchard::keys::OutgoingViewingKey::from(*key),
}
}
}
/// Metadata related to the ZIP 32 derivation of a transparent address. /// Metadata related to the ZIP 32 derivation of a transparent address.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]

View File

@ -44,7 +44,7 @@ jubjub.workspace = true
secrecy.workspace = true secrecy.workspace = true
# - Shielded protocols # - Shielded protocols
orchard.workspace = true orchard = { workspace = true, optional = true }
sapling.workspace = true sapling.workspace = true
# - Note commitment trees # - Note commitment trees
@ -90,7 +90,7 @@ multicore = ["maybe-rayon/threads", "zcash_primitives/multicore"]
## Enables support for storing data related to the sending and receiving of ## Enables support for storing data related to the sending and receiving of
## Orchard funds. ## Orchard funds.
orchard = ["zcash_client_backend/orchard"] orchard = ["dep:orchard", "zcash_client_backend/orchard"]
## Exposes APIs that are useful for testing, such as `proptest` strategies. ## Exposes APIs that are useful for testing, such as `proptest` strategies.
test-dependencies = [ test-dependencies = [

View File

@ -340,6 +340,7 @@ mod tests {
scanning::ScanError, scanning::ScanError,
wallet::OvkPolicy, wallet::OvkPolicy,
zip321::{Payment, TransactionRequest}, zip321::{Payment, TransactionRequest},
ShieldedProtocol,
}; };
use crate::{ use crate::{
@ -529,7 +530,7 @@ mod tests {
}]) }])
.unwrap(); .unwrap();
let input_selector = GreedyInputSelector::new( let input_selector = GreedyInputSelector::new(
SingleOutputChangeStrategy::new(FeeRule::standard(), None), SingleOutputChangeStrategy::new(FeeRule::standard(), None, ShieldedProtocol::Sapling),
DustOutputPolicy::default(), DustOutputPolicy::default(),
); );
assert_matches!( assert_matches!(

View File

@ -69,11 +69,14 @@ use zcash_client_backend::{
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock, proto::compact_formats::CompactBlock,
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput}, wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
DecryptedOutput, PoolType, ShieldedProtocol, TransferType, DecryptedOutput, ShieldedProtocol, TransferType,
}; };
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore}; use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
#[cfg(feature = "orchard")]
use zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use {
zcash_client_backend::wallet::TransparentAddressMetadata, zcash_client_backend::wallet::TransparentAddressMetadata,
@ -180,10 +183,18 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
fn get_spendable_note( fn get_spendable_note(
&self, &self,
txid: &TxId, txid: &TxId,
_protocol: ShieldedProtocol, protocol: ShieldedProtocol,
index: u32, index: u32,
) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> { ) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
wallet::sapling::get_spendable_sapling_note(self.conn.borrow(), &self.params, txid, index) match protocol {
ShieldedProtocol::Sapling => wallet::sapling::get_spendable_sapling_note(
self.conn.borrow(),
&self.params,
txid,
index,
),
ShieldedProtocol::Orchard => Ok(None),
}
} }
fn select_spendable_notes( fn select_spendable_notes(
@ -588,12 +599,13 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
for output in d_tx.sapling_outputs { for output in d_tx.sapling_outputs {
match output.transfer_type { match output.transfer_type {
TransferType::Outgoing | TransferType::WalletInternal => { TransferType::Outgoing | TransferType::WalletInternal => {
let value = output.note.value();
let recipient = if output.transfer_type == TransferType::Outgoing { let recipient = if output.transfer_type == TransferType::Outgoing {
Recipient::Sapling(output.note.recipient()) Recipient::Sapling(output.note.recipient())
} else { } else {
Recipient::InternalAccount( Recipient::InternalAccount(
output.account, output.account,
PoolType::Shielded(ShieldedProtocol::Sapling) Note::Sapling(output.note.clone()),
) )
}; };
@ -604,7 +616,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
tx_ref, tx_ref,
output.index, output.index,
&recipient, &recipient,
NonNegativeAmount::from_u64(output.note.value().inner()).map_err(|_| { NonNegativeAmount::from_u64(value.inner()).map_err(|_| {
SqliteClientError::CorruptedData( SqliteClientError::CorruptedData(
"Note value is not a valid Zcash amount.".to_string(), "Note value is not a valid Zcash amount.".to_string(),
) )
@ -712,21 +724,28 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
output, output,
)?; )?;
if let Some((account, note)) = output.sapling_change_to() { match output.recipient() {
wallet::sapling::put_received_note( Recipient::InternalAccount(account, Note::Sapling(note)) => {
wdb.conn.0, wallet::sapling::put_received_note(
&DecryptedOutput { wdb.conn.0,
index: output.output_index(), &DecryptedOutput {
note: note.clone(), index: output.output_index(),
account: *account, note: note.clone(),
memo: output account: *account,
.memo() memo: output
.map_or_else(MemoBytes::empty, |memo| memo.clone()), .memo()
transfer_type: TransferType::WalletInternal, .map_or_else(MemoBytes::empty, |memo| memo.clone()),
}, transfer_type: TransferType::WalletInternal,
tx_ref, },
None, tx_ref,
)?; None,
)?;
}
#[cfg(feature = "orchard")]
Recipient::InternalAccount(_account, Note::Orchard(_note)) => {
todo!();
}
_ => (),
} }
} }
@ -805,6 +824,37 @@ impl<P: consensus::Parameters> WalletCommitmentTrees for WalletDb<rusqlite::Conn
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?; .map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
Ok(()) Ok(())
} }
#[cfg(feature = "orchard")]
type OrchardShardStore<'a> = SqliteShardStore<
&'a rusqlite::Transaction<'a>,
orchard::tree::MerkleHashOrchard,
ORCHARD_SHARD_HEIGHT,
>;
#[cfg(feature = "orchard")]
fn with_orchard_tree_mut<F, A, E>(&mut self, _callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::OrchardShardStore<'a>,
{ ORCHARD_SHARD_HEIGHT * 2 },
ORCHARD_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>,
{
todo!()
}
#[cfg(feature = "orchard")]
fn put_orchard_subtree_roots(
&mut self,
_start_index: u64,
_roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>> {
todo!()
}
} }
impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTransaction<'conn>, P> { impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTransaction<'conn>, P> {
@ -845,6 +895,37 @@ impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTran
roots, roots,
) )
} }
#[cfg(feature = "orchard")]
type OrchardShardStore<'a> = SqliteShardStore<
&'a rusqlite::Transaction<'a>,
orchard::tree::MerkleHashOrchard,
ORCHARD_SHARD_HEIGHT,
>;
#[cfg(feature = "orchard")]
fn with_orchard_tree_mut<F, A, E>(&mut self, _callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::OrchardShardStore<'a>,
{ ORCHARD_SHARD_HEIGHT * 2 },
ORCHARD_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>,
{
todo!()
}
#[cfg(feature = "orchard")]
fn put_orchard_subtree_roots(
&mut self,
_start_index: u64,
_roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>> {
todo!()
}
} }
/// A handle for the SQLite block source. /// A handle for the SQLite block source.

View File

@ -22,7 +22,6 @@ use sapling::{
zip32::DiversifiableFullViewingKey, zip32::DiversifiableFullViewingKey,
Note, Nullifier, PaymentAddress, Note, Nullifier, PaymentAddress,
}; };
use zcash_client_backend::fees::{standard, DustOutputPolicy};
#[allow(deprecated)] #[allow(deprecated)]
use zcash_client_backend::{ use zcash_client_backend::{
address::Address, address::Address,
@ -45,6 +44,10 @@ use zcash_client_backend::{
wallet::OvkPolicy, wallet::OvkPolicy,
zip321, zip321,
}; };
use zcash_client_backend::{
fees::{standard, DustOutputPolicy},
ShieldedProtocol,
};
use zcash_note_encryption::Domain; use zcash_note_encryption::Domain;
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
@ -442,6 +445,7 @@ impl<Cache> TestState<Cache> {
ovk_policy: OvkPolicy, ovk_policy: OvkPolicy,
min_confirmations: NonZeroU32, min_confirmations: NonZeroU32,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Result< ) -> Result<
NonEmpty<TxId>, NonEmpty<TxId>,
data_api::error::Error< data_api::error::Error<
@ -465,6 +469,7 @@ impl<Cache> TestState<Cache> {
ovk_policy, ovk_policy,
min_confirmations, min_confirmations,
change_memo, change_memo,
fallback_change_pool,
) )
} }
@ -547,6 +552,7 @@ impl<Cache> TestState<Cache> {
amount: NonNegativeAmount, amount: NonNegativeAmount,
memo: Option<MemoBytes>, memo: Option<MemoBytes>,
change_memo: Option<MemoBytes>, change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Result< ) -> Result<
Proposal<StandardFeeRule, ReceivedNoteId>, Proposal<StandardFeeRule, ReceivedNoteId>,
data_api::error::Error< data_api::error::Error<
@ -567,6 +573,7 @@ impl<Cache> TestState<Cache> {
amount, amount,
memo, memo,
change_memo, change_memo,
fallback_change_pool,
); );
if let Ok(proposal) = &result { if let Ok(proposal) = &result {
@ -1053,12 +1060,14 @@ impl TestCache for FsBlockCache {
pub(crate) fn input_selector( pub(crate) fn input_selector(
fee_rule: StandardFeeRule, fee_rule: StandardFeeRule,
change_memo: Option<&str>, change_memo: Option<&str>,
fallback_change_pool: ShieldedProtocol,
) -> GreedyInputSelector< ) -> GreedyInputSelector<
WalletDb<rusqlite::Connection, Network>, WalletDb<rusqlite::Connection, Network>,
standard::SingleOutputChangeStrategy, standard::SingleOutputChangeStrategy,
> { > {
let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::<Memo>().unwrap())); let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::<Memo>().unwrap()));
let change_strategy = standard::SingleOutputChangeStrategy::new(fee_rule, change_memo); let change_strategy =
standard::SingleOutputChangeStrategy::new(fee_rule, change_memo, fallback_change_pool);
GreedyInputSelector::new(change_strategy, DustOutputPolicy::default()) GreedyInputSelector::new(change_strategy, DustOutputPolicy::default())
} }

View File

@ -74,6 +74,7 @@ use std::num::NonZeroU32;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use tracing::debug; use tracing::debug;
use zcash_client_backend::data_api::{AccountBalance, Ratio, WalletSummary}; use zcash_client_backend::data_api::{AccountBalance, Ratio, WalletSummary};
use zcash_client_backend::wallet::Note;
use zcash_primitives::transaction::components::amount::NonNegativeAmount; use zcash_primitives::transaction::components::amount::NonNegativeAmount;
use zcash_primitives::zip32::Scope; use zcash_primitives::zip32::Scope;
@ -136,7 +137,6 @@ pub(crate) fn pool_code(pool_type: PoolType) -> i64 {
match pool_type { match pool_type {
PoolType::Transparent => 0i64, PoolType::Transparent => 0i64,
PoolType::Shielded(ShieldedProtocol::Sapling) => 2i64, PoolType::Shielded(ShieldedProtocol::Sapling) => 2i64,
#[cfg(zcash_unstable = "orchard")]
PoolType::Shielded(ShieldedProtocol::Orchard) => 3i64, PoolType::Shielded(ShieldedProtocol::Orchard) => 3i64,
} }
} }
@ -817,7 +817,6 @@ pub(crate) fn get_received_memo(
) )
.optional()? .optional()?
.flatten(), .flatten(),
#[cfg(zcash_unstable = "orchard")]
_ => { _ => {
return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded( return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded(
note_id.protocol(), note_id.protocol(),
@ -1796,7 +1795,7 @@ pub(crate) fn update_expired_notes(
// and `put_sent_output` // and `put_sent_output`
fn recipient_params<P: consensus::Parameters>( fn recipient_params<P: consensus::Parameters>(
params: &P, params: &P,
to: &Recipient, to: &Recipient<Note>,
) -> (Option<String>, Option<u32>, PoolType) { ) -> (Option<String>, Option<u32>, PoolType) {
match to { match to {
Recipient::Transparent(addr) => (Some(addr.encode(params)), None, PoolType::Transparent), Recipient::Transparent(addr) => (Some(addr.encode(params)), None, PoolType::Transparent),
@ -1806,7 +1805,11 @@ fn recipient_params<P: consensus::Parameters>(
PoolType::Shielded(ShieldedProtocol::Sapling), PoolType::Shielded(ShieldedProtocol::Sapling),
), ),
Recipient::Unified(addr, pool) => (Some(addr.encode(params)), None, *pool), Recipient::Unified(addr, pool) => (Some(addr.encode(params)), None, *pool),
Recipient::InternalAccount(id, pool) => (None, Some(u32::from(*id)), *pool), Recipient::InternalAccount(id, note) => (
None,
Some(u32::from(*id)),
PoolType::Shielded(note.protocol()),
),
} }
} }
@ -1862,7 +1865,7 @@ pub(crate) fn put_sent_output<P: consensus::Parameters>(
from_account: AccountId, from_account: AccountId,
tx_ref: i64, tx_ref: i64,
output_index: usize, output_index: usize,
recipient: &Recipient, recipient: &Recipient<Note>,
value: NonNegativeAmount, value: NonNegativeAmount,
memo: Option<&MemoBytes>, memo: Option<&MemoBytes>,
) -> Result<(), SqliteClientError> { ) -> Result<(), SqliteClientError> {
@ -2227,6 +2230,8 @@ mod tests {
#[test] #[test]
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
fn transparent_balance_across_shielding() { fn transparent_balance_across_shielding() {
use zcash_client_backend::ShieldedProtocol;
let mut st = TestBuilder::new() let mut st = TestBuilder::new()
.with_block_cache() .with_block_cache()
.with_test_account(AccountBirthday::from_sapling_activation) .with_test_account(AccountBirthday::from_sapling_activation)
@ -2311,6 +2316,7 @@ mod tests {
fixed::SingleOutputChangeStrategy::new( fixed::SingleOutputChangeStrategy::new(
FixedFeeRule::non_standard(NonNegativeAmount::ZERO), FixedFeeRule::non_standard(NonNegativeAmount::ZERO),
None, None,
ShieldedProtocol::Sapling,
), ),
DustOutputPolicy::default(), DustOutputPolicy::default(),
); );

View File

@ -579,8 +579,11 @@ pub(crate) mod tests {
let fee_rule = StandardFeeRule::PreZip313; let fee_rule = StandardFeeRule::PreZip313;
let change_memo = "Test change memo".parse::<Memo>().unwrap(); let change_memo = "Test change memo".parse::<Memo>().unwrap();
let change_strategy = let change_strategy = standard::SingleOutputChangeStrategy::new(
standard::SingleOutputChangeStrategy::new(fee_rule, Some(change_memo.clone().into())); fee_rule,
Some(change_memo.clone().into()),
ShieldedProtocol::Sapling,
);
let input_selector = let input_selector =
&GreedyInputSelector::new(change_strategy, DustOutputPolicy::default()); &GreedyInputSelector::new(change_strategy, DustOutputPolicy::default());
@ -731,7 +734,7 @@ pub(crate) mod tests {
let fee_rule = StandardFeeRule::Zip317; let fee_rule = StandardFeeRule::Zip317;
let input_selector = GreedyInputSelector::new( let input_selector = GreedyInputSelector::new(
standard::SingleOutputChangeStrategy::new(fee_rule, None), standard::SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling),
DustOutputPolicy::default(), DustOutputPolicy::default(),
); );
let proposal0 = st let proposal0 = st
@ -856,7 +859,8 @@ pub(crate) mod tests {
None, None,
OvkPolicy::Sender, OvkPolicy::Sender,
NonZeroU32::new(1).unwrap(), NonZeroU32::new(1).unwrap(),
None None,
ShieldedProtocol::Sapling
), ),
Err(data_api::error::Error::KeyNotRecognized) Err(data_api::error::Error::KeyNotRecognized)
); );
@ -885,7 +889,8 @@ pub(crate) mod tests {
&to, &to,
NonNegativeAmount::const_from_u64(1), NonNegativeAmount::const_from_u64(1),
None, None,
None None,
ShieldedProtocol::Sapling
), ),
Err(data_api::error::Error::ScanRequired) Err(data_api::error::Error::ScanRequired)
); );
@ -952,7 +957,8 @@ pub(crate) mod tests {
&to, &to,
NonNegativeAmount::const_from_u64(70000), NonNegativeAmount::const_from_u64(70000),
None, None,
None None,
ShieldedProtocol::Sapling
), ),
Err(data_api::error::Error::InsufficientFunds { Err(data_api::error::Error::InsufficientFunds {
available, available,
@ -981,7 +987,8 @@ pub(crate) mod tests {
&to, &to,
NonNegativeAmount::const_from_u64(70000), NonNegativeAmount::const_from_u64(70000),
None, None,
None None,
ShieldedProtocol::Sapling
), ),
Err(data_api::error::Error::InsufficientFunds { Err(data_api::error::Error::InsufficientFunds {
available, available,
@ -1016,6 +1023,7 @@ pub(crate) mod tests {
amount_sent, amount_sent,
None, None,
None, None,
ShieldedProtocol::Sapling,
) )
.unwrap(); .unwrap();
@ -1073,6 +1081,7 @@ pub(crate) mod tests {
NonNegativeAmount::const_from_u64(15000), NonNegativeAmount::const_from_u64(15000),
None, None,
None, None,
ShieldedProtocol::Sapling,
) )
.unwrap(); .unwrap();
@ -1091,7 +1100,8 @@ pub(crate) mod tests {
&to, &to,
NonNegativeAmount::const_from_u64(2000), NonNegativeAmount::const_from_u64(2000),
None, None,
None None,
ShieldedProtocol::Sapling
), ),
Err(data_api::error::Error::InsufficientFunds { Err(data_api::error::Error::InsufficientFunds {
available, available,
@ -1120,7 +1130,8 @@ pub(crate) mod tests {
&to, &to,
NonNegativeAmount::const_from_u64(2000), NonNegativeAmount::const_from_u64(2000),
None, None,
None None,
ShieldedProtocol::Sapling
), ),
Err(data_api::error::Error::InsufficientFunds { Err(data_api::error::Error::InsufficientFunds {
available, available,
@ -1153,6 +1164,7 @@ pub(crate) mod tests {
amount_sent2, amount_sent2,
None, None,
None, None,
ShieldedProtocol::Sapling,
) )
.unwrap(); .unwrap();
@ -1220,6 +1232,7 @@ pub(crate) mod tests {
NonNegativeAmount::const_from_u64(15000), NonNegativeAmount::const_from_u64(15000),
None, None,
None, None,
ShieldedProtocol::Sapling,
)?; )?;
// Executing the proposal should succeed // Executing the proposal should succeed
@ -1322,6 +1335,7 @@ pub(crate) mod tests {
NonNegativeAmount::const_from_u64(50000), NonNegativeAmount::const_from_u64(50000),
None, None,
None, None,
ShieldedProtocol::Sapling,
) )
.unwrap(); .unwrap();
@ -1384,6 +1398,7 @@ pub(crate) mod tests {
NonNegativeAmount::const_from_u64(50000), NonNegativeAmount::const_from_u64(50000),
None, None,
None, None,
ShieldedProtocol::Sapling,
) )
.unwrap(); .unwrap();
@ -1455,7 +1470,7 @@ pub(crate) mod tests {
#[allow(deprecated)] #[allow(deprecated)]
let fee_rule = FixedFeeRule::standard(); let fee_rule = FixedFeeRule::standard();
let input_selector = GreedyInputSelector::new( let input_selector = GreedyInputSelector::new(
fixed::SingleOutputChangeStrategy::new(fee_rule, None), fixed::SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling),
DustOutputPolicy::default(), DustOutputPolicy::default(),
); );
@ -1552,7 +1567,8 @@ pub(crate) mod tests {
assert_eq!(st.get_total_balance(account), total); assert_eq!(st.get_total_balance(account), total);
assert_eq!(st.get_spendable_balance(account, 1), total); assert_eq!(st.get_spendable_balance(account, 1), total);
let input_selector = input_selector(StandardFeeRule::Zip317, None); let input_selector =
input_selector(StandardFeeRule::Zip317, None, ShieldedProtocol::Sapling);
// This first request will fail due to insufficient non-dust funds // This first request will fail due to insufficient non-dust funds
let req = TransactionRequest::new(vec![Payment { let req = TransactionRequest::new(vec![Payment {
@ -1657,7 +1673,7 @@ pub(crate) mod tests {
let fee_rule = StandardFeeRule::PreZip313; let fee_rule = StandardFeeRule::PreZip313;
let input_selector = GreedyInputSelector::new( let input_selector = GreedyInputSelector::new(
standard::SingleOutputChangeStrategy::new(fee_rule, None), standard::SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling),
DustOutputPolicy::default(), DustOutputPolicy::default(),
); );
@ -1825,7 +1841,8 @@ pub(crate) mod tests {
None, None,
OvkPolicy::Sender, OvkPolicy::Sender,
NonZeroU32::new(5).unwrap(), NonZeroU32::new(5).unwrap(),
None None,
ShieldedProtocol::Sapling
), ),
Ok(_) Ok(_)
); );

View File

@ -14,7 +14,8 @@ The entries below are relative to the `zcash_client_backend` crate as of
- `address` - `address`
- `encoding` - `encoding`
- `keys` - `keys`
- `zcash_keys::address::UnifiedAddress::unknown`: - `zcash_keys::address::UnifiedAddress::{unknown, has_orchard, has_sapling,
has_transparent, receiver_types}`:
- `zcash_keys::keys`: - `zcash_keys::keys`:
- `AddressGenerationError` - `AddressGenerationError`
- `UnifiedAddressRequest` - `UnifiedAddressRequest`

View File

@ -1,10 +1,8 @@
//! Structs for handling supported address types. //! Structs for handling supported address types.
use std::convert::TryFrom;
use sapling::PaymentAddress; use sapling::PaymentAddress;
use zcash_address::{ use zcash_address::{
unified::{self, Container, Encoding}, unified::{self, Container, Encoding, Typecode},
ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress, ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress,
}; };
use zcash_primitives::{consensus, legacy::TransparentAddress}; use zcash_primitives::{consensus, legacy::TransparentAddress};
@ -108,17 +106,37 @@ impl UnifiedAddress {
} }
} }
/// Returns whether this address has an Orchard receiver.
///
/// This method is available irrespective of whether the `orchard` feature flag is enabled.
pub fn has_orchard(&self) -> bool {
#[cfg(not(feature = "orchard"))]
return false;
#[cfg(feature = "orchard")]
return self.orchard.is_some();
}
/// Returns the Orchard receiver within this Unified Address, if any. /// Returns the Orchard receiver within this Unified Address, if any.
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
pub fn orchard(&self) -> Option<&orchard::Address> { pub fn orchard(&self) -> Option<&orchard::Address> {
self.orchard.as_ref() self.orchard.as_ref()
} }
/// Returns whether this address has a Sapling receiver.
pub fn has_sapling(&self) -> bool {
self.sapling.is_some()
}
/// Returns the Sapling receiver within this Unified Address, if any. /// Returns the Sapling receiver within this Unified Address, if any.
pub fn sapling(&self) -> Option<&PaymentAddress> { pub fn sapling(&self) -> Option<&PaymentAddress> {
self.sapling.as_ref() self.sapling.as_ref()
} }
/// Returns whether this address has a Transparent receiver.
pub fn has_transparent(&self) -> bool {
self.transparent.is_some()
}
/// Returns the transparent receiver within this Unified Address, if any. /// Returns the transparent receiver within this Unified Address, if any.
pub fn transparent(&self) -> Option<&TransparentAddress> { pub fn transparent(&self) -> Option<&TransparentAddress> {
self.transparent.as_ref() self.transparent.as_ref()
@ -168,6 +186,24 @@ impl UnifiedAddress {
self.to_address(params.address_network().expect("Unrecognized network")) self.to_address(params.address_network().expect("Unrecognized network"))
.to_string() .to_string()
} }
/// Returns the set of receiver typecodes.
pub fn receiver_types(&self) -> Vec<Typecode> {
let result = std::iter::empty();
#[cfg(feature = "orchard")]
let result = result.chain(self.orchard.map(|_| Typecode::Orchard));
let result = result.chain(self.sapling.map(|_| Typecode::Sapling));
let result = result.chain(self.transparent.map(|taddr| match taddr {
TransparentAddress::PublicKeyHash(_) => Typecode::P2pkh,
TransparentAddress::ScriptHash(_) => Typecode::P2sh,
}));
let result = result.chain(
self.unknown()
.iter()
.map(|(typecode, _)| Typecode::Unknown(*typecode)),
);
result.collect()
}
} }
/// An address that funds can be sent to. /// An address that funds can be sent to.