Skip to content

Commit

Permalink
WIP: add support for transparent-source-only addresses.
Browse files Browse the repository at this point in the history
Signed-off-by: Daira-Emma Hopwood <[email protected]>
  • Loading branch information
daira committed Mar 12, 2024
1 parent a9aabb2 commit 4f54370
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 7 deletions.
3 changes: 3 additions & 0 deletions zcash_client_backend/src/data_api/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
}

Expand Down
153 changes: 152 additions & 1 deletion zcash_client_backend/src/data_api/wallet/input_selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -206,6 +212,8 @@ pub enum GreedyInputSelectorError<ChangeStrategyErrT, NoteRefT> {
Balance(BalanceError),
/// A unified address did not contain a supported receiver.
UnsupportedAddress(Box<UnifiedAddress>),
/// Support for transparent-source-only addresses requires the transparent-inputs feature.
UnsupportedTransparentSourceOnlyAddress,
/// An error was encountered in change selection.
Change(ChangeError<ChangeStrategyErrT, NoteRefT>),
}
Expand All @@ -223,6 +231,9 @@ impl<CE: fmt::Display, N: fmt::Display> 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)
}
Expand Down Expand Up @@ -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) => {
Expand All @@ -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));
Expand Down Expand Up @@ -386,6 +435,98 @@ where
}
}

// struct Zip320SecondStep {
// transaction_request: TransactionRequest,
// payment_pools: BTreeMap<usize, PoolType>,
// 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<ReceivedNote<DbT::NoteRef, Note>> = vec![];
let mut prior_available = NonNegativeAmount::ZERO;
let mut amount_required = NonNegativeAmount::ZERO;
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions zcash_client_backend/src/zip321.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -767,7 +769,7 @@ pub mod testing {
other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3),
) -> Payment {
let is_shielded = match recipient_address {
Address::Transparent(_) => false,
Address::Transparent(_) | Address::TransparentSourceOnly(_) => false,
Address::Sapling(_) | Address::Unified(_) => true,
};

Expand Down
4 changes: 3 additions & 1 deletion zcash_client_sqlite/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1266,7 +1266,9 @@ fn fake_compact_block_spending<P: consensus::Parameters, Fvk: TestFvk>(
)
.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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
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()));
}
Expand Down Expand Up @@ -231,7 +231,9 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
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(),
Expand Down
23 changes: 22 additions & 1 deletion zcash_keys/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -287,6 +299,10 @@ impl TryFromRawAddress for Address {
fn try_from_raw_transparent_p2sh(data: [u8; 20]) -> Result<Self, ConversionError<Self::Error>> {
Ok(TransparentAddress::ScriptHash(data).into())
}

fn try_from_raw_tex(data: [u8; 20]) -> Result<Self, ConversionError<Self::Error>> {
Ok(Address::TransparentSourceOnly(data))
}
}

impl Address {
Expand All @@ -310,6 +326,7 @@ impl Address {
}
},
Address::Unified(ua) => ua.to_address(net),
Address::TransparentSourceOnly(data) => ZcashAddress::from_tex(net, *data),
}
.to_string()
}
Expand All @@ -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) => {
Expand Down Expand Up @@ -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::<u8>()).prop_map(Address::TransparentSourceOnly),
]
}

Expand All @@ -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::<u8>()).prop_map(Address::TransparentSourceOnly),
];
}
}
Expand Down

0 comments on commit 4f54370

Please sign in to comment.