From 4f54370afa72c0b396cb9ca4e3c66e0a69affb89 Mon Sep 17 00:00:00 2001 From: Daira-Emma Hopwood Date: Tue, 12 Mar 2024 00:09:46 +0000 Subject: [PATCH] WIP: add support for transparent-source-only addresses. Signed-off-by: Daira-Emma Hopwood --- zcash_client_backend/src/data_api/wallet.rs | 3 + .../src/data_api/wallet/input_selection.rs | 153 +++++++++++++++++- zcash_client_backend/src/zip321.rs | 6 +- zcash_client_sqlite/src/testing.rs | 4 +- .../wallet/init/migrations/ufvk_support.rs | 6 +- zcash_keys/src/address.rs | 23 ++- 6 files changed, 188 insertions(+), 7 deletions(-) diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index c2bc244626..49de9aa461 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -1032,6 +1032,9 @@ where } transparent_output_meta.push((to, payment.amount)); } + Address::TransparentSourceOnly(_) => { + panic!("Transparent-source-only addresses should not occur at this stage"); + } } } diff --git a/zcash_client_backend/src/data_api/wallet/input_selection.rs b/zcash_client_backend/src/data_api/wallet/input_selection.rs index 1627698bab..3232bd58c0 100644 --- a/zcash_client_backend/src/data_api/wallet/input_selection.rs +++ b/zcash_client_backend/src/data_api/wallet/input_selection.rs @@ -31,7 +31,13 @@ use crate::{ #[cfg(feature = "transparent-inputs")] use { - std::collections::BTreeSet, std::convert::Infallible, + crate::{ + fees::TransactionBalance, + proposal::{StepOutput, StepOutputIndex}, + zip321::Payment, + }, + std::collections::BTreeSet, + std::convert::Infallible, zcash_primitives::legacy::TransparentAddress, zcash_primitives::transaction::components::OutPoint, }; @@ -206,6 +212,8 @@ pub enum GreedyInputSelectorError { Balance(BalanceError), /// A unified address did not contain a supported receiver. UnsupportedAddress(Box), + /// Support for transparent-source-only addresses requires the transparent-inputs feature. + UnsupportedTransparentSourceOnlyAddress, /// An error was encountered in change selection. Change(ChangeError), } @@ -223,6 +231,9 @@ impl fmt::Display for GreedyInputSelectorErro // don't have network parameters here write!(f, "Unified address contains no supported receivers.") } + GreedyInputSelectorError::UnsupportedTransparentSourceOnlyAddress => { + write!(f, "Support for transparent-source-only addresses requires the transparent-inputs feature.") + } GreedyInputSelectorError::Change(err) => { write!(f, "An error occurred computing change and fees: {}", err) } @@ -343,6 +354,22 @@ where #[cfg(feature = "orchard")] let mut orchard_outputs = vec![]; let mut payment_pools = BTreeMap::new(); + #[cfg(feature = "transparent-inputs")] + let mut ephemeral_outputs: Vec<(usize, Payment)> = vec![]; + + // TR - a + // - ephb + // - ephc + // - d + // + // becomes + // + // TR1 - a TR2 - b + // - eph-placeholder - c + // - d Proposal2 + // - spend from eph-placeholder + // + for (idx, payment) in transaction_request.payments() { match &payment.recipient_address { Address::Transparent(addr) => { @@ -352,6 +379,28 @@ where script_pubkey: addr.script(), }); } + #[cfg(feature = "transparent-inputs")] + Address::TransparentSourceOnly(data) => { + ephemeral_outputs.push(( + *idx, + Payment { + recipient_address: Address::Transparent( + TransparentAddress::PublicKeyHash(*data), + ), + amount: payment.amount, + memo: None, + label: payment.label.clone(), + message: payment.message.clone(), + other_params: payment.other_params.clone(), + }, + )); + } + #[cfg(not(feature = "transparent-inputs"))] + Address::TransparentSourceOnly(_) => { + return Err(InputSelectorError::Selection( + GreedyInputSelectorError::UnsupportedTransparentSourceOnlyAddress, + )); + } Address::Sapling(_) => { payment_pools.insert(*idx, PoolType::Shielded(ShieldedProtocol::Sapling)); sapling_outputs.push(SaplingPayment(payment.amount)); @@ -386,6 +435,98 @@ where } } + // struct Zip320SecondStep { + // transaction_request: TransactionRequest, + // payment_pools: BTreeMap, + // prior_step_inputs: StepOutput, + // balance: TransactionBalance, + // } + + // If ephemeral state was detected, handle it here. + #[cfg(feature = "transparent-inputs")] + let (transaction_request, second_step) = if ephemeral_outputs.is_empty() { + (transaction_request, None) + } else { + // Construct a new TransactionRequest from `transaction_request` that excludes + // the ephemeral outputs, and in their place includes a single transparent + // output to the legacy address for this account. + + let mut proposal1_excludes = BTreeSet::new(); + let mut proposal2_payments = vec![]; + let mut proposal2_payment_pools = BTreeMap::new(); + let mut total_ephemeral_plus_fee: NonNegativeAmount = (|| todo!())(); + let (first_idx, _) = ephemeral_outputs[0]; + for (idx2, (idx1, payment)) in ephemeral_outputs.into_iter().enumerate() { + total_ephemeral_plus_fee = (total_ephemeral_plus_fee + payment.amount).ok_or( + InputSelectorError::Selection(GreedyInputSelectorError::Balance( + BalanceError::Overflow, + )), + )?; + proposal1_excludes.insert(idx1); + proposal2_payments.push(payment); + proposal2_payment_pools.insert(idx2, PoolType::Transparent); + } + + // TODO: Calculate fee for transaction request 2 and add it to + // total_ephemeral_plus_fee. + + let proposal1_transaction_request = TransactionRequest::from_indexed( + transaction_request + .payments() + .iter() + .filter_map(|(idx, payment)| { + // If this is the first ephemeral output in the original request, + // replace it with a transparent output to the legacy address. + // We use the legacy address here only as a dummy address for the + // ZIP-321 URI serialized in the proposal. It will be replaced by + // a unique ephemeral P2PKH address when the proposal is created. + if *idx == first_idx { + let legacy_address: TransparentAddress = (|| todo!())(); + transparent_outputs.push(TxOut { + value: total_ephemeral_plus_fee, + script_pubkey: legacy_address.script(), + }); + payment_pools.insert(first_idx, PoolType::Transparent); + Some(( + first_idx, + Payment { + recipient_address: Address::Transparent(legacy_address), + amount: total_ephemeral_plus_fee, + memo: None, + label: None, + message: None, + // TODO: Add ZIP 320 ephemeral marker, or shift this + // into being a change output (which seems to need + // changes to the Proposal protobuf, as it doesn't + // explicitly store change, or have any way to mark + // the ephemeral output). + other_params: vec![], + }, + )) + } else if proposal1_excludes.contains(&idx) { + None + } else { + Some((*idx, payment.clone())) + } + }) + .collect(), + ) + .expect("correct by construction"); + let proposal2_transaction_request = + TransactionRequest::new(proposal2_payments).expect("correct by construction"); + + ( + proposal1_transaction_request, + Some(Zip320SecondStep { + transaction_request: proposal2_transaction_request, + payment_pools: proposal2_payment_pools, + // TODO: Update this depending on how the ephemeral output is represented. + prior_step_inputs: StepOutput::new(0, StepOutputIndex::Payment(first_idx)), + balance: (|| todo!())(), + }), + ) + }; + let mut shielded_inputs: Vec> = vec![]; let mut prior_available = NonNegativeAmount::ZERO; let mut amount_required = NonNegativeAmount::ZERO; @@ -432,6 +573,16 @@ where match balance { Ok(balance) => { + #[cfg(feature = "transparent-inputs")] + if let Some(zip320_step) = second_step { + return Proposal::multi_step( + (*self.change_strategy.fee_rule()).clone(), + target_height, + (|| todo!())(), + ) + .map_err(InputSelectorError::Proposal); + } + return Proposal::single_step( transaction_request, payment_pools, diff --git a/zcash_client_backend/src/zip321.rs b/zcash_client_backend/src/zip321.rs index bd020bb3e1..bbd91d1d7e 100644 --- a/zcash_client_backend/src/zip321.rs +++ b/zcash_client_backend/src/zip321.rs @@ -551,7 +551,9 @@ mod parse { Param::Amount(a) => payment.amount = a, Param::Memo(m) => match payment.recipient_address { Address::Sapling(_) | Address::Unified(_) => payment.memo = Some(m), - Address::Transparent(_) => return Err(Zip321Error::TransparentMemo(i)), + Address::Transparent(_) | Address::TransparentSourceOnly(_) => { + return Err(Zip321Error::TransparentMemo(i)) + } }, Param::Label(m) => payment.label = Some(m), @@ -767,7 +769,7 @@ pub mod testing { other_params in btree_map(VALID_PARAMNAME, any::(), 0..3), ) -> Payment { let is_shielded = match recipient_address { - Address::Transparent(_) => false, + Address::Transparent(_) | Address::TransparentSourceOnly(_) => false, Address::Sapling(_) | Address::Unified(_) => true, }; diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 8682e5449b..4cabc37fab 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -1266,7 +1266,9 @@ fn fake_compact_block_spending( ) .0, ), - Address::Transparent(_) => panic!("transparent addresses not supported in compact blocks"), + Address::Transparent(_) | Address::TransparentSourceOnly(_) => { + panic!("transparent addresses not supported in compact blocks") + } Address::Unified(ua) => { // This is annoying to implement, because the protocol-aware UA type has no // concept of ZIP 316 preference order. diff --git a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs index 88fd58af65..c44523cbe2 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs @@ -104,7 +104,7 @@ impl RusqliteMigration for Migration

{ idx))); } } - Address::Transparent(_) => { + Address::Transparent(_) | Address::TransparentSourceOnly(_) => { return Err(WalletMigrationError::CorruptedData( "Address field value decoded to a transparent address; should have been Sapling or unified.".to_string())); } @@ -231,7 +231,9 @@ impl RusqliteMigration for Migration

{ Address::Sapling(_) => { Ok(pool_code(PoolType::Shielded(ShieldedProtocol::Sapling))) } - Address::Transparent(_) => Ok(pool_code(PoolType::Transparent)), + Address::Transparent(_) | Address::TransparentSourceOnly(_) => { + Ok(pool_code(PoolType::Transparent)) + } Address::Unified(_) => Err(WalletMigrationError::CorruptedData( "Unified addresses should not yet appear in the sent_notes table." .to_string(), diff --git a/zcash_keys/src/address.rs b/zcash_keys/src/address.rs index dd1fb9ac43..9a5c6c8a7d 100644 --- a/zcash_keys/src/address.rs +++ b/zcash_keys/src/address.rs @@ -236,10 +236,22 @@ impl UnifiedAddress { /// An address that funds can be sent to. #[derive(Debug, PartialEq, Eq, Clone)] pub enum Address { + /// A Sapling payment address. #[cfg(feature = "sapling")] Sapling(PaymentAddress), + + /// A transparent address corresponding to either a public key or a `Script`. Transparent(TransparentAddress), + + /// A [ZIP 316] Unified Address. + /// + /// [ZIP 316]: https://zips.z.cash/zip-0316 Unified(UnifiedAddress), + + /// A [ZIP 320] transparent-source-only P2PKH address, or "TEX address". + /// + /// [ZIP 320]: https://zips.z.cash/zip-0320 + TransparentSourceOnly([u8; 20]), } #[cfg(feature = "sapling")] @@ -287,6 +299,10 @@ impl TryFromRawAddress for Address { fn try_from_raw_transparent_p2sh(data: [u8; 20]) -> Result> { Ok(TransparentAddress::ScriptHash(data).into()) } + + fn try_from_raw_tex(data: [u8; 20]) -> Result> { + Ok(Address::TransparentSourceOnly(data)) + } } impl Address { @@ -310,6 +326,7 @@ impl Address { } }, Address::Unified(ua) => ua.to_address(net), + Address::TransparentSourceOnly(data) => ZcashAddress::from_tex(net, *data), } .to_string() } @@ -320,7 +337,9 @@ impl Address { Address::Sapling(_) => { matches!(pool_type, PoolType::Shielded(ShieldedProtocol::Sapling)) } - Address::Transparent(_) => matches!(pool_type, PoolType::Transparent), + Address::Transparent(_) | Address::TransparentSourceOnly(_) => { + matches!(pool_type, PoolType::Transparent) + } Address::Unified(ua) => match pool_type { PoolType::Transparent => ua.transparent().is_some(), PoolType::Shielded(ShieldedProtocol::Sapling) => { @@ -368,6 +387,7 @@ pub mod testing { arb_payment_address().prop_map(Address::Sapling), arb_transparent_addr().prop_map(Address::Transparent), arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified), + proptest::array::uniform20(any::()).prop_map(Address::TransparentSourceOnly), ] } @@ -376,6 +396,7 @@ pub mod testing { return prop_oneof![ arb_transparent_addr().prop_map(Address::Transparent), arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified), + proptest::array::uniform20(any::()).prop_map(Address::TransparentSourceOnly), ]; } }