zcash_client_backend: Add `RecipientAddress::Unified`

This commit is contained in:
Jack Grigg 2022-06-07 11:35:53 +00:00
parent f20366cf86
commit ed6016857e
6 changed files with 226 additions and 8 deletions

View File

@ -25,6 +25,8 @@ and this library adheres to Rust's notion of
of a `zcash_client_backend::zip321::TransactionRequest` value.
This facilitates the implementation of ZIP 321 support in wallets and
provides substantially greater flexibility in transaction creation.
- `zcash_client_backend::address`:
- `RecipientAddress::Unified`
- `zcash_client_backend::proto`:
- `actions` field on `compact_formats::CompactTx`
- `compact_formats::CompactOrchardAction`
@ -35,6 +37,7 @@ and this library adheres to Rust's notion of
- New experimental APIs that should be considered unstable, and are
likely to be modified and/or moved to a different module in a future
release:
- `zcash_client_backend::address::UnifiedAddress`
- `zcash_client_backend::keys::{UnifiedSpendingKey`, `UnifiedFullViewingKey`}
- `zcash_client_backend::encoding::AddressCodec`
- `zcash_client_backend::encoding::encode_payment_address`

View File

@ -24,6 +24,7 @@ hdwallet = { version = "0.3.1", optional = true }
jubjub = "0.9"
log = "0.4"
nom = "7"
orchard = "0.1"
percent-encoding = "2.1.0"
proptest = { version = "1.0.0", optional = true }
protobuf = "~2.27.1" # MSRV 1.52.1
@ -48,7 +49,11 @@ zcash_proofs = { version = "0.6", path = "../zcash_proofs" }
[features]
transparent-inputs = ["ripemd", "hdwallet", "sha2", "secp256k1", "zcash_primitives/transparent-inputs"]
test-dependencies = ["proptest", "zcash_primitives/test-dependencies"]
test-dependencies = [
"proptest",
"orchard/test-dependencies",
"zcash_primitives/test-dependencies",
]
[lib]
bench = false

View File

@ -1,6 +1,11 @@
//! Structs for handling supported address types.
use zcash_address::{ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress};
use std::convert::TryFrom;
use zcash_address::{
unified::{self, Container, Encoding},
ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress,
};
use zcash_primitives::{consensus, constants, legacy::TransparentAddress, sapling::PaymentAddress};
fn params_to_network<P: consensus::Parameters>(params: &P) -> Network {
@ -13,12 +18,134 @@ fn params_to_network<P: consensus::Parameters>(params: &P) -> Network {
}
}
/// A Unified Address.
#[derive(Clone, Debug, PartialEq)]
pub struct UnifiedAddress {
orchard: Option<orchard::Address>,
sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>,
unknown: Vec<(u32, Vec<u8>)>,
}
impl TryFrom<unified::Address> for UnifiedAddress {
type Error = &'static str;
fn try_from(ua: unified::Address) -> Result<Self, Self::Error> {
let mut orchard = None;
let mut sapling = None;
let mut transparent = None;
// We can use as-parsed order here for efficiency, because we're breaking out the
// receivers we support from the unknown receivers.
let unknown = ua
.items_as_parsed()
.iter()
.filter_map(|receiver| match receiver {
unified::Receiver::Orchard(data) => {
Option::from(orchard::Address::from_raw_address_bytes(data))
.ok_or("Invalid Orchard receiver in Unified Address")
.map(|addr| {
orchard = Some(addr);
None
})
.transpose()
}
unified::Receiver::Sapling(data) => PaymentAddress::from_bytes(data)
.ok_or("Invalid Sapling receiver in Unified Address")
.map(|pa| {
sapling = Some(pa);
None
})
.transpose(),
unified::Receiver::P2pkh(data) => {
transparent = Some(TransparentAddress::PublicKey(*data));
None
}
unified::Receiver::P2sh(data) => {
transparent = Some(TransparentAddress::Script(*data));
None
}
unified::Receiver::Unknown { typecode, data } => {
Some(Ok((*typecode, data.clone())))
}
})
.collect::<Result<_, _>>()?;
Ok(Self {
orchard,
sapling,
transparent,
unknown,
})
}
}
impl UnifiedAddress {
/// Constructs a Unified Address from a given set of receivers.
///
/// Returns `None` if the receivers would produce an invalid Unified Address (namely,
/// if no shielded receiver is provided).
pub fn from_receivers(
orchard: Option<orchard::Address>,
sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>,
) -> Option<Self> {
if orchard.is_some() || sapling.is_some() {
Some(Self {
orchard,
sapling,
transparent,
unknown: vec![],
})
} else {
// UAs require at least one shielded receiver.
None
}
}
/// Returns the Sapling receiver within this Unified Address, if any.
pub fn sapling(&self) -> Option<&PaymentAddress> {
self.sapling.as_ref()
}
fn to_address(&self, net: Network) -> ZcashAddress {
let ua = unified::Address::try_from_items(
self.unknown
.iter()
.map(|(typecode, data)| unified::Receiver::Unknown {
typecode: *typecode,
data: data.clone(),
})
.chain(self.transparent.as_ref().map(|taddr| match taddr {
TransparentAddress::PublicKey(data) => unified::Receiver::P2pkh(*data),
TransparentAddress::Script(data) => unified::Receiver::P2sh(*data),
}))
.chain(
self.sapling
.as_ref()
.map(|pa| pa.to_bytes())
.map(unified::Receiver::Sapling),
)
.chain(
self.orchard
.as_ref()
.map(|addr| addr.to_raw_address_bytes())
.map(unified::Receiver::Orchard),
)
.collect(),
)
.expect("UnifiedAddress should only be constructed safely");
ZcashAddress::from_unified(net, ua)
}
}
/// An address that funds can be sent to.
// TODO: rename to ParsedAddress
#[derive(Debug, PartialEq, Clone)]
pub enum RecipientAddress {
Shielded(PaymentAddress),
Transparent(TransparentAddress),
Unified(UnifiedAddress),
}
impl From<PaymentAddress> for RecipientAddress {
@ -33,6 +160,12 @@ impl From<TransparentAddress> for RecipientAddress {
}
}
impl From<UnifiedAddress> for RecipientAddress {
fn from(addr: UnifiedAddress) -> Self {
RecipientAddress::Unified(addr)
}
}
impl TryFromRawAddress for RecipientAddress {
type Error = &'static str;
@ -41,6 +174,14 @@ impl TryFromRawAddress for RecipientAddress {
Ok(pa.into())
}
fn try_from_raw_unified(
ua: zcash_address::unified::Address,
) -> Result<Self, ConversionError<Self::Error>> {
UnifiedAddress::try_from(ua)
.map_err(ConversionError::User)
.map(RecipientAddress::from)
}
fn try_from_raw_transparent_p2pkh(
data: [u8; 20],
) -> Result<Self, ConversionError<Self::Error>> {
@ -69,7 +210,42 @@ impl RecipientAddress {
}
TransparentAddress::Script(data) => ZcashAddress::from_transparent_p2sh(net, *data),
},
RecipientAddress::Unified(ua) => ua.to_address(net),
}
.to_string()
}
}
#[cfg(test)]
mod tests {
use zcash_primitives::{consensus::MAIN_NETWORK, zip32::ExtendedFullViewingKey};
use super::{RecipientAddress, UnifiedAddress};
use crate::keys::sapling;
#[test]
fn ua_round_trip() {
let orchard = {
let sk = orchard::keys::SpendingKey::from_zip32_seed(&[0; 32], 0, 0).unwrap();
let fvk = orchard::keys::FullViewingKey::from(&sk);
Some(fvk.address_at(0u32, orchard::keys::Scope::External))
};
let sapling = {
let extsk = sapling::spending_key(&[0; 32], 0, 0.into());
let extfvk = ExtendedFullViewingKey::from(&extsk);
Some(extfvk.default_address().1)
};
let transparent = { None };
let ua = UnifiedAddress::from_receivers(orchard, sapling, transparent).unwrap();
let addr = RecipientAddress::Unified(ua);
let addr_str = addr.encode(&MAIN_NETWORK);
assert_eq!(
RecipientAddress::decode(&MAIN_NETWORK, &addr_str),
Some(addr)
);
}
}

View File

@ -334,6 +334,16 @@ where
for payment in request.payments() {
match &payment.recipient_address {
RecipientAddress::Unified(ua) => builder
.add_sapling_output(
ovk,
ua.sapling()
.expect("TODO: Add Orchard support to builder")
.clone(),
payment.amount,
payment.memo.clone().unwrap_or_else(MemoBytes::empty),
)
.map_err(Error::Builder),
RecipientAddress::Shielded(to) => builder
.add_sapling_output(
ovk,
@ -359,7 +369,8 @@ where
let sent_outputs = request.payments().iter().enumerate().map(|(i, payment)| {
let idx = match &payment.recipient_address {
// Sapling outputs are shuffled, so we need to look up where the output ended up.
RecipientAddress::Shielded(_) =>
// TODO: When we add Orchard support, we will need to trial-decrypt to find them.
RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) =>
tx_metadata.output_index(i).expect("An output should exist in the transaction for each shielded payment."),
RecipientAddress::Transparent(addr) => {
let script = addr.script();

View File

@ -452,7 +452,9 @@ mod parse {
match v {
Param::Amount(a) => payment.amount = a,
Param::Memo(m) => match payment.recipient_address {
RecipientAddress::Shielded(_) => payment.memo = Some(m),
RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => {
payment.memo = Some(m)
}
RecipientAddress::Transparent(_) => {
return Err(Zip321Error::TransparentMemo(i))
}
@ -646,14 +648,24 @@ pub mod testing {
transaction::components::amount::testing::arb_nonnegative_amount,
};
use crate::address::RecipientAddress;
use crate::address::{RecipientAddress, UnifiedAddress};
use super::{MemoBytes, Payment, TransactionRequest};
prop_compose! {
fn arb_unified_addr()(
sapling in arb_shielded_addr(),
transparent in option::of(arb_transparent_addr()),
) -> UnifiedAddress {
UnifiedAddress::from_receivers(None, Some(sapling), transparent).unwrap()
}
}
pub fn arb_addr() -> impl Strategy<Value = RecipientAddress> {
prop_oneof![
arb_shielded_addr().prop_map(RecipientAddress::Shielded),
arb_transparent_addr().prop_map(RecipientAddress::Transparent),
arb_unified_addr().prop_map(RecipientAddress::Unified),
]
}
@ -676,15 +688,15 @@ pub mod testing {
other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3),
) -> Payment {
let is_sapling = match recipient_address {
let is_shielded = match recipient_address {
RecipientAddress::Transparent(_) => false,
RecipientAddress::Shielded(_) => true,
RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => true,
};
Payment {
recipient_address,
amount,
memo: memo.filter(|_| is_sapling),
memo: memo.filter(|_| is_shielded),
label,
message,
other_params: other_params.into_iter().collect(),

View File

@ -642,6 +642,17 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
for output in &sent_tx.outputs {
match output.recipient_address {
// TODO: Store the entire UA, not just the Sapling component.
// This will require more info about the output index.
RecipientAddress::Unified(ua) => wallet::insert_sent_note(
up,
tx_ref,
output.output_index,
sent_tx.account,
ua.sapling().expect("TODO: Add Orchard support"),
output.value,
output.memo.as_ref(),
)?,
RecipientAddress::Shielded(addr) => wallet::insert_sent_note(
up,
tx_ref,