1use std::{
4 collections::{BTreeMap, BTreeSet},
5 fmt::{self, Debug, Display},
6};
7
8use nonempty::NonEmpty;
9use zcash_primitives::transaction::TxId;
10use zcash_protocol::{consensus::BlockHeight, value::Zatoshis, PoolType, ShieldedProtocol};
11use zip321::TransactionRequest;
12
13use crate::{
14 fees::TransactionBalance,
15 wallet::{Note, ReceivedNote, WalletTransparentOutput},
16};
17
18#[derive(Debug, Clone)]
20pub enum ProposalError {
21 RequestTotalInvalid,
23 Overflow,
25 BalanceError {
29 input_total: Zatoshis,
30 output_total: Zatoshis,
31 },
32 ShieldingInvalid,
38 AnchorNotFound(BlockHeight),
40 ReferenceError(StepOutput),
42 StepDoubleSpend(StepOutput),
44 ChainDoubleSpend(PoolType, TxId, u32),
46 PaymentPoolsMismatch,
49 SpendsChange(StepOutput),
51 #[cfg(feature = "transparent-inputs")]
53 EphemeralOutputLeftUnspent(StepOutput),
54 #[cfg(feature = "transparent-inputs")]
56 PaysTexFromShielded,
57 #[cfg(feature = "transparent-inputs")]
60 EphemeralOutputsInvalid,
61}
62
63impl Display for ProposalError {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 ProposalError::RequestTotalInvalid => write!(
67 f,
68 "The total requested output value is not a valid Zcash amount."
69 ),
70 ProposalError::Overflow => write!(
71 f,
72 "The total of transaction inputs overflows the valid range of Zcash values."
73 ),
74 ProposalError::BalanceError {
75 input_total,
76 output_total,
77 } => write!(
78 f,
79 "Balance error: the output total {} was not equal to the input total {}",
80 u64::from(*output_total),
81 u64::from(*input_total)
82 ),
83 ProposalError::ShieldingInvalid => write!(
84 f,
85 "The proposal violates the rules for a shielding transaction."
86 ),
87 ProposalError::AnchorNotFound(h) => {
88 write!(f, "Unable to compute anchor for block height {:?}", h)
89 }
90 ProposalError::ReferenceError(r) => {
91 write!(f, "No prior step output found for reference {:?}", r)
92 }
93 ProposalError::StepDoubleSpend(r) => write!(
94 f,
95 "The proposal uses the output of step {:?} in more than one place.",
96 r
97 ),
98 ProposalError::ChainDoubleSpend(pool, txid, index) => write!(
99 f,
100 "The proposal attempts to spend the same output twice: {}, {}, {}",
101 pool, txid, index
102 ),
103 ProposalError::PaymentPoolsMismatch => write!(
104 f,
105 "The chosen payment pools did not match the payments of the transaction request."
106 ),
107 ProposalError::SpendsChange(r) => write!(
108 f,
109 "The proposal attempts to spends the change output created at step {:?}.",
110 r,
111 ),
112 #[cfg(feature = "transparent-inputs")]
113 ProposalError::EphemeralOutputLeftUnspent(r) => write!(
114 f,
115 "The proposal created an ephemeral output at step {:?} that was not spent in any later step.",
116 r,
117 ),
118 #[cfg(feature = "transparent-inputs")]
119 ProposalError::PaysTexFromShielded => write!(
120 f,
121 "The proposal included a payment to a TEX address and a spend from a shielded input in the same step.",
122 ),
123 #[cfg(feature = "transparent-inputs")]
124 ProposalError::EphemeralOutputsInvalid => write!(
125 f,
126 "The proposal generator failed to correctly generate an ephemeral change output when needed for sending to a TEX address."
127 ),
128 }
129 }
130}
131
132impl std::error::Error for ProposalError {}
133
134#[derive(Clone, PartialEq, Eq)]
136pub struct ShieldedInputs<NoteRef> {
137 anchor_height: BlockHeight,
138 notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
139}
140
141impl<NoteRef> ShieldedInputs<NoteRef> {
142 pub fn from_parts(
144 anchor_height: BlockHeight,
145 notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
146 ) -> Self {
147 Self {
148 anchor_height,
149 notes,
150 }
151 }
152
153 pub fn anchor_height(&self) -> BlockHeight {
156 self.anchor_height
157 }
158
159 pub fn notes(&self) -> &NonEmpty<ReceivedNote<NoteRef, Note>> {
161 &self.notes
162 }
163}
164
165#[derive(Clone, PartialEq, Eq)]
171pub struct Proposal<FeeRuleT, NoteRef> {
172 fee_rule: FeeRuleT,
173 min_target_height: BlockHeight,
174 steps: NonEmpty<Step<NoteRef>>,
175}
176
177impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
178 pub fn multi_step(
189 fee_rule: FeeRuleT,
190 min_target_height: BlockHeight,
191 steps: NonEmpty<Step<NoteRef>>,
192 ) -> Result<Self, ProposalError> {
193 let mut consumed_chain_inputs: BTreeSet<(PoolType, TxId, u32)> = BTreeSet::new();
194 let mut consumed_prior_inputs: BTreeSet<StepOutput> = BTreeSet::new();
195
196 for (i, step) in steps.iter().enumerate() {
197 for prior_ref in step.prior_step_inputs() {
198 if prior_ref.step_index() >= i {
200 return Err(ProposalError::ReferenceError(*prior_ref));
201 }
202 let prior_step = &steps[prior_ref.step_index()];
204 match prior_ref.output_index() {
205 StepOutputIndex::Payment(idx) => {
206 if prior_step.transaction_request().payments().len() <= idx {
207 return Err(ProposalError::ReferenceError(*prior_ref));
208 }
209 }
210 StepOutputIndex::Change(idx) => {
211 if prior_step.balance().proposed_change().len() <= idx {
212 return Err(ProposalError::ReferenceError(*prior_ref));
213 }
214 }
215 }
216 if !consumed_prior_inputs.insert(*prior_ref) {
218 return Err(ProposalError::StepDoubleSpend(*prior_ref));
219 }
220 }
221
222 for t_out in step.transparent_inputs() {
223 let key = (
224 PoolType::TRANSPARENT,
225 TxId::from_bytes(*t_out.outpoint().hash()),
226 t_out.outpoint().n(),
227 );
228 if !consumed_chain_inputs.insert(key) {
229 return Err(ProposalError::ChainDoubleSpend(key.0, key.1, key.2));
230 }
231 }
232
233 for s_out in step.shielded_inputs().iter().flat_map(|i| i.notes().iter()) {
234 let key = (
235 match &s_out.note() {
236 Note::Sapling(_) => PoolType::SAPLING,
237 #[cfg(feature = "orchard")]
238 Note::Orchard(_) => PoolType::ORCHARD,
239 },
240 *s_out.txid(),
241 s_out.output_index().into(),
242 );
243 if !consumed_chain_inputs.insert(key) {
244 return Err(ProposalError::ChainDoubleSpend(key.0, key.1, key.2));
245 }
246 }
247 }
248
249 Ok(Self {
250 fee_rule,
251 min_target_height,
252 steps,
253 })
254 }
255
256 #[allow(clippy::too_many_arguments)]
273 pub fn single_step(
274 transaction_request: TransactionRequest,
275 payment_pools: BTreeMap<usize, PoolType>,
276 transparent_inputs: Vec<WalletTransparentOutput>,
277 shielded_inputs: Option<ShieldedInputs<NoteRef>>,
278 balance: TransactionBalance,
279 fee_rule: FeeRuleT,
280 min_target_height: BlockHeight,
281 is_shielding: bool,
282 ) -> Result<Self, ProposalError> {
283 Ok(Self {
284 fee_rule,
285 min_target_height,
286 steps: NonEmpty::singleton(Step::from_parts(
287 &[],
288 transaction_request,
289 payment_pools,
290 transparent_inputs,
291 shielded_inputs,
292 vec![],
293 balance,
294 is_shielding,
295 )?),
296 })
297 }
298
299 pub fn fee_rule(&self) -> &FeeRuleT {
301 &self.fee_rule
302 }
303
304 pub fn min_target_height(&self) -> BlockHeight {
309 self.min_target_height
310 }
311
312 pub fn steps(&self) -> &NonEmpty<Step<NoteRef>> {
315 &self.steps
316 }
317}
318
319impl<FeeRuleT: Debug, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
320 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321 f.debug_struct("Proposal")
322 .field("fee_rule", &self.fee_rule)
323 .field("min_target_height", &self.min_target_height)
324 .field("steps", &self.steps)
325 .finish()
326 }
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
331pub enum StepOutputIndex {
332 Payment(usize),
333 Change(usize),
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
338pub struct StepOutput {
339 step_index: usize,
340 output_index: StepOutputIndex,
341}
342
343impl StepOutput {
344 pub fn new(step_index: usize, output_index: StepOutputIndex) -> Self {
346 Self {
347 step_index,
348 output_index,
349 }
350 }
351
352 pub fn step_index(&self) -> usize {
354 self.step_index
355 }
356
357 pub fn output_index(&self) -> StepOutputIndex {
360 self.output_index
361 }
362}
363
364#[derive(Clone, PartialEq, Eq)]
366pub struct Step<NoteRef> {
367 transaction_request: TransactionRequest,
368 payment_pools: BTreeMap<usize, PoolType>,
369 transparent_inputs: Vec<WalletTransparentOutput>,
370 shielded_inputs: Option<ShieldedInputs<NoteRef>>,
371 prior_step_inputs: Vec<StepOutput>,
372 balance: TransactionBalance,
373 is_shielding: bool,
374}
375
376impl<NoteRef> Step<NoteRef> {
377 #[allow(clippy::too_many_arguments)]
396 pub fn from_parts(
397 prior_steps: &[Step<NoteRef>],
398 transaction_request: TransactionRequest,
399 payment_pools: BTreeMap<usize, PoolType>,
400 transparent_inputs: Vec<WalletTransparentOutput>,
401 shielded_inputs: Option<ShieldedInputs<NoteRef>>,
402 prior_step_inputs: Vec<StepOutput>,
403 balance: TransactionBalance,
404 is_shielding: bool,
405 ) -> Result<Self, ProposalError> {
406 if transaction_request.payments().len() != payment_pools.len() {
408 return Err(ProposalError::PaymentPoolsMismatch);
409 }
410 for (idx, pool) in &payment_pools {
411 if !transaction_request
412 .payments()
413 .get(idx)
414 .iter()
415 .any(|payment| payment.recipient_address().can_receive_as(*pool))
416 {
417 return Err(ProposalError::PaymentPoolsMismatch);
418 }
419 }
420
421 let transparent_input_total = transparent_inputs
422 .iter()
423 .map(|out| out.txout().value)
424 .try_fold(Zatoshis::ZERO, |acc, a| {
425 (acc + a).ok_or(ProposalError::Overflow)
426 })?;
427
428 let shielded_input_total = shielded_inputs
429 .iter()
430 .flat_map(|s_in| s_in.notes().iter())
431 .map(|out| out.note().value())
432 .try_fold(Zatoshis::ZERO, |acc, a| (acc + a))
433 .ok_or(ProposalError::Overflow)?;
434
435 let prior_step_input_total = prior_step_inputs
436 .iter()
437 .map(|s_ref| {
438 let step = prior_steps
439 .get(s_ref.step_index)
440 .ok_or(ProposalError::ReferenceError(*s_ref))?;
441 Ok(match s_ref.output_index {
442 StepOutputIndex::Payment(i) => step
443 .transaction_request
444 .payments()
445 .get(&i)
446 .ok_or(ProposalError::ReferenceError(*s_ref))?
447 .amount(),
448 StepOutputIndex::Change(i) => step
449 .balance
450 .proposed_change()
451 .get(i)
452 .ok_or(ProposalError::ReferenceError(*s_ref))?
453 .value(),
454 })
455 })
456 .collect::<Result<Vec<_>, _>>()?
457 .into_iter()
458 .try_fold(Zatoshis::ZERO, |acc, a| (acc + a))
459 .ok_or(ProposalError::Overflow)?;
460
461 let input_total = (transparent_input_total + shielded_input_total + prior_step_input_total)
462 .ok_or(ProposalError::Overflow)?;
463
464 let request_total = transaction_request
465 .total()
466 .map_err(|_| ProposalError::RequestTotalInvalid)?;
467 let output_total = (request_total + balance.total()).ok_or(ProposalError::Overflow)?;
468
469 if is_shielding
470 && (transparent_input_total == Zatoshis::ZERO
471 || shielded_input_total > Zatoshis::ZERO
472 || request_total > Zatoshis::ZERO)
473 {
474 return Err(ProposalError::ShieldingInvalid);
475 }
476
477 if input_total == output_total {
478 Ok(Self {
479 transaction_request,
480 payment_pools,
481 transparent_inputs,
482 shielded_inputs,
483 prior_step_inputs,
484 balance,
485 is_shielding,
486 })
487 } else {
488 Err(ProposalError::BalanceError {
489 input_total,
490 output_total,
491 })
492 }
493 }
494
495 pub fn transaction_request(&self) -> &TransactionRequest {
497 &self.transaction_request
498 }
499 pub fn payment_pools(&self) -> &BTreeMap<usize, PoolType> {
502 &self.payment_pools
503 }
504 pub fn transparent_inputs(&self) -> &[WalletTransparentOutput] {
506 &self.transparent_inputs
507 }
508 pub fn shielded_inputs(&self) -> Option<&ShieldedInputs<NoteRef>> {
510 self.shielded_inputs.as_ref()
511 }
512 pub fn prior_step_inputs(&self) -> &[StepOutput] {
515 self.prior_step_inputs.as_ref()
516 }
517 pub fn balance(&self) -> &TransactionBalance {
519 &self.balance
520 }
521 pub fn is_shielding(&self) -> bool {
525 self.is_shielding
526 }
527
528 pub fn involves(&self, pool_type: PoolType) -> bool {
530 let input_in_this_pool = || match pool_type {
531 PoolType::Transparent => self.is_shielding || !self.transparent_inputs.is_empty(),
532 PoolType::Shielded(ShieldedProtocol::Sapling) => {
533 self.shielded_inputs.iter().any(|s_in| {
534 s_in.notes()
535 .iter()
536 .any(|note| matches!(note.note(), Note::Sapling(_)))
537 })
538 }
539 #[cfg(feature = "orchard")]
540 PoolType::Shielded(ShieldedProtocol::Orchard) => {
541 self.shielded_inputs.iter().any(|s_in| {
542 s_in.notes()
543 .iter()
544 .any(|note| matches!(note.note(), Note::Orchard(_)))
545 })
546 }
547 #[cfg(not(feature = "orchard"))]
548 PoolType::Shielded(ShieldedProtocol::Orchard) => false,
549 };
550 let output_in_this_pool = || self.payment_pools().values().any(|pool| *pool == pool_type);
551 let change_in_this_pool = || {
552 self.balance
553 .proposed_change()
554 .iter()
555 .any(|c| c.output_pool() == pool_type)
556 };
557
558 input_in_this_pool() || output_in_this_pool() || change_in_this_pool()
559 }
560}
561
562impl<NoteRef> Debug for Step<NoteRef> {
563 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
564 f.debug_struct("Step")
565 .field("transaction_request", &self.transaction_request)
566 .field("transparent_inputs", &self.transparent_inputs)
567 .field(
568 "shielded_inputs",
569 &self.shielded_inputs().map(|i| i.notes.len()),
570 )
571 .field("prior_step_inputs", &self.prior_step_inputs)
572 .field(
573 "anchor_height",
574 &self.shielded_inputs().map(|i| i.anchor_height),
575 )
576 .field("balance", &self.balance)
577 .field("is_shielding", &self.is_shielding)
578 .finish_non_exhaustive()
579 }
580}