Skip to content

Commit

Permalink
zcash_client_backend: Add support for creation of Orchard outputs.
Browse files Browse the repository at this point in the history
  • Loading branch information
nuttycom committed Jan 16, 2024
1 parent 9f38413 commit a3cc2df
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 73 deletions.
12 changes: 6 additions & 6 deletions zcash_client_backend/src/data_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ pub struct SentTransactionOutput {
recipient: Recipient,
value: NonNegativeAmount,
memo: Option<MemoBytes>,
sapling_change_to: Option<(AccountId, sapling::Note)>,
change_to: Option<(AccountId, Note)>,
}

impl SentTransactionOutput {
Expand All @@ -813,14 +813,14 @@ impl SentTransactionOutput {
recipient: Recipient,
value: NonNegativeAmount,
memo: Option<MemoBytes>,
sapling_change_to: Option<(AccountId, sapling::Note)>,
change_to: Option<(AccountId, Note)>,
) -> Self {
Self {
output_index,
recipient,
value,
memo,
sapling_change_to,
change_to,
}
}

Expand Down Expand Up @@ -849,9 +849,9 @@ impl SentTransactionOutput {
}

/// 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()
/// transaction) was sent, along with the change or shielding note.
pub fn change_to(&self) -> Option<&(AccountId, Note)> {
self.change_to.as_ref()
}
}

Expand Down
12 changes: 9 additions & 3 deletions zcash_client_backend/src/data_api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use zcash_primitives::{
zip32::AccountId,
};

use crate::address::UnifiedAddress;
use crate::data_api::wallet::input_selection::InputSelectorError;
use crate::PoolType;

Expand Down Expand Up @@ -58,8 +59,12 @@ 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),

/// The Unified address provided as a transaction recipient did not contain any receiver types
/// to which the wallet knows how to send funds.
NoSupportedReceivers(Box<UnifiedAddress>),

/// A note being spent does not correspond to either the internal or external
/// full viewing key for an account.
Expand Down Expand Up @@ -117,7 +122,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 create spend to an unsupported pool type: {}", 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 not contain any receivers to which the wallet can send funds."),
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),

#[cfg(feature = "transparent-inputs")]
Expand Down
198 changes: 155 additions & 43 deletions zcash_client_backend/src/data_api/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ use super::InputSource;
#[cfg(feature = "transparent-inputs")]
use {
crate::wallet::WalletTransparentOutput, input_selection::ShieldingSelector,
sapling::keys::OutgoingViewingKey, std::convert::Infallible,
zcash_primitives::legacy::TransparentAddress,
std::convert::Infallible, zcash_primitives::legacy::TransparentAddress,
};

/// Scans a [`Transaction`] for any information that can be decrypted by the accounts in
Expand Down Expand Up @@ -547,32 +546,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.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.shielded_inputs().map_or_else(
|| Ok((sapling::Anchor::empty_tree(), vec![])),
|inputs| {
Expand Down Expand Up @@ -695,6 +668,62 @@ where
utxos
};

#[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.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.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.transaction_request().payments() {
Expand All @@ -705,9 +734,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(),
Expand All @@ -720,24 +768,33 @@ 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)?;
}
} else {
return Err(Error::UnsupportedPoolType(PoolType::Shielded(
ShieldedProtocol::Orchard,
)));

continue;
}

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) => {
Expand All @@ -758,8 +815,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(),
)?;
Expand All @@ -774,20 +831,69 @@ where
}
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),
))
}
}
}
}

// Build the transaction with the specified fee rule
let build_result = builder.build(OsRng, spend_prover, output_prover, proposal.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 received_as = if let Recipient::InternalAccount(
account,
PoolType::Shielded(ShieldedProtocol::Orchard),
) = recipient
{
build_result
.transaction()
.orchard_bundle()
.and_then(|bundle| {
bundle
.decrypt_output_with_key(output_index, &orchard_internal_ivk)
.map(|(note, _, _)| (account, Note::Orchard(note)))
})
} else {
None
};

SentTransactionOutput::from_parts(output_index, recipient, value, memo, received_as)
});

let sapling_internal_ivk =
PreparedIncomingViewingKey::new(&sapling_dfvk.to_ivk(Scope::Internal));
let sapling_outputs =
sapling_output_meta
.into_iter()
Expand All @@ -808,14 +914,14 @@ where
.sapling_bundle()
.and_then(|bundle| {
try_sapling_note_decryption(
&internal_ivk,
&sapling_internal_ivk,
&bundle.shielded_outputs()[output_index],
consensus::sapling_zip212_enforcement(
params,
proposal.min_target_height(),
),
)
.map(|(note, _, _)| (account, note))
.map(|(note, _, _)| (account, Note::Sapling(note)))
})
} else {
None
Expand Down Expand Up @@ -847,12 +953,18 @@ where
)
});

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.balance().fee_required()),
#[cfg(feature = "transparent-inputs")]
utxos_spent: utxos.iter().map(|utxo| utxo.outpoint().clone()).collect(),
Expand Down
5 changes: 2 additions & 3 deletions zcash_client_backend/src/fees/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion zcash_client_backend/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,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.
Expand Down
Loading

0 comments on commit a3cc2df

Please sign in to comment.