Skip to content

Commit 24b8147

Browse files
committed
WIP: add support for transparent-source-only addresses.
Signed-off-by: Daira-Emma Hopwood <[email protected]>
1 parent a9aabb2 commit 24b8147

File tree

7 files changed

+191
-7
lines changed

7 files changed

+191
-7
lines changed

zcash_client_backend/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ and this library adheres to Rust's notion of
7676
constraint on its `<AccountId>` parameter has been strengthened to `Copy`.
7777
- `zcash_client_backend::fees`:
7878
- Arguments to `ChangeStrategy::compute_balance` have changed.
79+
- `zcash_client_backend::input_selection::GreedyInputSelectorError` has a
80+
new variant `UnsupportedTransparentSourceOnlyAddress`.
7981
- `zcash_client_backend::proto`:
8082
- `ProposalDecodingError` has a new variant `TransparentMemo`.
8183
- `zcash_client_backend::zip321::render::amount_str` now takes a

zcash_client_backend/src/data_api/wallet.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,9 @@ where
10321032
}
10331033
transparent_output_meta.push((to, payment.amount));
10341034
}
1035+
Address::TransparentSourceOnly(_) => {
1036+
panic!("Transparent-source-only addresses should not occur at this stage");
1037+
}
10351038
}
10361039
}
10371040

zcash_client_backend/src/data_api/wallet/input_selection.rs

+153-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ use crate::{
3131

3232
#[cfg(feature = "transparent-inputs")]
3333
use {
34-
std::collections::BTreeSet, std::convert::Infallible,
34+
crate::{
35+
fees::TransactionBalance,
36+
proposal::{StepOutput, StepOutputIndex},
37+
zip321::Payment,
38+
},
39+
std::collections::BTreeSet,
40+
std::convert::Infallible,
3541
zcash_primitives::legacy::TransparentAddress,
3642
zcash_primitives::transaction::components::OutPoint,
3743
};
@@ -206,6 +212,8 @@ pub enum GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT> {
206212
Balance(BalanceError),
207213
/// A unified address did not contain a supported receiver.
208214
UnsupportedAddress(Box<UnifiedAddress>),
215+
/// Support for transparent-source-only addresses requires the transparent-inputs feature.
216+
UnsupportedTransparentSourceOnlyAddress,
209217
/// An error was encountered in change selection.
210218
Change(ChangeError<ChangeStrategyErrT, NoteRefT>),
211219
}
@@ -223,6 +231,9 @@ impl<CE: fmt::Display, N: fmt::Display> fmt::Display for GreedyInputSelectorErro
223231
// don't have network parameters here
224232
write!(f, "Unified address contains no supported receivers.")
225233
}
234+
GreedyInputSelectorError::UnsupportedTransparentSourceOnlyAddress => {
235+
write!(f, "Support for transparent-source-only addresses requires the transparent-inputs feature.")
236+
}
226237
GreedyInputSelectorError::Change(err) => {
227238
write!(f, "An error occurred computing change and fees: {}", err)
228239
}
@@ -343,6 +354,22 @@ where
343354
#[cfg(feature = "orchard")]
344355
let mut orchard_outputs = vec![];
345356
let mut payment_pools = BTreeMap::new();
357+
#[cfg(feature = "transparent-inputs")]
358+
let mut ephemeral_outputs: Vec<(usize, Payment)> = vec![];
359+
360+
// TR - a
361+
// - ephb
362+
// - ephc
363+
// - d
364+
//
365+
// becomes
366+
//
367+
// TR1 - a TR2 - b
368+
// - eph-placeholder - c
369+
// - d Proposal2
370+
// - spend from eph-placeholder
371+
//
372+
346373
for (idx, payment) in transaction_request.payments() {
347374
match &payment.recipient_address {
348375
Address::Transparent(addr) => {
@@ -352,6 +379,28 @@ where
352379
script_pubkey: addr.script(),
353380
});
354381
}
382+
#[cfg(feature = "transparent-inputs")]
383+
Address::TransparentSourceOnly(data) => {
384+
ephemeral_outputs.push((
385+
*idx,
386+
Payment {
387+
recipient_address: Address::Transparent(
388+
TransparentAddress::PublicKeyHash(*data),
389+
),
390+
amount: payment.amount,
391+
memo: None,
392+
label: payment.label.clone(),
393+
message: payment.message.clone(),
394+
other_params: payment.other_params.clone(),
395+
},
396+
));
397+
}
398+
#[cfg(not(feature = "transparent-inputs"))]
399+
Address::TransparentSourceOnly(_) => {
400+
return Err(InputSelectorError::Selection(
401+
GreedyInputSelectorError::UnsupportedTransparentSourceOnlyAddress,
402+
));
403+
}
355404
Address::Sapling(_) => {
356405
payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling));
357406
sapling_outputs.push(SaplingPayment(payment.amount));
@@ -386,6 +435,99 @@ where
386435
}
387436
}
388437

438+
#[cfg(feature = "transparent-inputs")]
439+
struct Zip320SecondStep {
440+
transaction_request: TransactionRequest,
441+
payment_pools: BTreeMap<usize, PoolType>,
442+
prior_step_inputs: StepOutput,
443+
balance: TransactionBalance,
444+
}
445+
446+
// If ephemeral state was detected, handle it here.
447+
#[cfg(feature = "transparent-inputs")]
448+
let (transaction_request, second_step) = if ephemeral_outputs.is_empty() {
449+
(transaction_request, None)
450+
} else {
451+
// Construct a new TransactionRequest from `transaction_request` that excludes
452+
// the ephemeral outputs, and in their place includes a single transparent
453+
// output to the legacy address for this account.
454+
455+
let mut proposal1_excludes = BTreeSet::new();
456+
let mut proposal2_payments = vec![];
457+
let mut proposal2_payment_pools = BTreeMap::new();
458+
let mut total_ephemeral_plus_fee: NonNegativeAmount = (|| todo!())();
459+
let (first_idx, _) = ephemeral_outputs[0];
460+
for (idx2, (idx1, payment)) in ephemeral_outputs.into_iter().enumerate() {
461+
total_ephemeral_plus_fee = (total_ephemeral_plus_fee + payment.amount).ok_or(
462+
InputSelectorError::Selection(GreedyInputSelectorError::Balance(
463+
BalanceError::Overflow,
464+
)),
465+
)?;
466+
proposal1_excludes.insert(idx1);
467+
proposal2_payments.push(payment);
468+
proposal2_payment_pools.insert(idx2, PoolType::Transparent);
469+
}
470+
471+
// TODO: Calculate fee for transaction request 2 and add it to
472+
// total_ephemeral_plus_fee.
473+
474+
let proposal1_transaction_request = TransactionRequest::from_indexed(
475+
transaction_request
476+
.payments()
477+
.iter()
478+
.filter_map(|(idx, payment)| {
479+
// If this is the first ephemeral output in the original request,
480+
// replace it with a transparent output to the legacy address.
481+
// We use the legacy address here only as a dummy address for the
482+
// ZIP-321 URI serialized in the proposal. It will be replaced by
483+
// a unique ephemeral P2PKH address when the proposal is created.
484+
if *idx == first_idx {
485+
let legacy_address: TransparentAddress = (|| todo!())();
486+
transparent_outputs.push(TxOut {
487+
value: total_ephemeral_plus_fee,
488+
script_pubkey: legacy_address.script(),
489+
});
490+
payment_pools.insert(first_idx, PoolType::Transparent);
491+
Some((
492+
first_idx,
493+
Payment {
494+
recipient_address: Address::Transparent(legacy_address),
495+
amount: total_ephemeral_plus_fee,
496+
memo: None,
497+
label: None,
498+
message: None,
499+
// TODO: Add ZIP 320 ephemeral marker, or shift this
500+
// into being a change output (which seems to need
501+
// changes to the Proposal protobuf, as it doesn't
502+
// explicitly store change, or have any way to mark
503+
// the ephemeral output).
504+
other_params: vec![],
505+
},
506+
))
507+
} else if proposal1_excludes.contains(&idx) {
508+
None
509+
} else {
510+
Some((*idx, payment.clone()))
511+
}
512+
})
513+
.collect(),
514+
)
515+
.expect("correct by construction");
516+
let proposal2_transaction_request =
517+
TransactionRequest::new(proposal2_payments).expect("correct by construction");
518+
519+
(
520+
proposal1_transaction_request,
521+
Some(Zip320SecondStep {
522+
transaction_request: proposal2_transaction_request,
523+
payment_pools: proposal2_payment_pools,
524+
// TODO: Update this depending on how the ephemeral output is represented.
525+
prior_step_inputs: StepOutput::new(0, StepOutputIndex::Payment(first_idx)),
526+
balance: (|| todo!())(),
527+
}),
528+
)
529+
};
530+
389531
let mut shielded_inputs: Vec<ReceivedNote<DbT::NoteRef, Note>> = vec![];
390532
let mut prior_available = NonNegativeAmount::ZERO;
391533
let mut amount_required = NonNegativeAmount::ZERO;
@@ -432,6 +574,16 @@ where
432574

433575
match balance {
434576
Ok(balance) => {
577+
#[cfg(feature = "transparent-inputs")]
578+
if let Some(zip320_step) = second_step {
579+
return Proposal::multi_step(
580+
(*self.change_strategy.fee_rule()).clone(),
581+
target_height,
582+
(|| todo!())(),
583+
)
584+
.map_err(InputSelectorError::Proposal);
585+
}
586+
435587
return Proposal::single_step(
436588
transaction_request,
437589
payment_pools,

zcash_client_backend/src/zip321.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,9 @@ mod parse {
551551
Param::Amount(a) => payment.amount = a,
552552
Param::Memo(m) => match payment.recipient_address {
553553
Address::Sapling(_) | Address::Unified(_) => payment.memo = Some(m),
554-
Address::Transparent(_) => return Err(Zip321Error::TransparentMemo(i)),
554+
Address::Transparent(_) | Address::TransparentSourceOnly(_) => {
555+
return Err(Zip321Error::TransparentMemo(i))
556+
}
555557
},
556558

557559
Param::Label(m) => payment.label = Some(m),
@@ -767,7 +769,7 @@ pub mod testing {
767769
other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3),
768770
) -> Payment {
769771
let is_shielded = match recipient_address {
770-
Address::Transparent(_) => false,
772+
Address::Transparent(_) | Address::TransparentSourceOnly(_) => false,
771773
Address::Sapling(_) | Address::Unified(_) => true,
772774
};
773775

zcash_client_sqlite/src/testing.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1266,7 +1266,9 @@ fn fake_compact_block_spending<P: consensus::Parameters, Fvk: TestFvk>(
12661266
)
12671267
.0,
12681268
),
1269-
Address::Transparent(_) => panic!("transparent addresses not supported in compact blocks"),
1269+
Address::Transparent(_) | Address::TransparentSourceOnly(_) => {
1270+
panic!("transparent addresses not supported in compact blocks")
1271+
}
12701272
Address::Unified(ua) => {
12711273
// This is annoying to implement, because the protocol-aware UA type has no
12721274
// concept of ZIP 316 preference order.

zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
104104
idx)));
105105
}
106106
}
107-
Address::Transparent(_) => {
107+
Address::Transparent(_) | Address::TransparentSourceOnly(_) => {
108108
return Err(WalletMigrationError::CorruptedData(
109109
"Address field value decoded to a transparent address; should have been Sapling or unified.".to_string()));
110110
}
@@ -231,7 +231,9 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
231231
Address::Sapling(_) => {
232232
Ok(pool_code(PoolType::Shielded(ShieldedProtocol::Sapling)))
233233
}
234-
Address::Transparent(_) => Ok(pool_code(PoolType::Transparent)),
234+
Address::Transparent(_) | Address::TransparentSourceOnly(_) => {
235+
Ok(pool_code(PoolType::Transparent))
236+
}
235237
Address::Unified(_) => Err(WalletMigrationError::CorruptedData(
236238
"Unified addresses should not yet appear in the sent_notes table."
237239
.to_string(),

zcash_keys/src/address.rs

+22-1
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,22 @@ impl UnifiedAddress {
236236
/// An address that funds can be sent to.
237237
#[derive(Debug, PartialEq, Eq, Clone)]
238238
pub enum Address {
239+
/// A Sapling payment address.
239240
#[cfg(feature = "sapling")]
240241
Sapling(PaymentAddress),
242+
243+
/// A transparent address corresponding to either a public key or a `Script`.
241244
Transparent(TransparentAddress),
245+
246+
/// A [ZIP 316] Unified Address.
247+
///
248+
/// [ZIP 316]: https://zips.z.cash/zip-0316
242249
Unified(UnifiedAddress),
250+
251+
/// A [ZIP 320] transparent-source-only P2PKH address, or "TEX address".
252+
///
253+
/// [ZIP 320]: https://zips.z.cash/zip-0320
254+
TransparentSourceOnly([u8; 20]),
243255
}
244256

245257
#[cfg(feature = "sapling")]
@@ -287,6 +299,10 @@ impl TryFromRawAddress for Address {
287299
fn try_from_raw_transparent_p2sh(data: [u8; 20]) -> Result<Self, ConversionError<Self::Error>> {
288300
Ok(TransparentAddress::ScriptHash(data).into())
289301
}
302+
303+
fn try_from_raw_tex(data: [u8; 20]) -> Result<Self, ConversionError<Self::Error>> {
304+
Ok(Address::TransparentSourceOnly(data))
305+
}
290306
}
291307

292308
impl Address {
@@ -310,6 +326,7 @@ impl Address {
310326
}
311327
},
312328
Address::Unified(ua) => ua.to_address(net),
329+
Address::TransparentSourceOnly(data) => ZcashAddress::from_tex(net, *data),
313330
}
314331
.to_string()
315332
}
@@ -320,7 +337,9 @@ impl Address {
320337
Address::Sapling(_) => {
321338
matches!(pool_type, PoolType::Shielded(ShieldedProtocol::Sapling))
322339
}
323-
Address::Transparent(_) => matches!(pool_type, PoolType::Transparent),
340+
Address::Transparent(_) | Address::TransparentSourceOnly(_) => {
341+
matches!(pool_type, PoolType::Transparent)
342+
}
324343
Address::Unified(ua) => match pool_type {
325344
PoolType::Transparent => ua.transparent().is_some(),
326345
PoolType::Shielded(ShieldedProtocol::Sapling) => {
@@ -368,6 +387,7 @@ pub mod testing {
368387
arb_payment_address().prop_map(Address::Sapling),
369388
arb_transparent_addr().prop_map(Address::Transparent),
370389
arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified),
390+
proptest::array::uniform20(any::<u8>()).prop_map(Address::TransparentSourceOnly),
371391
]
372392
}
373393

@@ -376,6 +396,7 @@ pub mod testing {
376396
return prop_oneof![
377397
arb_transparent_addr().prop_map(Address::Transparent),
378398
arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified),
399+
proptest::array::uniform20(any::<u8>()).prop_map(Address::TransparentSourceOnly),
379400
];
380401
}
381402
}

0 commit comments

Comments
 (0)