zcash_client_backend: Add support for creation of Orchard outputs.

This commit is contained in:
Kris Nuttycombe 2024-01-09 10:50:17 -07:00
parent f27f601b7d
commit daf88a12e5
9 changed files with 263 additions and 116 deletions

View File

@ -65,6 +65,7 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::wallet`:
- `Note`
- `ReceivedNote`
- `Recipient::{map, transpose}`
- `WalletSaplingOutput::recipient_key_scope`
- `TransparentAddressMetadata` (which replaces `zcash_keys::address::AddressMetadata`).
- `impl {Debug, Clone} for OvkPolicy`
@ -127,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`
@ -232,6 +233,11 @@ and this library adheres to Rust's notion of
- 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` now wraps a bare `[u8; 32]` instead of a Sapling `OutgoingViewingKey`.
- `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
@ -273,6 +279,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

@ -818,26 +818,32 @@ pub struct SentTransaction<'a> {
/// A type that represents an output (either Sapling or transparent) that was sent by the wallet.
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,
}
}
@ -850,9 +856,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.
@ -864,12 +870,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

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,11 @@ 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 create a send change to an unsupported pool.
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 +141,8 @@ 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(_) => write!(f, "A recipient's unified address does not contain any receivers to which the wallet can send funds."),
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

@ -44,7 +44,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,
@ -645,32 +644,6 @@ where
.map_err(Error::DataSource)?
.ok_or(Error::KeyNotRecognized)?;
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
// 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 internal_ovk = || {
#[cfg(feature = "transparent-inputs")]
return if proposal_step.is_shielding() {
Some(OutgoingViewingKey(
usk.transparent()
.to_account_pubkey()
.internal_ovk()
.as_bytes(),
))
} else {
Some(dfvk.to_ovk(Scope::Internal))
};
#[cfg(not(feature = "transparent-inputs"))]
Some(dfvk.to_ovk(Scope::Internal))
};
let (sapling_anchor, sapling_inputs) = proposal_step.shielded_inputs().map_or_else(
|| Ok((sapling::Anchor::empty_tree(), vec![])),
|inputs| {
@ -851,6 +824,62 @@ 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(ovk) => Some(orchard::keys::OutgoingViewingKey::from(ovk)),
OvkPolicy::Discard => None,
};
#[cfg(feature = "orchard")]
let orchard_internal_ovk = || {
#[cfg(feature = "transparent-inputs")]
return if proposal_step.is_shielding() {
Some(orchard::keys::OutgoingViewingKey::from(
usk.transparent()
.to_account_pubkey()
.internal_ovk()
.as_bytes(),
))
} else {
Some(orchard_fvk.to_ovk(orchard::keys::Scope::Internal))
};
#[cfg(not(feature = "transparent-inputs"))]
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(ovk) => Some(sapling::keys::OutgoingViewingKey(ovk)),
OvkPolicy::Discard => None,
};
let sapling_internal_ovk = || {
#[cfg(feature = "transparent-inputs")]
return if proposal_step.is_shielding() {
Some(sapling::keys::OutgoingViewingKey(
usk.transparent()
.to_account_pubkey()
.internal_ovk()
.as_bytes(),
))
} else {
Some(sapling_dfvk.to_ovk(Scope::Internal))
};
#[cfg(not(feature = "transparent-inputs"))]
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() {
@ -861,9 +890,28 @@ where
.as_ref()
.map_or_else(MemoBytes::empty, |m| m.clone());
#[cfg(feature = "orchard")]
if let Some(orchard_receiver) = ua.orchard() {
builder.add_orchard_output(
orchard_external_ovk.clone(),
*orchard_receiver,
payment.amount.into(),
memo.clone(),
)?;
orchard_output_meta.push((
Recipient::Unified(
ua.clone(),
PoolType::Shielded(ShieldedProtocol::Orchard),
),
payment.amount,
Some(memo),
));
continue;
}
if let Some(sapling_receiver) = ua.sapling() {
builder.add_sapling_output(
external_ovk,
sapling_external_ovk,
*sapling_receiver,
payment.amount,
memo.clone(),
@ -876,24 +924,32 @@ where
payment.amount,
Some(memo),
));
} else if let Some(taddr) = ua.transparent() {
continue;
}
if let Some(taddr) = ua.transparent() {
if payment.memo.is_some() {
return Err(Error::MemoForbidden);
} else {
builder.add_transparent_output(taddr, payment.amount)?;
continue;
}
} else {
return Err(Error::NoSupportedReceivers(
ua.unknown().iter().map(|(tc, _)| *tc).collect(),
));
}
return Err(Error::NoSupportedReceivers(Box::new(ua.clone())));
}
Address::Sapling(addr) => {
let memo = payment
.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) => {
@ -914,8 +970,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(),
)?;
@ -931,12 +987,27 @@ where
#[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),
))
}
}
}
}
@ -944,7 +1015,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(|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))
})
})
.transpose()
.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()
@ -955,27 +1058,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(|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))
})
})
.transpose()
.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)| {
@ -992,21 +1096,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

@ -96,9 +96,8 @@ 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)
// For all other transactions, send change to Orchard.
(ShieldedProtocol::Orchard, 0, 1)
};
#[cfg(not(feature = "orchard"))]
let (change_pool, sapling_change) = (ShieldedProtocol::Sapling, 1);

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<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 transpose(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.
@ -393,7 +415,7 @@ pub enum OvkPolicy {
///
/// Transaction outputs will be decryptable by the recipients, and whoever controls
/// the provided outgoing viewing key.
Custom(sapling::keys::OutgoingViewingKey),
Custom([u8; 32]),
/// Use no outgoing viewing key. Transaction outputs will be decryptable by their
/// recipients, but not by the sender.

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

@ -69,7 +69,7 @@ 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};
@ -591,12 +591,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()),
)
};
@ -607,7 +608,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(),
)
@ -715,21 +716,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!();
}
_ => (),
}
}

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;
@ -1796,7 +1797,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 +1807,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 +1867,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> {