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`
- `ScannedBlock::orchard`
- `ScannedBlockCommitments::orchard`
- `ORCHARD_SHARD_HEIGHT`
- `BlockMetadata::orchard_tree_size`
- `zcash_client_backend::fees::orchard`
- `zcash_client_backend::fees::ChangeValue::orchard`
- `zcash_client_backend::wallet`:
@ -29,6 +31,10 @@ and this library adheres to Rust's notion of
- Changes to the `WalletRead` trait:
- Added `get_orchard_nullifiers`
- `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`:
- Arguments to `ChangeStrategy::compute_balance` have changed.
@ -59,6 +65,7 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::wallet`:
- `Note`
- `ReceivedNote`
- `Recipient::{map_internal_account, internal_account_transpose_option}`
- `WalletSaplingOutput::recipient_key_scope`
- `TransparentAddressMetadata` (which replaces `zcash_keys::address::AddressMetadata`).
- `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
been removed from `error::Error`.
- New variants have been added:
- `Error::UnsupportedPoolType`
- `Error::UnsupportedChangeType`
- `Error::NoSupportedReceivers`
- `Error::NoSpendingKey`
- `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 `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::create_spend_to_address` now takes additional `change_memo` and
`fallback_change_pool` arguments. 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
@ -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
optional memo to be associated with the change output.
- `ChangeError` has a new `BundleError` variant.
- `fixed::SingleOutputChangeStrategy::new` and
`zip317::SingleOutputChangeStrategy::new` each now accept an additional
`change_memo` argument.
- `fixed::SingleOutputChangeStrategy::new`,
`zip317::SingleOutputChangeStrategy::new`, and
`standard::SingleOutputChangeStrategy::new` each now accept additional
`change_memo` and `fallback_change_pool` arguments.
- `zcash_client_backend::wallet`:
- The fields of `ReceivedSaplingNote` are now private. Use
`ReceivedSaplingNote::from_parts` for construction instead. Accessor methods
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::zip321::TransactionRequest::payments` now returns a
`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::::wallet::input_selection::{Proposal, ShieldedInputs, ProposalError}`
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::ScannedBlock::from_parts` has been made crate-private.
- `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 sapling::{Node, NOTE_COMMITMENT_TREE_DEPTH};
use secrecy::SecretVec;
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
use zcash_primitives::{
@ -51,6 +50,13 @@ pub mod wallet;
/// `lightwalletd` when using the `GetSubtreeRoots` GRPC call.
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
/// belonging to the wallet.
pub enum NullifierQuery {
@ -808,29 +814,37 @@ pub struct SentTransaction<'a> {
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 {
output_index: usize,
recipient: Recipient,
recipient: Recipient<Note>,
value: NonNegativeAmount,
memo: Option<MemoBytes>,
sapling_change_to: Option<(AccountId, sapling::Note)>,
}
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(
output_index: usize,
recipient: Recipient,
recipient: Recipient<Note>,
value: NonNegativeAmount,
memo: Option<MemoBytes>,
sapling_change_to: Option<(AccountId, sapling::Note)>,
) -> Self {
Self {
output_index,
recipient,
value,
memo,
sapling_change_to,
}
}
@ -843,9 +857,9 @@ impl SentTransactionOutput {
pub fn output_index(&self) -> usize {
self.output_index
}
/// Returns the recipient address of the transaction, or the account id for wallet-internal
/// transactions.
pub fn recipient(&self) -> &Recipient {
/// Returns the recipient address of the transaction, or the account id and
/// resulting note for wallet-internal outputs.
pub fn recipient(&self) -> &Recipient<Note> {
&self.recipient
}
/// Returns the value of the newly created output.
@ -857,12 +871,6 @@ impl SentTransactionOutput {
pub fn memo(&self) -> Option<&MemoBytes> {
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
@ -870,7 +878,7 @@ impl SentTransactionOutput {
#[derive(Clone, Debug)]
pub struct AccountBirthday {
height: BlockHeight,
sapling_frontier: Frontier<Node, NOTE_COMMITMENT_TREE_DEPTH>,
sapling_frontier: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
recover_until: Option<BlockHeight>,
}
@ -911,7 +919,7 @@ impl AccountBirthday {
#[cfg(feature = "test-dependencies")]
pub fn from_parts(
height: BlockHeight,
sapling_frontier: Frontier<Node, NOTE_COMMITMENT_TREE_DEPTH>,
sapling_frontier: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
recover_until: Option<BlockHeight>,
) -> Self {
Self {
@ -944,7 +952,9 @@ impl AccountBirthday {
/// Returns the Sapling note commitment tree frontier as of the end of the block at
/// [`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
}
@ -1081,13 +1091,15 @@ pub trait WalletWrite: WalletRead {
/// also provide operations related to Orchard note commitment trees in the future.
pub trait WalletCommitmentTrees {
type Error;
/// The type of the backing [`ShardStore`] for the Sapling note commitment tree.
type SaplingShardStore<'a>: ShardStore<
H = sapling::Node,
CheckpointId = BlockHeight,
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>
where
for<'a> F: FnMut(
@ -1099,12 +1111,48 @@ pub trait WalletCommitmentTrees {
) -> Result<A, E>,
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(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<sapling::Node>],
) -> 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")]
@ -1138,6 +1186,9 @@ pub mod testing {
#[cfg(feature = "transparent-inputs")]
use {crate::wallet::TransparentAddressMetadata, zcash_primitives::legacy::TransparentAddress};
#[cfg(feature = "orchard")]
use super::ORCHARD_SHARD_HEIGHT;
pub struct MockWalletDb {
pub network: Network,
pub sapling_tree: ShardTree<
@ -1145,6 +1196,12 @@ pub mod testing {
{ SAPLING_SHARD_HEIGHT * 2 },
SAPLING_SHARD_HEIGHT,
>,
#[cfg(feature = "orchard")]
pub orchard_tree: ShardTree<
MemoryShardStore<orchard::tree::MerkleHashOrchard, BlockHeight>,
{ ORCHARD_SHARD_HEIGHT * 2 },
ORCHARD_SHARD_HEIGHT,
>,
}
impl MockWalletDb {
@ -1152,6 +1209,8 @@ pub mod testing {
Self {
network,
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(())
}
#[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,
};
use crate::address::UnifiedAddress;
use crate::data_api::wallet::input_selection::InputSelectorError;
use crate::proposal::ProposalError;
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.
MemoForbidden,
/// Attempted to create a spend to an unsupported pool type (currently, Orchard).
UnsupportedPoolType(PoolType),
/// Attempted to send change to an unsupported pool.
///
/// This is indicative of a programming error; execution of a transaction proposal that
/// presumes support for the specified pool was performed using an application that does not
/// provide such support.
UnsupportedChangeType(PoolType),
/// Attempted to create a spend to an unsupported Unified Address receiver
NoSupportedReceivers(Vec<u32>),
NoSupportedReceivers(Box<UnifiedAddress>),
/// A proposed transaction cannot be built because it requires spending an input
/// for which no spending key is available.
@ -140,8 +145,12 @@ where
Error::ScanRequired => write!(f, "Must scan blocks first"),
Error::Builder(e) => write!(f, "An error occurred building the transaction: {}", e),
Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."),
Error::UnsupportedPoolType(t) => write!(f, "Attempted to send to an unsupported pool: {}", t),
Error::NoSupportedReceivers(t) => write!(f, "Unified address contained only unsupported receiver types: {:?}", &t[..]),
Error::UnsupportedChangeType(t) => write!(f, "Attempted to send change to an unsupported pool type: {}", t),
Error::NoSupportedReceivers(ua) => write!(
f,
"A recipient's unified address does not contain any receivers to which the wallet can send funds; required one of {}",
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::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,
fees::{self, DustOutputPolicy},
keys::UnifiedSpendingKey,
proposal::ProposalError,
proposal::{self, Proposal},
proposal::{self, Proposal, ProposalError},
wallet::{Note, OvkPolicy, Recipient},
zip321::{self, Payment},
PoolType, ShieldedProtocol,
@ -44,7 +43,6 @@ use super::InputSource;
#[cfg(feature = "transparent-inputs")]
use {
input_selection::ShieldingSelector,
sapling::keys::OutgoingViewingKey,
std::convert::Infallible,
zcash_keys::encoding::AddressCodec,
zcash_primitives::legacy::TransparentAddress,
@ -211,6 +209,7 @@ pub fn create_spend_to_address<DbT, ParamsT>(
ovk_policy: OvkPolicy,
min_confirmations: NonZeroU32,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Result<
NonEmpty<TxId>,
Error<
@ -241,6 +240,7 @@ where
amount,
memo,
change_memo,
fallback_change_pool,
)?;
create_proposed_transactions(
@ -359,7 +359,7 @@ where
/// 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`].
/// [`create_proposed_transactions`].
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
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.
///
/// 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
/// in the execution of the returned proposal.
///
@ -424,6 +424,8 @@ where
/// * `amount`: The amount to send.
/// * `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.
/// * `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::type_complexity)]
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,
memo: Option<MemoBytes>,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
) -> Result<
Proposal<StandardFeeRule, DbT::NoteRef>,
Error<
@ -462,7 +465,11 @@ where
"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 =
GreedyInputSelector::<DbT, _>::new(change_strategy, DustOutputPolicy::default());
@ -645,72 +652,85 @@ where
.map_err(Error::DataSource)?
.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 external_ovk = match ovk_policy {
OvkPolicy::Sender => Some(dfvk.to_ovk(Scope::External)),
OvkPolicy::Custom(ovk) => Some(ovk),
OvkPolicy::Discard => None,
};
let sapling_inputs = inputs
.notes()
.iter()
.filter_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 internal_ovk = || {
#[cfg(feature = "transparent-inputs")]
return if proposal_step.is_shielding() {
Some(OutgoingViewingKey(
usk.transparent()
.to_account_pubkey()
.internal_ovk()
.as_bytes(),
))
sapling_tree
.witness_at_checkpoint_id_caching(
selected.note_commitment_tree_position(),
&inputs.anchor_height(),
)
.map(|merkle_path| Some((key, note, merkle_path)))
.map_err(Error::from)
.transpose()
}
#[cfg(feature = "orchard")]
Note::Orchard(_) => None,
})
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?;
Ok((Some(anchor), sapling_inputs))
})
},
)?
} else {
Some(dfvk.to_ovk(Scope::Internal))
(None, vec![])
};
#[cfg(not(feature = "transparent-inputs"))]
Some(dfvk.to_ovk(Scope::Internal))
};
#[cfg(feature = "orchard")]
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(
|| Ok((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();
let orchard_inputs = inputs
.notes()
.iter()
.filter_map(|selected| match selected.note() {
#[cfg(feature = "orchard")]
Note::Orchard(note) => orchard_tree
.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
.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");
}
}
Ok((Some(anchor), orchard_inputs))
})
.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
// are no possible transparent inputs, so we ignore those
@ -718,13 +738,18 @@ where
params.clone(),
min_target_height,
BuildConfig::Standard {
sapling_anchor: Some(sapling_anchor),
orchard_anchor: None,
sapling_anchor,
orchard_anchor,
},
);
for (key, note, merkle_path) in sapling_inputs.into_iter() {
builder.add_sapling_spend(&key, note.clone(), merkle_path)?;
for (sapling_key, sapling_note, merkle_path) in sapling_inputs.into_iter() {
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")]
@ -817,9 +842,72 @@ where
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 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 {
Address::Unified(ua) => {
let memo = payment
@ -827,31 +915,56 @@ where
.as_ref()
.map_or_else(MemoBytes::empty, |m| m.clone());
if let Some(sapling_receiver) = ua.sapling() {
builder.add_sapling_output(
external_ovk,
*sapling_receiver,
payment.amount,
memo.clone(),
)?;
sapling_output_meta.push((
Recipient::Unified(
ua.clone(),
PoolType::Shielded(ShieldedProtocol::Sapling),
),
payment.amount,
Some(memo),
));
} else if let Some(taddr) = ua.transparent() {
if payment.memo.is_some() {
return Err(Error::MemoForbidden);
} else {
builder.add_transparent_output(taddr, payment.amount)?;
match output_pool {
#[cfg(not(feature = "orchard"))]
PoolType::Shielded(ShieldedProtocol::Orchard) => {
return Err(Error::ProposalNotSupported);
}
#[cfg(feature = "orchard")]
PoolType::Shielded(ShieldedProtocol::Orchard) => {
builder.add_orchard_output(
orchard_external_ovk.clone(),
*ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction"),
payment.amount.into(),
memo.clone(),
)?;
orchard_output_meta.push((
Recipient::Unified(
ua.clone(),
PoolType::Shielded(ShieldedProtocol::Orchard),
),
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) => {
@ -859,7 +972,12 @@ where
.memo
.as_ref()
.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)));
}
Address::Transparent(to) => {
@ -880,8 +998,8 @@ where
match change_value.output_pool() {
ShieldedProtocol::Sapling => {
builder.add_sapling_output(
internal_ovk(),
dfvk.change_address().1,
sapling_internal_ovk(),
sapling_dfvk.change_address().1,
change_value.value(),
memo.clone(),
)?;
@ -894,15 +1012,29 @@ where
Some(memo),
))
}
#[cfg(zcash_unstable = "orchard")]
ShieldedProtocol::Orchard => {
#[cfg(not(feature = "orchard"))]
return Err(Error::UnsupportedPoolType(PoolType::Shielded(
return Err(Error::UnsupportedChangeType(PoolType::Shielded(
ShieldedProtocol::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
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 =
sapling_output_meta
.into_iter()
@ -921,27 +1085,28 @@ where
.output_index(i)
.expect("An output should exist in the transaction for each Sapling payment.");
let received_as = if let Recipient::InternalAccount(
account,
PoolType::Shielded(ShieldedProtocol::Sapling),
) = recipient
{
build_result
.transaction()
.sapling_bundle()
.and_then(|bundle| {
try_sapling_note_decryption(
&internal_ivk,
&bundle.shielded_outputs()[output_index],
consensus::sapling_zip212_enforcement(params, min_target_height),
)
.map(|(note, _, _)| (account, note))
})
} else {
None
};
let recipient = recipient
.map_internal_account(|pool| {
assert!(pool == PoolType::Shielded(ShieldedProtocol::Sapling));
build_result
.transaction()
.sapling_bundle()
.and_then(|bundle| {
try_sapling_note_decryption(
&sapling_internal_ivk,
&bundle.shielded_outputs()[output_index],
consensus::sapling_zip212_enforcement(
params,
min_target_height,
),
)
.map(|(note, _, _)| Note::Sapling(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, received_as)
SentTransactionOutput::from_parts(output_index, recipient, value, memo)
});
let transparent_outputs = transparent_output_meta.into_iter().map(|(addr, value)| {
@ -958,21 +1123,21 @@ where
.map(|(index, _)| index)
.expect("An output should exist in the transaction for each transparent payment.");
SentTransactionOutput::from_parts(
output_index,
Recipient::Transparent(*addr),
value,
None,
None,
)
SentTransactionOutput::from_parts(output_index, Recipient::Transparent(*addr), value, None)
});
let mut outputs = vec![];
#[cfg(feature = "orchard")]
outputs.extend(orchard_outputs);
outputs.extend(sapling_outputs);
outputs.extend(transparent_outputs);
wallet_db
.store_sent_tx(&SentTransaction {
tx: build_result.transaction(),
created: time::OffsetDateTime::now_utc(),
account,
outputs: sapling_outputs.chain(transparent_outputs).collect(),
outputs,
fee_amount: Amount::from(proposal_step.balance().fee_required()),
#[cfg(feature = "transparent-inputs")]
utxos_spent,

View File

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

View File

@ -34,6 +34,7 @@ pub(crate) fn single_change_output_balance<
dust_output_policy: &DustOutputPolicy,
default_dust_threshold: NonNegativeAmount,
change_memo: Option<MemoBytes>,
_fallback_change_pool: ShieldedProtocol,
) -> Result<TransactionBalance, ChangeError<E, NoteRefT>>
where
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.
#[cfg(feature = "orchard")]
#[allow(clippy::if_same_then_else)]
let (change_pool, sapling_change, orchard_change) =
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
@ -96,9 +96,12 @@ where
// Sapling outputs, so that we avoid pool-crossing.
(ShieldedProtocol::Sapling, 1, 0)
} else {
// For all other transactions, send change to Sapling.
// FIXME: Change this to Orchard once Orchard outputs are enabled.
(ShieldedProtocol::Sapling, 1, 0)
// This is a fully-transparent transaction, so the caller gets to decide
// where to shield change.
match _fallback_change_pool {
ShieldedProtocol::Orchard => (_fallback_change_pool, 0, 1),
ShieldedProtocol::Sapling => (_fallback_change_pool, 1, 0),
}
};
#[cfg(not(feature = "orchard"))]
let (change_pool, sapling_change) = (ShieldedProtocol::Sapling, 1);

View File

@ -9,6 +9,8 @@ use zcash_primitives::{
},
};
use crate::ShieldedProtocol;
use super::{
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
DustOutputPolicy, TransactionBalance,
@ -22,15 +24,24 @@ use super::orchard as orchard_fees;
pub struct SingleOutputChangeStrategy {
fee_rule: FixedFeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
}
impl SingleOutputChangeStrategy {
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified fee rule
/// 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 {
fee_rule,
change_memo,
fallback_change_pool,
}
}
}
@ -65,6 +76,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
dust_output_policy,
self.fee_rule().fixed_fee(),
self.change_memo.clone(),
self.fallback_change_pool,
)
}
}
@ -89,13 +101,15 @@ mod tests {
tests::{TestSaplingInput, TestTransparentInput},
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
},
ShieldedProtocol,
};
#[test]
fn change_without_dust() {
#[allow(deprecated)]
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
let result = change_strategy.compute_balance(
@ -136,7 +150,8 @@ mod tests {
fn dust_change() {
#[allow(deprecated)]
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
let result = change_strategy.compute_balance(

View File

@ -14,6 +14,8 @@ use zcash_primitives::{
},
};
use crate::ShieldedProtocol;
use super::{
fixed, sapling as sapling_fees, zip317, ChangeError, ChangeStrategy, DustOutputPolicy,
TransactionBalance,
@ -27,15 +29,24 @@ use super::orchard as orchard_fees;
pub struct SingleOutputChangeStrategy {
fee_rule: StandardFeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
}
impl SingleOutputChangeStrategy {
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
/// 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 {
fee_rule,
change_memo,
fallback_change_pool,
}
}
}
@ -63,6 +74,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
StandardFeeRule::PreZip313 => fixed::SingleOutputChangeStrategy::new(
FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(10000)),
self.change_memo.clone(),
self.fallback_change_pool,
)
.compute_balance(
params,
@ -78,6 +90,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
StandardFeeRule::Zip313 => fixed::SingleOutputChangeStrategy::new(
FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(1000)),
self.change_memo.clone(),
self.fallback_change_pool,
)
.compute_balance(
params,
@ -93,6 +106,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
StandardFeeRule::Zip317 => zip317::SingleOutputChangeStrategy::new(
Zip317FeeRule::standard(),
self.change_memo.clone(),
self.fallback_change_pool,
)
.compute_balance(
params,

View File

@ -13,6 +13,8 @@ use zcash_primitives::{
},
};
use crate::ShieldedProtocol;
use super::{
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
DustOutputPolicy, TransactionBalance,
@ -26,15 +28,24 @@ use super::orchard as orchard_fees;
pub struct SingleOutputChangeStrategy {
fee_rule: Zip317FeeRule,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedProtocol,
}
impl SingleOutputChangeStrategy {
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
/// 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 {
fee_rule,
change_memo,
fallback_change_pool,
}
}
}
@ -145,6 +156,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
dust_output_policy,
self.fee_rule.marginal_fee(),
self.change_memo.clone(),
self.fallback_change_pool,
)
}
}
@ -170,11 +182,16 @@ mod tests {
tests::{TestSaplingInput, TestTransparentInput},
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
},
ShieldedProtocol,
};
#[test]
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
let result = change_strategy.compute_balance(
@ -213,7 +230,11 @@ mod tests {
#[test]
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
let result = change_strategy.compute_balance(
@ -252,7 +273,11 @@ mod tests {
#[test]
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
let result = change_strategy.compute_balance(
@ -296,7 +321,11 @@ mod tests {
#[test]
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
let result = change_strategy.compute_balance(

View File

@ -97,7 +97,6 @@ pub enum ShieldedProtocol {
/// The Sapling protocol
Sapling,
/// The Orchard protocol
#[cfg(zcash_unstable = "orchard")]
Orchard,
}
@ -118,7 +117,6 @@ impl PoolType {
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();
@ -136,7 +134,6 @@ impl fmt::Display for PoolType {
match self {
PoolType::Transparent => f.write_str("Transparent"),
PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"),
#[cfg(zcash_unstable = "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 {
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> {
@ -501,6 +554,7 @@ impl<NoteRef> Debug for Step<NoteRef> {
"shielded_inputs",
&self.shielded_inputs().map(|i| i.notes.len()),
)
.field("prior_step_inputs", &self.prior_step_inputs)
.field(
"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) {
Ok(proposal::ValuePool::Transparent) => Ok(PoolType::Transparent),
Ok(proposal::ValuePool::Sapling) => Ok(PoolType::Shielded(ShieldedProtocol::Sapling)),
#[cfg(zcash_unstable = "orchard")]
Ok(proposal::ValuePool::Orchard) => Ok(PoolType::Shielded(ShieldedProtocol::Orchard)),
_ => Err(ProposalDecodingError::ValuePoolNotSupported(pool_id)),
}
@ -354,7 +353,6 @@ impl From<ShieldedProtocol> for proposal::ValuePool {
fn from(value: ShieldedProtocol) -> Self {
match value {
ShieldedProtocol::Sapling => proposal::ValuePool::Sapling,
#[cfg(zcash_unstable = "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
/// output.
#[derive(Debug, Clone)]
pub enum Recipient {
pub enum Recipient<N> {
Transparent(TransparentAddress),
Sapling(sapling::PaymentAddress),
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.
@ -380,26 +402,43 @@ impl<NoteRef> orchard_fees::InputView<NoteRef> for ReceivedNote<NoteRef, orchard
/// [ZIP 310]: https://zips.z.cash/zip-0310
#[derive(Debug, Clone)]
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
/// recipients.
///
/// [`ExtendedFullViewingKey`]: sapling::zip32::ExtendedFullViewingKey
/// [`UnifiedFullViewingKey`]: zcash_keys::keys::UnifiedFullViewingKey
Sender,
/// Use a custom outgoing viewing key. This might for instance be derived from a
/// separate seed than the wallet's spending keys.
/// Use custom outgoing viewing keys. These might for instance be derived from a
/// different seed than the wallet's spending keys.
///
/// Transaction outputs will be decryptable by the recipients, and whoever controls
/// the provided outgoing viewing key.
Custom(sapling::keys::OutgoingViewingKey),
/// Use no outgoing viewing key. Transaction outputs will be decryptable by their
/// the provided outgoing viewing keys.
Custom {
sapling: sapling::keys::OutgoingViewingKey,
#[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.
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.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg(feature = "transparent-inputs")]

View File

@ -44,7 +44,7 @@ jubjub.workspace = true
secrecy.workspace = true
# - Shielded protocols
orchard.workspace = true
orchard = { workspace = true, optional = true }
sapling.workspace = true
# - 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
## 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.
test-dependencies = [

View File

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

View File

@ -69,11 +69,14 @@ use zcash_client_backend::{
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock,
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
DecryptedOutput, ShieldedProtocol, TransferType,
};
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
#[cfg(feature = "orchard")]
use zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT;
#[cfg(feature = "transparent-inputs")]
use {
zcash_client_backend::wallet::TransparentAddressMetadata,
@ -180,10 +183,18 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
fn get_spendable_note(
&self,
txid: &TxId,
_protocol: ShieldedProtocol,
protocol: ShieldedProtocol,
index: u32,
) -> 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(
@ -588,12 +599,13 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
for output in d_tx.sapling_outputs {
match output.transfer_type {
TransferType::Outgoing | TransferType::WalletInternal => {
let value = output.note.value();
let recipient = if output.transfer_type == TransferType::Outgoing {
Recipient::Sapling(output.note.recipient())
} else {
Recipient::InternalAccount(
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,
output.index,
&recipient,
NonNegativeAmount::from_u64(output.note.value().inner()).map_err(|_| {
NonNegativeAmount::from_u64(value.inner()).map_err(|_| {
SqliteClientError::CorruptedData(
"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,
)?;
if let Some((account, note)) = output.sapling_change_to() {
wallet::sapling::put_received_note(
wdb.conn.0,
&DecryptedOutput {
index: output.output_index(),
note: note.clone(),
account: *account,
memo: output
.memo()
.map_or_else(MemoBytes::empty, |memo| memo.clone()),
transfer_type: TransferType::WalletInternal,
},
tx_ref,
None,
)?;
match output.recipient() {
Recipient::InternalAccount(account, Note::Sapling(note)) => {
wallet::sapling::put_received_note(
wdb.conn.0,
&DecryptedOutput {
index: output.output_index(),
note: note.clone(),
account: *account,
memo: output
.memo()
.map_or_else(MemoBytes::empty, |memo| memo.clone()),
transfer_type: TransferType::WalletInternal,
},
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)))?;
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> {
@ -845,6 +895,37 @@ impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTran
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.

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,8 @@
//! Structs for handling supported address types.
use std::convert::TryFrom;
use sapling::PaymentAddress;
use zcash_address::{
unified::{self, Container, Encoding},
unified::{self, Container, Encoding, Typecode},
ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress,
};
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.
#[cfg(feature = "orchard")]
pub fn orchard(&self) -> Option<&orchard::Address> {
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.
pub fn sapling(&self) -> Option<&PaymentAddress> {
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.
pub fn transparent(&self) -> Option<&TransparentAddress> {
self.transparent.as_ref()
@ -168,6 +186,24 @@ impl UnifiedAddress {
self.to_address(params.address_network().expect("Unrecognized network"))
.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.