zcash_client_backend: Add Orchard support to transaction proposals.

This commit is contained in:
Kris Nuttycombe 2023-12-04 14:45:22 -07:00
parent 56f2ac573c
commit adc75566a0
13 changed files with 393 additions and 247 deletions

1
Cargo.lock generated
View File

@ -3056,6 +3056,7 @@ dependencies = [
"incrementalmerkletree",
"jubjub",
"maybe-rayon",
"orchard",
"proptest",
"prost",
"rand_core",

View File

@ -21,9 +21,10 @@ and this library adheres to Rust's notion of
with_orchard_balance_mut,
add_unshielded_value
}`
- `WalletRead::get_orchard_nullifiers`
- `wallet::propose_standard_transfer_to_address`
- `wallet::input_selection::Proposal::from_parts`
- `wallet::input_selection::SaplingInputs`
- `wallet::input_selection::Proposal::{from_parts, shielded_inputs}`
- `wallet::input_selection::ShieldedInputs`
- `wallet::input_selection::ShieldingSelector` has been
factored out from the `InputSelector` trait to separate out transparent
functionality and move it behind the `transparent-inputs` feature flag.
@ -134,8 +135,7 @@ and this library adheres to Rust's notion of
- `wallet::create_proposed_transaction` now forces implementations to ignore
the database identifiers for its contained notes by universally quantifying
the `NoteRef` type parameter.
- `wallet::input_selection::Proposal::sapling_inputs` now returns type
`Option<&SaplingInputs>`.
- Arguments to `wallet::input_selection::Proposal::from_parts` have changed.
- `wallet::input_selection::Proposal::min_anchor_height` has been removed in
favor of storing this value in `SaplingInputs`.
- `wallet::input_selection::GreedyInputSelector` now has relaxed requirements
@ -146,9 +146,8 @@ and this library adheres to Rust's notion of
- `ChangeValue` is now a struct. In addition to the existing change value, it
now also provides the output pool to which change should be sent and an
optional memo to be associated with the change output.
- `fixed::SingleOutputChangeStrategy::new` and
`zip317::SingleOutputChangeStrategy::new` each now accept an additional
`change_memo` argument.
- `fixed::SingleOutputChangeStrategy::new` and `zip317::SingleOutputChangeStrategy::new`
each now accept an additional `change_memo` argument
- `zcash_client_backend::wallet`:
- The fields of `ReceivedSaplingNote` are now private. Use
`ReceivedSaplingNote::from_parts` for construction instead. Accessor methods
@ -200,9 +199,11 @@ and this library adheres to Rust's notion of
### Removed
- `zcash_client_backend::wallet::ReceivedSaplingNote` has been replaced by
`zcash_client_backend::ReceivedNote`.
- `zcash_client_backend::data_api::WalletRead::is_valid_account_extfvk` has been
removed; it was unused in the ECC mobile wallet SDKs and has been superseded by
`get_account_for_ufvk`.
- `zcash_client_backend::data_api`
- `WalletRead::is_valid_account_extfvk` has been removed; it was unused in
the ECC mobile wallet SDKs and has been superseded by `get_account_for_ufvk`.
- `wallet::input_selection::Proposal::sapling_inputs` has been replaced
by `Proposal::shielded_inputs`
- `zcash_client_backend::data_api::WalletRead::{
get_spendable_sapling_notes
select_spendable_sapling_notes,

View File

@ -3,6 +3,12 @@
This library contains Rust structs and traits for creating shielded Zcash light
clients.
## Building
Note that in order to (re)build the GRPC interface, you will need `protoc` on
your `$PATH`. This is not required unless you make changes to any of the files
in `./proto/`.
## License
Licensed under either of

View File

@ -11,11 +11,13 @@ message Proposal {
uint32 protoVersion = 1;
// ZIP 321 serialized transaction request
string transactionRequest = 2;
// The transparent UTXOs to use as inputs to the transaction.
repeated ProposedInput transparentInputs = 3;
// The Sapling input notes and anchor height to be used in creating the transaction.
SaplingInputs saplingInputs = 4;
// The total value, fee amount, and change outputs of the proposed
// The anchor height to be used in creating the transaction, if any.
// Setting the anchor height to zero will disallow the use of any shielded
// inputs.
uint32 anchorHeight = 3;
// The inputs to be used in creating the transaction.
repeated ProposedInput inputs = 4;
// The total value, fee value, and change outputs of the proposed
// transaction
TransactionBalance balance = 5;
// The fee rule used in constructing this proposal
@ -30,18 +32,26 @@ message Proposal {
bool isShielding = 8;
}
message SaplingInputs {
// The Sapling anchor height to be used in creating the transaction
uint32 anchorHeight = 1;
// The unique identifier and amount for each proposed Sapling input
repeated ProposedInput inputs = 2;
enum ValuePool {
// Protobuf requires that enums have a zero discriminant as the default
// value. However, we need to require that a known value pool is selected,
// and we do not want to fall back to any default, so sending the
// PoolNotSpecified value will be treated as an error.
PoolNotSpecified = 0;
// The transparent value pool (P2SH is not distinguished from P2PKH)
Transparent = 1;
// The Sapling value pool
Sapling = 2;
// The Orchard value pool
Orchard = 3;
}
// The unique identifier and amount for each proposed input.
// The unique identifier and value for each proposed input.
message ProposedInput {
bytes txid = 1;
uint32 index = 2;
uint64 value = 3;
ValuePool valuePool = 2;
uint32 index = 3;
uint64 value = 4;
}
// The fee rule used in constructing a Proposal
@ -59,17 +69,18 @@ enum FeeRule {
Zip317 = 3;
}
// The proposed change outputs and fee amount.
// The proposed change outputs and fee value.
message TransactionBalance {
repeated ChangeValue proposedChange = 1;
uint64 feeRequired = 2;
}
// An enumeration of change value types.
// A proposed change output. If the transparent value pool is selected,
// the `memo` field must be null.
message ChangeValue {
oneof value {
SaplingChange saplingValue = 1;
}
uint64 value = 1;
ValuePool valuePool = 2;
MemoBytes memo = 3;
}
// An object wrapper for memo bytes, to facilitate representing the
@ -77,10 +88,3 @@ message ChangeValue {
message MemoBytes {
bytes value = 1;
}
// The amount and memo for a proposed Sapling change output.
message SaplingChange {
uint64 amount = 1;
MemoBytes memo = 2;
}

View File

@ -28,7 +28,7 @@ use crate::{
decrypt::DecryptedOutput,
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::service::TreeState,
wallet::{NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput, WalletTx},
ShieldedProtocol,
};
@ -360,7 +360,7 @@ pub trait InputSource {
txid: &TxId,
protocol: ShieldedProtocol,
index: u32,
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error>;
) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error>;
/// Returns a list of spendable notes sufficient to cover the specified target value, if
/// possible. Only spendable notes corresponding to the specified shielded protocol will
@ -372,7 +372,7 @@ pub trait InputSource {
sources: &[ShieldedProtocol],
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error>;
) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, Self::Error>;
/// Fetches a spendable transparent output.
///
@ -526,14 +526,23 @@ pub trait WalletRead {
/// Returns a transaction.
fn get_transaction(&self, txid: TxId) -> Result<Transaction, Self::Error>;
/// Returns the nullifiers for notes that the wallet is tracking, along with their associated
/// account IDs, that are either unspent or have not yet been confirmed as spent (in that a
/// spending transaction known to the wallet has not yet been included in a block).
/// Returns the nullifiers for Sapling notes that the wallet is tracking, along with their
/// associated account IDs, that are either unspent or have not yet been confirmed as spent (in
/// that a spending transaction known to the wallet has not yet been included in a block).
fn get_sapling_nullifiers(
&self,
query: NullifierQuery,
) -> Result<Vec<(AccountId, sapling::Nullifier)>, Self::Error>;
/// Returns the nullifiers for Orchard notes that the wallet is tracking, along with their
/// associated account IDs, that are either unspent or have not yet been confirmed as spent (in
/// that a spending transaction known to the wallet has not yet been included in a block).
#[cfg(feature = "orchard")]
fn get_orchard_nullifiers(
&self,
query: NullifierQuery,
) -> Result<Vec<(AccountId, orchard::note::Nullifier)>, Self::Error>;
/// Returns the set of all transparent receivers associated with the given account.
///
/// The set contains all transparent receivers that are known to have been derived
@ -1096,7 +1105,7 @@ pub mod testing {
use crate::{
address::{AddressMetadata, UnifiedAddress},
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
wallet::{NoteId, ReceivedNote, WalletTransparentOutput},
wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput},
ShieldedProtocol,
};
@ -1133,7 +1142,7 @@ pub mod testing {
_txid: &TxId,
_protocol: ShieldedProtocol,
_index: u32,
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error> {
) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
Ok(None)
}
@ -1144,7 +1153,7 @@ pub mod testing {
_sources: &[ShieldedProtocol],
_anchor_height: BlockHeight,
_exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error> {
) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
Ok(Vec::new())
}
}
@ -1251,6 +1260,14 @@ pub mod testing {
Ok(Vec::new())
}
#[cfg(feature = "orchard")]
fn get_orchard_nullifiers(
&self,
_query: NullifierQuery,
) -> Result<Vec<(AccountId, orchard::note::Nullifier)>, Self::Error> {
Ok(Vec::new())
}
fn get_transparent_receivers(
&self,
_account: AccountId,

View File

@ -573,7 +573,7 @@ where
Some(dfvk.to_ovk(Scope::Internal))
};
let (sapling_anchor, sapling_inputs) = proposal.sapling_inputs().map_or_else(
let (sapling_anchor, sapling_inputs) = proposal.shielded_inputs().map_or_else(
|| Ok((sapling::Anchor::empty_tree(), vec![])),
|inputs| {
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {

View File

@ -21,16 +21,19 @@ use crate::{
address::{Address, UnifiedAddress},
data_api::InputSource,
fees::{sapling, ChangeError, ChangeStrategy, DustOutputPolicy, TransactionBalance},
wallet::{ReceivedNote, WalletTransparentOutput},
wallet::{Note, ReceivedNote, WalletTransparentOutput},
zip321::TransactionRequest,
ShieldedProtocol,
};
#[cfg(any(feature = "transparent-inputs", feature = "orchard"))]
use std::convert::Infallible;
#[cfg(feature = "transparent-inputs")]
use {std::collections::BTreeSet, zcash_primitives::transaction::components::OutPoint};
#[cfg(feature = "orchard")]
use {crate::fees::orchard as orchard_fees, std::convert::Infallible};
use crate::fees::orchard as orchard_fees;
/// The type of errors that may be produced in input selection.
pub enum InputSelectorError<DbErrT, SelectorErrT> {
@ -83,7 +86,7 @@ impl<DE: fmt::Display, SE: fmt::Display> fmt::Display for InputSelectorError<DE,
pub struct Proposal<FeeRuleT, NoteRef> {
transaction_request: TransactionRequest,
transparent_inputs: Vec<WalletTransparentOutput>,
sapling_inputs: Option<SaplingInputs<NoteRef>>,
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
balance: TransactionBalance,
fee_rule: FeeRuleT,
min_target_height: BlockHeight,
@ -151,7 +154,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
pub fn from_parts(
transaction_request: TransactionRequest,
transparent_inputs: Vec<WalletTransparentOutput>,
sapling_inputs: Option<SaplingInputs<NoteRef>>,
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
balance: TransactionBalance,
fee_rule: FeeRuleT,
min_target_height: BlockHeight,
@ -163,14 +166,14 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
(acc? + a).ok_or(ProposalError::Overflow)
})?;
let sapling_input_total = sapling_inputs
let shielded_input_total = shielded_inputs
.iter()
.flat_map(|s_in| s_in.notes().iter())
.map(|out| out.value())
.map(|out| out.note().value())
.fold(Some(NonNegativeAmount::ZERO), |acc, a| (acc? + a))
.ok_or(ProposalError::Overflow)?;
let input_total =
(transparent_input_total + sapling_input_total).ok_or(ProposalError::Overflow)?;
(transparent_input_total + shielded_input_total).ok_or(ProposalError::Overflow)?;
let request_total = transaction_request
.total()
@ -179,7 +182,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
if is_shielding
&& (transparent_input_total == NonNegativeAmount::ZERO
|| sapling_input_total > NonNegativeAmount::ZERO
|| shielded_input_total > NonNegativeAmount::ZERO
|| request_total > NonNegativeAmount::ZERO)
{
return Err(ProposalError::ShieldingInvalid);
@ -189,7 +192,7 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
Ok(Self {
transaction_request,
transparent_inputs,
sapling_inputs,
shielded_inputs,
balance,
fee_rule,
min_target_height,
@ -212,8 +215,8 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
&self.transparent_inputs
}
/// Returns the Sapling inputs that have been selected to fund the transaction.
pub fn sapling_inputs(&self) -> Option<&SaplingInputs<NoteRef>> {
self.sapling_inputs.as_ref()
pub fn shielded_inputs(&self) -> Option<&ShieldedInputs<NoteRef>> {
self.shielded_inputs.as_ref()
}
/// Returns the change outputs to be added to the transaction and the fee to be paid.
pub fn balance(&self) -> &TransactionBalance {
@ -244,12 +247,12 @@ impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
.field("transaction_request", &self.transaction_request)
.field("transparent_inputs", &self.transparent_inputs)
.field(
"sapling_inputs",
&self.sapling_inputs().map(|i| i.notes.len()),
"shielded_inputs",
&self.shielded_inputs().map(|i| i.notes.len()),
)
.field(
"sapling_anchor_height",
&self.sapling_inputs().map(|i| i.anchor_height),
"anchor_height",
&self.shielded_inputs().map(|i| i.anchor_height),
)
.field("balance", &self.balance)
//.field("fee_rule", &self.fee_rule)
@ -261,14 +264,17 @@ impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
/// The Sapling inputs to a proposed transaction.
#[derive(Clone, PartialEq, Eq)]
pub struct SaplingInputs<NoteRef> {
pub struct ShieldedInputs<NoteRef> {
anchor_height: BlockHeight,
notes: NonEmpty<ReceivedNote<NoteRef>>,
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
}
impl<NoteRef> SaplingInputs<NoteRef> {
/// Constructs a [`SaplingInputs`] from its constituent parts.
pub fn from_parts(anchor_height: BlockHeight, notes: NonEmpty<ReceivedNote<NoteRef>>) -> Self {
impl<NoteRef> ShieldedInputs<NoteRef> {
/// Constructs a [`ShieldedInputs`] from its constituent parts.
pub fn from_parts(
anchor_height: BlockHeight,
notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
) -> Self {
Self {
anchor_height,
notes,
@ -282,7 +288,7 @@ impl<NoteRef> SaplingInputs<NoteRef> {
}
/// Returns the list of Sapling notes to be used as inputs to the proposed transaction.
pub fn notes(&self) -> &NonEmpty<ReceivedNote<NoteRef>> {
pub fn notes(&self) -> &NonEmpty<ReceivedNote<NoteRef, Note>> {
&self.notes
}
}
@ -559,7 +565,7 @@ where
}
}
let mut sapling_inputs: Vec<ReceivedNote<DbT::NoteRef>> = vec![];
let mut shielded_inputs: Vec<ReceivedNote<DbT::NoteRef, Note>> = vec![];
let mut prior_available = NonNegativeAmount::ZERO;
let mut amount_required = NonNegativeAmount::ZERO;
let mut exclude: Vec<DbT::NoteRef> = vec![];
@ -574,13 +580,30 @@ where
&transparent_outputs,
&(
::sapling::builder::BundleType::DEFAULT,
&sapling_inputs[..],
&shielded_inputs
.iter()
.filter_map(|i| {
i.clone().traverse_opt(|wn| match wn {
Note::Sapling(n) => Some(n),
#[cfg(feature = "orchard")]
_ => None,
})
})
.collect::<Vec<_>>()[..],
&sapling_outputs[..],
),
#[cfg(feature = "orchard")]
&(
::orchard::builder::BundleType::DEFAULT,
&Vec::<Infallible>::new()[..],
&shielded_inputs
.iter()
.filter_map(|i| {
i.clone().traverse_opt(|wn| match wn {
Note::Orchard(n) => Some(n),
_ => None,
})
})
.collect::<Vec<_>>()[..],
&orchard_outputs[..],
),
&self.dust_output_policy,
@ -591,8 +614,8 @@ where
return Ok(Proposal {
transaction_request,
transparent_inputs: vec![],
sapling_inputs: NonEmpty::from_vec(sapling_inputs).map(|notes| {
SaplingInputs {
shielded_inputs: NonEmpty::from_vec(shielded_inputs).map(|notes| {
ShieldedInputs {
anchor_height,
notes,
}
@ -612,19 +635,19 @@ where
Err(other) => return Err(other.into()),
}
sapling_inputs = wallet_db
shielded_inputs = wallet_db
.select_spendable_notes(
account,
amount_required.into(),
&[ShieldedProtocol::Sapling],
&[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard],
anchor_height,
&exclude,
)
.map_err(InputSelectorError::DataSource)?;
let new_available = sapling_inputs
let new_available = shielded_inputs
.iter()
.map(|n| n.value())
.map(|n| n.note().value())
.sum::<Option<NonNegativeAmount>>()
.ok_or(BalanceError::Overflow)?;
@ -737,7 +760,7 @@ where
Ok(Proposal {
transaction_request: TransactionRequest::empty(),
transparent_inputs,
sapling_inputs: None,
shielded_inputs: None,
balance,
fee_rule: (*self.change_strategy.fee_rule()).clone(),
min_target_height: target_height,

View File

@ -22,7 +22,7 @@ use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
use crate::{
data_api::{
wallet::input_selection::{Proposal, ProposalError, SaplingInputs},
wallet::input_selection::{Proposal, ProposalError, ShieldedInputs},
InputSource,
},
fees::{ChangeValue, TransactionBalance},
@ -221,6 +221,8 @@ pub enum ProposalDecodingError<DbError> {
Zip321(Zip321Error),
/// A transaction identifier string did not decode to a valid transaction ID.
TxIdInvalid(TryFromSliceError),
/// An invalid value pool identifier was encountered.
ValuePoolInvalid(i32),
/// A failure occurred trying to retrieve an unspent note or UTXO from the wallet database.
InputRetrieval(DbError),
/// The unspent note or UTXO corresponding to a proposal input was not found in the wallet
@ -238,6 +240,8 @@ pub enum ProposalDecodingError<DbError> {
ProposalInvalid(ProposalError),
/// An inputs field for the given protocol was present, but contained no input note references.
EmptyShieldedInputs(ShieldedProtocol),
/// Change outputs to the specified pool are not supported.
InvalidChangeRecipient(PoolType),
}
impl<E> From<Zip321Error> for ProposalDecodingError<E> {
@ -253,6 +257,9 @@ impl<E: Display> Display for ProposalDecodingError<E> {
ProposalDecodingError::TxIdInvalid(err) => {
write!(f, "Invalid transaction id: {:?}", err)
}
ProposalDecodingError::ValuePoolInvalid(id) => {
write!(f, "Invalid value pool identifier: {:?}", id)
}
ProposalDecodingError::InputRetrieval(err) => write!(
f,
"An error occurred retrieving a transaction input: {}",
@ -281,6 +288,11 @@ impl<E: Display> Display for ProposalDecodingError<E> {
"An inputs field was present for {:?}, but contained no note references.",
protocol
),
ProposalDecodingError::InvalidChangeRecipient(pool_type) => write!(
f,
"Change outputs to the {} pool are not supported.",
pool_type
),
}
}
}
@ -296,10 +308,38 @@ impl<E: std::error::Error + 'static> std::error::Error for ProposalDecodingError
}
}
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)),
Ok(proposal::ValuePool::Orchard) => Ok(PoolType::Shielded(ShieldedProtocol::Orchard)),
_ => Err(ProposalDecodingError::ValuePoolInvalid(pool_id)),
}
}
impl proposal::ProposedInput {
pub fn parse_txid(&self) -> Result<TxId, TryFromSliceError> {
Ok(TxId::from_bytes(self.txid[..].try_into()?))
}
pub fn pool_type<T>(&self) -> Result<PoolType, ProposalDecodingError<T>> {
pool_type(self.value_pool)
}
}
impl proposal::ChangeValue {
pub fn pool_type<T>(&self) -> Result<PoolType, ProposalDecodingError<T>> {
pool_type(self.value_pool)
}
}
impl From<ShieldedProtocol> for proposal::ValuePool {
fn from(value: ShieldedProtocol) -> Self {
match value {
ShieldedProtocol::Sapling => proposal::ValuePool::Sapling,
ShieldedProtocol::Orchard => proposal::ValuePool::Orchard,
}
}
}
impl proposal::Proposal {
@ -311,50 +351,40 @@ impl proposal::Proposal {
) -> Option<Self> {
let transaction_request = value.transaction_request().to_uri(params)?;
let transparent_inputs = value
let anchor_height = value
.shielded_inputs()
.map_or_else(|| 0, |i| u32::from(i.anchor_height()));
let inputs = value
.transparent_inputs()
.iter()
.map(|utxo| proposal::ProposedInput {
txid: utxo.outpoint().hash().to_vec(),
value_pool: proposal::ValuePool::Transparent.into(),
index: utxo.outpoint().n(),
value: utxo.txout().value.into(),
})
.chain(value.shielded_inputs().iter().flat_map(|s_in| {
s_in.notes().iter().map(|rec_note| proposal::ProposedInput {
txid: rec_note.txid().as_ref().to_vec(),
value_pool: proposal::ValuePool::from(rec_note.note().protocol()).into(),
index: rec_note.output_index().into(),
value: rec_note.note().value().into(),
})
}))
.collect();
let sapling_inputs = value
.sapling_inputs()
.map(|sapling_inputs| proposal::SaplingInputs {
anchor_height: sapling_inputs.anchor_height().into(),
inputs: sapling_inputs
.notes()
.iter()
.map(|rec_note| proposal::ProposedInput {
txid: rec_note.txid().as_ref().to_vec(),
index: rec_note.output_index().into(),
value: rec_note.value().into(),
})
.collect(),
});
let balance = Some(proposal::TransactionBalance {
proposed_change: value
.balance()
.proposed_change()
.iter()
.map(|change| match change.output_pool() {
ShieldedProtocol::Sapling => proposal::ChangeValue {
value: Some(proposal::change_value::Value::SaplingValue(
proposal::SaplingChange {
amount: change.value().into(),
memo: change.memo().map(|memo_bytes| proposal::MemoBytes {
value: memo_bytes.as_slice().to_vec(),
}),
},
)),
},
ShieldedProtocol::Orchard => {
unimplemented!("FIXME: implement Orchard change outputs!")
}
.map(|change| proposal::ChangeValue {
value: change.value().into(),
value_pool: proposal::ValuePool::from(change.output_pool()).into(),
memo: change.memo().map(|memo_bytes| proposal::MemoBytes {
value: memo_bytes.as_slice().to_vec(),
}),
})
.collect(),
fee_required: value.balance().fee_required().into(),
@ -364,8 +394,8 @@ impl proposal::Proposal {
Some(proposal::Proposal {
proto_version: PROPOSAL_SER_V1,
transaction_request,
transparent_inputs,
sapling_inputs,
anchor_height,
inputs,
balance,
fee_rule: match value.fee_rule() {
StandardFeeRule::PreZip313 => proposal::FeeRule::PreZip313,
@ -402,72 +432,59 @@ impl proposal::Proposal {
let transaction_request =
TransactionRequest::from_uri(params, &self.transaction_request)?;
let transparent_inputs = self
.transparent_inputs
.iter()
.map(|t_in| {
let txid = t_in
.parse_txid()
.map_err(ProposalDecodingError::TxIdInvalid)?;
#[cfg(not(feature = "transparent-inputs"))]
return Err(ProposalDecodingError::InputNotFound(
txid,
PoolType::Transparent,
t_in.index,
));
#[cfg(not(feature = "transparent-inputs"))]
let transparent_inputs = vec![];
#[cfg(feature = "transparent-inputs")]
let mut transparent_inputs = vec![];
#[cfg(feature = "transparent-inputs")]
{
let outpoint = OutPoint::new(txid.into(), t_in.index);
wallet_db
.get_unspent_transparent_output(&outpoint)
.map_err(ProposalDecodingError::InputRetrieval)?
.ok_or({
ProposalDecodingError::InputNotFound(
txid,
PoolType::Transparent,
t_in.index,
)
})
let mut received_notes = vec![];
for input in self.inputs.iter() {
let txid = input
.parse_txid()
.map_err(ProposalDecodingError::TxIdInvalid)?;
match input.pool_type()? {
PoolType::Transparent => {
#[cfg(not(feature = "transparent-inputs"))]
return Err(ProposalDecodingError::ValuePoolInvalid(1));
#[cfg(feature = "transparent-inputs")]
{
let outpoint = OutPoint::new(txid.into(), input.index);
transparent_inputs.push(
wallet_db
.get_unspent_transparent_output(&outpoint)
.map_err(ProposalDecodingError::InputRetrieval)?
.ok_or({
ProposalDecodingError::InputNotFound(
txid,
PoolType::Transparent,
input.index,
)
})?,
);
}
}
})
.collect::<Result<Vec<_>, _>>()?;
let sapling_inputs = self.sapling_inputs.as_ref().map(|s_in| {
s_in.inputs
.iter()
.map(|s_in| {
let txid = s_in
.parse_txid()
.map_err(ProposalDecodingError::TxIdInvalid)?;
PoolType::Shielded(protocol) => received_notes.push(
wallet_db
.get_spendable_note(&txid, ShieldedProtocol::Sapling, s_in.index)
.get_spendable_note(&txid, protocol, input.index)
.map_err(ProposalDecodingError::InputRetrieval)
.and_then(|opt| {
opt.ok_or({
ProposalDecodingError::InputNotFound(
txid,
PoolType::Shielded(ShieldedProtocol::Sapling),
s_in.index,
PoolType::Shielded(protocol),
input.index,
)
})
})
})
.collect::<Result<Vec<_>, _>>()
.and_then(|notes| {
NonEmpty::from_vec(notes)
.map(|notes| {
SaplingInputs::from_parts(s_in.anchor_height.into(), notes)
})
.ok_or({
ProposalDecodingError::EmptyShieldedInputs(
ShieldedProtocol::Sapling,
)
})
})
});
})?,
),
}
}
let shielded_inputs = NonEmpty::from_vec(received_notes)
.map(|notes| ShieldedInputs::from_parts(self.anchor_height.into(), notes));
let proto_balance = self
.balance
@ -477,31 +494,23 @@ impl proposal::Proposal {
proto_balance
.proposed_change
.iter()
.filter_map(|change| {
// An empty `value` field can be treated as though the whole
// `ChangeValue` was absent; this optionality is an artifact of
// protobuf encoding.
change.value.as_ref().map(
|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
match cv {
proposal::change_value::Value::SaplingValue(sc) => {
Ok(ChangeValue::sapling(
NonNegativeAmount::from_u64(sc.amount).map_err(
|_| ProposalDecodingError::BalanceInvalid,
)?,
sc.memo
.as_ref()
.map(|bytes| {
MemoBytes::from_bytes(&bytes.value).map_err(
ProposalDecodingError::MemoInvalid,
)
})
.transpose()?,
))
}
}
},
)
.map(|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
match cv.pool_type()? {
PoolType::Shielded(ShieldedProtocol::Sapling) => {
Ok(ChangeValue::sapling(
NonNegativeAmount::from_u64(cv.value)
.map_err(|_| ProposalDecodingError::BalanceInvalid)?,
cv.memo
.as_ref()
.map(|bytes| {
MemoBytes::from_bytes(&bytes.value)
.map_err(ProposalDecodingError::MemoInvalid)
})
.transpose()?,
))
}
t => Err(ProposalDecodingError::InvalidChangeRecipient(t)),
}
})
.collect::<Result<Vec<_>, _>>()?,
NonNegativeAmount::from_u64(proto_balance.fee_required)
@ -512,7 +521,7 @@ impl proposal::Proposal {
Proposal::from_parts(
transaction_request,
transparent_inputs,
sapling_inputs.transpose()?,
shielded_inputs,
balance,
fee_rule,
self.min_target_height.into(),

View File

@ -8,13 +8,15 @@ pub struct Proposal {
/// ZIP 321 serialized transaction request
#[prost(string, tag = "2")]
pub transaction_request: ::prost::alloc::string::String,
/// The transparent UTXOs to use as inputs to the transaction.
#[prost(message, repeated, tag = "3")]
pub transparent_inputs: ::prost::alloc::vec::Vec<ProposedInput>,
/// The Sapling input notes and anchor height to be used in creating the transaction.
#[prost(message, optional, tag = "4")]
pub sapling_inputs: ::core::option::Option<SaplingInputs>,
/// The total value, fee amount, and change outputs of the proposed
/// The anchor height to be used in creating the transaction, if any.
/// Setting the anchor height to zero will disallow the use of any shielded
/// inputs.
#[prost(uint32, tag = "3")]
pub anchor_height: u32,
/// The inputs to be used in creating the transaction.
#[prost(message, repeated, tag = "4")]
pub inputs: ::prost::alloc::vec::Vec<ProposedInput>,
/// The total value, fee value, and change outputs of the proposed
/// transaction
#[prost(message, optional, tag = "5")]
pub balance: ::core::option::Option<TransactionBalance>,
@ -32,28 +34,20 @@ pub struct Proposal {
#[prost(bool, tag = "8")]
pub is_shielding: bool,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SaplingInputs {
/// The Sapling anchor height to be used in creating the transaction
#[prost(uint32, tag = "1")]
pub anchor_height: u32,
/// The unique identifier and amount for each proposed Sapling input
#[prost(message, repeated, tag = "2")]
pub inputs: ::prost::alloc::vec::Vec<ProposedInput>,
}
/// The unique identifier and amount for each proposed input.
/// The unique identifier and value for each proposed input.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProposedInput {
#[prost(bytes = "vec", tag = "1")]
pub txid: ::prost::alloc::vec::Vec<u8>,
#[prost(uint32, tag = "2")]
#[prost(enumeration = "ValuePool", tag = "2")]
pub value_pool: i32,
#[prost(uint32, tag = "3")]
pub index: u32,
#[prost(uint64, tag = "3")]
#[prost(uint64, tag = "4")]
pub value: u64,
}
/// The proposed change outputs and fee amount.
/// The proposed change outputs and fee value.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TransactionBalance {
@ -62,21 +56,17 @@ pub struct TransactionBalance {
#[prost(uint64, tag = "2")]
pub fee_required: u64,
}
/// An enumeration of change value types.
/// A proposed change output. If the transparent value pool is selected,
/// the `memo` field must be null.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ChangeValue {
#[prost(oneof = "change_value::Value", tags = "1")]
pub value: ::core::option::Option<change_value::Value>,
}
/// Nested message and enum types in `ChangeValue`.
pub mod change_value {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum Value {
#[prost(message, tag = "1")]
SaplingValue(super::SaplingChange),
}
#[prost(uint64, tag = "1")]
pub value: u64,
#[prost(enumeration = "ValuePool", tag = "2")]
pub value_pool: i32,
#[prost(message, optional, tag = "3")]
pub memo: ::core::option::Option<MemoBytes>,
}
/// An object wrapper for memo bytes, to facilitate representing the
/// `change_memo == None` case.
@ -86,14 +76,44 @@ pub struct MemoBytes {
#[prost(bytes = "vec", tag = "1")]
pub value: ::prost::alloc::vec::Vec<u8>,
}
/// The amount and memo for a proposed Sapling change output.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SaplingChange {
#[prost(uint64, tag = "1")]
pub amount: u64,
#[prost(message, optional, tag = "2")]
pub memo: ::core::option::Option<MemoBytes>,
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum ValuePool {
/// Protobuf requires that enums have a zero discriminant as the default
/// value. However, we need to require that a known value pool is selected,
/// and we do not want to fall back to any default, so sending the
/// PoolNotSpecified value will be treated as an error.
PoolNotSpecified = 0,
/// The transparent value pool (P2SH is not distinguished from P2PKH)
Transparent = 1,
/// The Sapling value pool
Sapling = 2,
/// The Orchard value pool
Orchard = 3,
}
impl ValuePool {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
ValuePool::PoolNotSpecified => "PoolNotSpecified",
ValuePool::Transparent => "Transparent",
ValuePool::Sapling => "Sapling",
ValuePool::Orchard => "Orchard",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"PoolNotSpecified" => Some(Self::PoolNotSpecified),
"Transparent" => Some(Self::Transparent),
"Sapling" => Some(Self::Sapling),
"Orchard" => Some(Self::Orchard),
_ => None,
}
}
}
/// The fee rule used in constructing a Proposal
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]

View File

@ -19,6 +19,9 @@ use zcash_primitives::{
use crate::{address::UnifiedAddress, fees::sapling as sapling_fees, PoolType, ShieldedProtocol};
#[cfg(feature = "orchard")]
use crate::fees::orchard as orchard_fees;
/// A unique identifier for a shielded transaction output
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NoteId {
@ -255,26 +258,34 @@ impl Note {
),
}
}
pub fn protocol(&self) -> ShieldedProtocol {
match self {
Note::Sapling(_) => ShieldedProtocol::Sapling,
#[cfg(feature = "orchard")]
Note::Orchard(_) => ShieldedProtocol::Orchard,
}
}
}
/// Information about a note that is tracked by the wallet that is available for spending,
/// with sufficient information for use in note selection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceivedNote<NoteRef> {
pub struct ReceivedNote<NoteRef, NoteT> {
note_id: NoteRef,
txid: TxId,
output_index: u16,
note: Note,
note: NoteT,
spending_key_scope: Scope,
note_commitment_tree_position: Position,
}
impl<NoteRef> ReceivedNote<NoteRef> {
impl<NoteRef, NoteT> ReceivedNote<NoteRef, NoteT> {
pub fn from_parts(
note_id: NoteRef,
txid: TxId,
output_index: u16,
note: Note,
note: NoteT,
spending_key_scope: Scope,
note_commitment_tree_position: Position,
) -> Self {
@ -297,27 +308,72 @@ impl<NoteRef> ReceivedNote<NoteRef> {
pub fn output_index(&self) -> u16 {
self.output_index
}
pub fn note(&self) -> &Note {
pub fn note(&self) -> &NoteT {
&self.note
}
pub fn value(&self) -> NonNegativeAmount {
self.note.value()
}
pub fn spending_key_scope(&self) -> Scope {
self.spending_key_scope
}
pub fn note_commitment_tree_position(&self) -> Position {
self.note_commitment_tree_position
}
/// Applies the given function to the `note` field of this ReceivedNote and returns
/// `None` if that function returns `None`, or otherwise a `Some` containing
/// a `ReceivedNote` with its `note` field swapped out for the result of the function.
///
/// The name `traverse` refers to the general operation that has the Haskell type
/// `Applicative f => (a -> f b) -> t a -> f (t b)`, that this method specializes
/// with `ReceivedNote<NoteRef, _>` for `t` and `Option<_>` for `f`.
pub fn traverse_opt<B>(
self,
f: impl FnOnce(NoteT) -> Option<B>,
) -> Option<ReceivedNote<NoteRef, B>> {
f(self.note).map(|n0| ReceivedNote {
note_id: self.note_id,
txid: self.txid,
output_index: self.output_index,
note: n0,
spending_key_scope: self.spending_key_scope,
note_commitment_tree_position: self.note_commitment_tree_position,
})
}
}
impl<NoteRef> sapling_fees::InputView<NoteRef> for ReceivedNote<NoteRef> {
impl<NoteRef> ReceivedNote<NoteRef, Note> {
pub fn protocol(&self) -> ShieldedProtocol {
match self.note() {
Note::Sapling(_) => ShieldedProtocol::Sapling,
#[cfg(feature = "orchard")]
Note::Orchard(_) => ShieldedProtocol::Orchard,
}
}
}
impl<NoteRef> sapling_fees::InputView<NoteRef> for ReceivedNote<NoteRef, sapling::Note> {
fn note_id(&self) -> &NoteRef {
&self.note_id
}
fn value(&self) -> NonNegativeAmount {
self.note.value()
self.note
.value()
.try_into()
.expect("Sapling note values are indirectly checked by consensus.")
}
}
#[cfg(feature = "orchard")]
impl<NoteRef> orchard_fees::InputView<NoteRef> for ReceivedNote<NoteRef, orchard::Note> {
fn note_id(&self) -> &NoteRef {
&self.note_id
}
fn value(&self) -> NonNegativeAmount {
self.note
.value()
.try_into()
.expect("Orchard note values are indirectly checked by consensus.")
}
}

View File

@ -22,6 +22,7 @@ rustdoc-args = ["--cfg", "docsrs"]
zcash_client_backend = { workspace = true, features = ["unstable-serialization", "unstable-spanning-tree"] }
zcash_encoding.workspace = true
zcash_primitives.workspace = true
orchard.workspace = true
# Dependencies exposed in a public API:
# (Breaking upgrades to these require a breaking upgrade to this crate.)

View File

@ -69,7 +69,7 @@ use zcash_client_backend::{
},
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock,
wallet::{NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
};
@ -172,7 +172,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
txid: &TxId,
_protocol: ShieldedProtocol,
index: u32,
) -> Result<Option<ReceivedNote<Self::NoteRef>>, Self::Error> {
) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
wallet::sapling::get_spendable_sapling_note(self.conn.borrow(), &self.params, txid, index)
}
@ -183,7 +183,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
_sources: &[ShieldedProtocol],
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedNote<Self::NoteRef>>, Self::Error> {
) -> Result<Vec<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
wallet::sapling::select_spendable_sapling_notes(
self.conn.borrow(),
&self.params,
@ -364,6 +364,14 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
}
#[cfg(feature = "orchard")]
fn get_orchard_nullifiers(
&self,
_query: NullifierQuery,
) -> Result<Vec<(AccountId, orchard::note::Nullifier)>, Self::Error> {
todo!()
}
}
impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P> {

View File

@ -100,7 +100,7 @@ impl ReceivedSaplingOutput for DecryptedOutput<sapling::Note> {
fn to_spendable_note<P: consensus::Parameters>(
params: &P,
row: &Row,
) -> Result<ReceivedNote<ReceivedNoteId>, SqliteClientError> {
) -> Result<ReceivedNote<ReceivedNoteId, Note>, SqliteClientError> {
let note_id = ReceivedNoteId(row.get(0)?);
let txid = row.get::<_, [u8; 32]>(1).map(TxId::from_bytes)?;
let output_index = row.get(2)?;
@ -182,7 +182,7 @@ pub(crate) fn get_spendable_sapling_note<P: consensus::Parameters>(
params: &P,
txid: &TxId,
index: u32,
) -> Result<Option<ReceivedNote<ReceivedNoteId>>, SqliteClientError> {
) -> Result<Option<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError> {
let mut stmt_select_note = conn.prepare_cached(
"SELECT id_note, txid, output_index, diversifier, value, rcm, commitment_tree_position,
accounts.ufvk, recipient_key_scope
@ -239,7 +239,7 @@ pub(crate) fn select_spendable_sapling_notes<P: consensus::Parameters>(
target_value: Amount,
anchor_height: BlockHeight,
exclude: &[ReceivedNoteId],
) -> Result<Vec<ReceivedNote<ReceivedNoteId>>, SqliteClientError> {
) -> Result<Vec<ReceivedNote<ReceivedNoteId, Note>>, SqliteClientError> {
let birthday_height = match wallet_birthday(conn)? {
Some(birthday) => birthday,
None => {