Skip to main content

zcash_client_backend/
proposal.rs

1//! Types related to the construction and evaluation of transaction proposals.
2
3use std::{
4    collections::{BTreeMap, BTreeSet},
5    fmt::{self, Debug, Display},
6};
7
8use nonempty::NonEmpty;
9#[cfg(feature = "orchard")]
10use zcash_primitives::transaction::components::orchard::{
11    ACTION_SIZE as ORCHARD_ACTION_SIZE, BUNDLE_OVERHEAD as ORCHARD_BUNDLE_OVERHEAD,
12    SPEND_AUTH_SIG_SIZE as ORCHARD_AUTH_SIG_SIZE,
13};
14use zcash_primitives::transaction::{
15    TxId, TxVersion,
16    components::sapling::{
17        BUNDLE_OVERHEAD as SAPLING_BUNDLE_OVERHEAD, OUTPUT_DESCRIPTION_SIZE, SPEND_DESCRIPTION_SIZE,
18    },
19    fees::transparent as transparent_fees,
20    fees::zip317::{
21        MAX_TRANSPARENT_INPUT_SIZE, P2PKH_STANDARD_INPUT_SIZE, P2PKH_STANDARD_OUTPUT_SIZE,
22        TRANSPARENT_BUNDLE_OVERHEAD, TX_HEADER_SIZE,
23    },
24};
25use zcash_protocol::{
26    PoolType, ShieldedPool,
27    consensus::{BlockHeight, BranchId},
28    constants::MAX_BLOCK_BYTES,
29    value::Zatoshis,
30};
31use zip321::{TransactionRequest, Zip321Error};
32#[cfg(feature = "orchard")]
33use {
34    orchard::Proof, zcash_primitives::transaction::builder::BundlePadding,
35    zcash_protocol::zip318::PoolMigrationConstants,
36};
37
38use crate::{
39    data_api::wallet::{ConfirmationsPolicy, TargetHeight},
40    fees::TransactionBalance,
41    wallet::{Note, OutputRef, ReceivedNote, WalletTransparentOutput},
42};
43
44/// Errors that can occur in construction of a [`Step`].
45#[derive(Debug, Clone)]
46#[non_exhaustive]
47pub enum ProposalError {
48    /// The total output value of the transaction request is not a valid Zcash amount.
49    RequestTotalInvalid,
50    /// The total of transaction inputs overflows the valid range of Zcash values.
51    Overflow,
52    /// The input total and output total of the payment request are not equal to one another. The
53    /// sum of transaction outputs, change, and fees is required to be exactly equal to the value
54    /// of provided inputs.
55    BalanceError {
56        input_total: Zatoshis,
57        output_total: Zatoshis,
58    },
59    /// The `is_shielding` flag may only be set to `true` under the following conditions:
60    /// * The total of transparent inputs is nonzero
61    /// * There exist no Sapling inputs
62    /// * There provided transaction request is empty; i.e. the only output values specified
63    ///   are change and fee amounts.
64    ShieldingInvalid,
65    /// No anchor information could be obtained for the specified block height.
66    AnchorNotFound(BlockHeight),
67    /// A proposal step produces a shielded bundle — it spends shielded notes, pays to a shielded
68    /// pool, or returns shielded change — but does not specify an anchor height against which its
69    /// shielded-tree lookups are performed. Only a purely transparent step may omit its anchor.
70    MissingShieldedAnchor,
71    /// A reference to the output of a prior step is invalid.
72    ReferenceError(StepOutput),
73    /// An attempted double-spend of a prior step output was detected.
74    StepDoubleSpend(StepOutput),
75    /// An attempted double-spend of an output belonging to the wallet was detected.
76    ChainDoubleSpend(OutputRef),
77    /// An input selected by the proposal could not be locked, because a concurrent
78    /// proposal or PCZT already holds a lock on it.
79    ///
80    /// This is a transient condition, not a defect in the proposal itself: the wrapped
81    /// output was spendable when it was selected, but another in-flight proposal locked
82    /// it first. Callers should treat this as "the account is busy" and retry proposal
83    /// creation; the retry will select around the locked output (or fail with an
84    /// insufficient-funds error if no other outputs are available).
85    InputsLocked(OutputRef),
86    /// There was a mismatch between the payments in the proposal's transaction request
87    /// and the payment pool selection values.
88    PaymentPoolsMismatch,
89    /// The proposal tried to spend a change output. Mark the `ChangeValue` as ephemeral if this is intended.
90    SpendsChange(StepOutput),
91    /// The proposal results in an invalid payment request according to ZIP-321.
92    Zip321(Zip321Error),
93    /// The ZIP 321 payment request at the wrapped index lacked payment amount information.
94    PaymentAmountMissing(usize),
95    /// A proposal step created an ephemeral output that was not spent in any later step.
96    #[cfg(feature = "transparent-inputs")]
97    EphemeralOutputLeftUnspent(StepOutput),
98    /// The proposal included a payment to a TEX address and a spend from a shielded input in the same step.
99    #[cfg(feature = "transparent-inputs")]
100    PaysTexFromShielded,
101    /// The change strategy provided to input selection failed to correctly generate an ephemeral
102    /// change output when needed for sending to a TEX address.
103    #[cfg(feature = "transparent-inputs")]
104    EphemeralOutputsInvalid,
105    /// The requested proposal would link activity on an ephemeral address to other wallet
106    /// activity.
107    #[cfg(feature = "transparent-inputs")]
108    EphemeralAddressLinkability,
109    /// A shielding proposal was constructed with a destination address that has no shielded
110    /// receiver. Shielding requires the destination to be able to receive shielded value.
111    #[cfg(feature = "transparent-inputs")]
112    ShieldingRequiresShieldedRecipient,
113    /// The transaction version requested is not compatible with the consensus branch for which the
114    /// transaction is intended.
115    IncompatibleTxVersion(BranchId),
116    /// After NU6.3 activation, a payment to an Orchard receiver must be delivered through the
117    /// Ironwood pool, which requires a version 6 transaction. The explicitly-requested transaction
118    /// version has no Ironwood bundle, so it cannot carry the payment. (The Orchard turnstile is a
119    /// consensus rule after NU6.3: no payment may add value to the Orchard pool, so such a payment
120    /// cannot be delivered as a plain Orchard output either.)
121    OrchardReceiverRequiresIronwood(TxVersion),
122    /// After Ironwood activation, a proposal step would create value in the Orchard pool.
123    /// The turnstile only permits value to leave the pool: a step may return change to it
124    /// only when strictly less value returns than the step's Orchard inputs remove.
125    /// (Payments may never be directed to the Orchard pool after Ironwood activation;
126    /// payment classification maintains that invariant, and step construction enforces it
127    /// via [`ProposalError::OrchardPoolPayment`].)
128    #[cfg(feature = "orchard")]
129    OrchardPoolValueCreation {
130        /// The total value of the Orchard notes spent by the step.
131        input_total: Zatoshis,
132        /// The total value of the Orchard-pool change outputs created by the step.
133        output_total: Zatoshis,
134    },
135    /// After Ironwood activation, the payment at the wrapped index of the step's
136    /// transaction request was directed to the Orchard pool. The turnstile only permits
137    /// value to leave the pool, so no payment may be directed to it; payment
138    /// classification routes Orchard-receiver payments to the Ironwood pool instead.
139    #[cfg(feature = "orchard")]
140    OrchardPoolPayment(usize),
141    /// The transaction the step describes would exceed the maximum size a Zcash block may
142    /// carry, and so cannot be included in any block. The serialized size of the step's
143    /// transaction was estimated from its inputs, outputs, and change, using conservative
144    /// per-element byte costs; the estimate exceeds [`MAX_BLOCK_BYTES`].
145    ///
146    /// `MAX_BLOCK_BYTES` is the *block* size limit, not a per-transaction limit: a
147    /// transaction that fits just under it still cannot be mined, because the block must
148    /// also carry its header and coinbase transaction. Passing the gate is therefore
149    /// necessary but not sufficient for mineability near the boundary; the builder is the
150    /// final authority.
151    ///
152    /// Recovery for a caller that receives this error: reduce the number of inputs by
153    /// lowering the payment amount or by consolidating small notes via send-to-self
154    /// transactions before retrying. Each consolidation round must be mined and its note
155    /// commitments anchored before the resulting notes are spendable, so recovery spans
156    /// multiple confirmations. For `propose_send_max` this error is a dead end — there is
157    /// no smaller amount to back off to — and requires a multi-transaction strategy.
158    ///
159    /// [`MAX_BLOCK_BYTES`]: zcash_protocol::constants::MAX_BLOCK_BYTES
160    TransactionTooLarge {
161        /// The conservative upper-bound estimate of the step's serialized byte size.
162        estimated_size: usize,
163        /// The maximum serialized byte size a Zcash block may carry (`MAX_BLOCK_BYTES`).
164        limit: usize,
165        /// The number of Sapling inputs the step spends, including prior-step inputs.
166        /// Carried so a wallet can explain the situation in terms of notes rather than
167        /// bytes; the different shielded pools have very different per-input costs.
168        sapling_input_count: usize,
169        /// The number of Orchard inputs the step spends, including prior-step inputs.
170        #[cfg(feature = "orchard")]
171        orchard_input_count: usize,
172        /// The number of Ironwood inputs the step spends, including prior-step inputs.
173        #[cfg(feature = "orchard")]
174        ironwood_input_count: usize,
175    },
176}
177
178impl Display for ProposalError {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        match self {
181            ProposalError::RequestTotalInvalid => write!(
182                f,
183                "The total requested output value is not a valid Zcash amount."
184            ),
185            ProposalError::Overflow => write!(
186                f,
187                "The total of transaction inputs overflows the valid range of Zcash values."
188            ),
189            ProposalError::BalanceError {
190                input_total,
191                output_total,
192            } => write!(
193                f,
194                "Balance error: the output total {} was not equal to the input total {}",
195                u64::from(*output_total),
196                u64::from(*input_total)
197            ),
198            ProposalError::ShieldingInvalid => write!(
199                f,
200                "The proposal violates the rules for a shielding transaction."
201            ),
202            ProposalError::AnchorNotFound(h) => {
203                write!(f, "Unable to compute anchor for block height {h:?}")
204            }
205            ProposalError::MissingShieldedAnchor => write!(
206                f,
207                "A proposal step that produces a shielded bundle must specify an anchor height."
208            ),
209            ProposalError::ReferenceError(r) => {
210                write!(f, "No prior step output found for reference {r:?}")
211            }
212            ProposalError::StepDoubleSpend(r) => write!(
213                f,
214                "The proposal uses the output of step {r:?} in more than one place."
215            ),
216            ProposalError::ChainDoubleSpend(output_ref) => write!(
217                f,
218                "The proposal attempts to spend the same output twice: {}, {}, {}",
219                output_ref.pool(),
220                output_ref.txid(),
221                output_ref.output_index()
222            ),
223            ProposalError::InputsLocked(output_ref) => write!(
224                f,
225                "A selected input is locked by a concurrent proposal (retry later): {}, {}, {}",
226                output_ref.pool(),
227                output_ref.txid(),
228                output_ref.output_index()
229            ),
230            ProposalError::PaymentPoolsMismatch => write!(
231                f,
232                "The chosen payment pools did not match the payments of the transaction request."
233            ),
234            ProposalError::SpendsChange(r) => write!(
235                f,
236                "The proposal attempts to spends the change output created at step {r:?}.",
237            ),
238            ProposalError::Zip321(r) => {
239                write!(f, "The proposal results in an invalid payment {r:?}.",)
240            }
241            ProposalError::PaymentAmountMissing(idx) => {
242                write!(
243                    f,
244                    "Payment amount not specified for requested payment at index {idx}."
245                )
246            }
247            #[cfg(feature = "transparent-inputs")]
248            ProposalError::EphemeralOutputLeftUnspent(r) => write!(
249                f,
250                "The proposal created an ephemeral output at step {r:?} that was not spent in any later step.",
251            ),
252            #[cfg(feature = "transparent-inputs")]
253            ProposalError::PaysTexFromShielded => write!(
254                f,
255                "The proposal included a payment to a TEX address and a spend from a shielded input in the same step.",
256            ),
257            #[cfg(feature = "transparent-inputs")]
258            ProposalError::EphemeralOutputsInvalid => write!(
259                f,
260                "The proposal generator failed to correctly generate an ephemeral change output when needed for sending to a TEX address."
261            ),
262            #[cfg(feature = "transparent-inputs")]
263            ProposalError::EphemeralAddressLinkability => write!(
264                f,
265                "The proposal requested spending funds in a way that would link activity on an ephemeral address to other wallet activity."
266            ),
267            #[cfg(feature = "transparent-inputs")]
268            ProposalError::ShieldingRequiresShieldedRecipient => write!(
269                f,
270                "A shielding proposal's destination must have a shielded receiver."
271            ),
272            #[cfg(feature = "orchard")]
273            ProposalError::OrchardPoolValueCreation {
274                input_total,
275                output_total,
276            } => write!(
277                f,
278                "After Ironwood activation, a step that spends {} zatoshis from the Orchard pool may not return {} zatoshis to it.",
279                u64::from(*input_total),
280                u64::from(*output_total),
281            ),
282            #[cfg(feature = "orchard")]
283            ProposalError::OrchardPoolPayment(index) => write!(
284                f,
285                "After Ironwood activation, no payment may be directed to the Orchard pool (payment index {index})."
286            ),
287            ProposalError::IncompatibleTxVersion(branch_id) => write!(
288                f,
289                "The requested transaction version is incompatible with consensus branch {branch_id:?}"
290            ),
291            ProposalError::OrchardReceiverRequiresIronwood(version) => write!(
292                f,
293                "After NU6.3 activation, a payment to an Orchard receiver requires a version 6 (Ironwood) transaction, but version {version:?} was requested."
294            ),
295            ProposalError::TransactionTooLarge {
296                estimated_size,
297                limit,
298                sapling_input_count,
299                #[cfg(feature = "orchard")]
300                orchard_input_count,
301                #[cfg(feature = "orchard")]
302                ironwood_input_count,
303            } => {
304                #[cfg(feature = "orchard")]
305                let _ = (orchard_input_count, ironwood_input_count);
306                write!(
307                    f,
308                    "The proposed transaction's estimated serialized size ({estimated_size} bytes) exceeds the maximum block size ({limit} bytes). It spends {sapling_input_count} Sapling inputs"
309                )?;
310                #[cfg(feature = "orchard")]
311                {
312                    write!(
313                        f,
314                        ", {orchard_input_count} Orchard inputs, {ironwood_input_count} Ironwood inputs"
315                    )?;
316                }
317                write!(
318                    f,
319                    "; reduce the payment amount or consolidate small notes before retrying."
320                )
321            }
322        }
323    }
324}
325
326impl std::error::Error for ProposalError {}
327
328/// The Sapling inputs to a proposed transaction.
329#[derive(Clone, PartialEq, Eq)]
330pub struct ShieldedInputs<NoteRef> {
331    notes: NonEmpty<ReceivedNote<NoteRef, Note>>,
332}
333
334impl<NoteRef> ShieldedInputs<NoteRef> {
335    /// Constructs a [`ShieldedInputs`] from its constituent parts.
336    pub fn from_parts(notes: NonEmpty<ReceivedNote<NoteRef, Note>>) -> Self {
337        Self { notes }
338    }
339
340    /// Returns the list of Sapling notes to be used as inputs to the proposed transaction.
341    pub fn notes(&self) -> &NonEmpty<ReceivedNote<NoteRef, Note>> {
342        &self.notes
343    }
344}
345
346/// A proposal for a series of transactions to be created.
347///
348/// Each step of the proposal represents a separate transaction to be created. At present, only
349/// transparent outputs of earlier steps may be spent in later steps; the ability to chain shielded
350/// transaction steps may be added in a future update.
351#[derive(Clone, PartialEq, Eq)]
352pub struct Proposal<FeeRuleT, NoteRef> {
353    fee_rule: FeeRuleT,
354    min_target_height: TargetHeight,
355    /// The confirmations policy under which the proposal was constructed. It is used to resolve
356    /// the anchor for a step that carries no explicit anchor height (a purely transparent step;
357    /// see [`Step::anchor_height`]).
358    confirmations_policy: ConfirmationsPolicy,
359    steps: NonEmpty<Step<NoteRef>>,
360    /// The transaction version explicitly requested when the proposal was constructed, if any.
361    /// When `None`, the transaction is built at the version implied by the target height (version 6
362    /// from NU6.3 onward).
363    proposed_version: Option<TxVersion>,
364}
365
366impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
367    /// Constructs a validated multi-step [`Proposal`].
368    ///
369    /// This operation validates the proposal for agreement between outputs and inputs
370    /// in the case of multi-step proposals, and ensures that no double-spends are being
371    /// proposed.
372    ///
373    /// Parameters:
374    /// * `fee_rule`: The fee rule observed by the proposed transaction.
375    /// * `min_target_height`: The minimum block height at which the transaction may be created.
376    /// * `steps`: A vector of steps that make up the proposal.
377    pub fn multi_step(
378        fee_rule: FeeRuleT,
379        min_target_height: TargetHeight,
380        confirmations_policy: ConfirmationsPolicy,
381        steps: NonEmpty<Step<NoteRef>>,
382    ) -> Result<Self, ProposalError> {
383        let mut consumed_chain_inputs: BTreeSet<OutputRef> = BTreeSet::new();
384        let mut consumed_prior_inputs: BTreeSet<StepOutput> = BTreeSet::new();
385
386        for (i, step) in steps.iter().enumerate() {
387            for prior_ref in step.prior_step_inputs() {
388                // check that there are no forward references
389                if prior_ref.step_index() >= i {
390                    return Err(ProposalError::ReferenceError(*prior_ref));
391                }
392                // check that the reference is valid
393                let prior_step = &steps[prior_ref.step_index()];
394                match prior_ref.output_index() {
395                    StepOutputIndex::Payment(idx) => {
396                        if prior_step.transaction_request().payments().len() <= idx {
397                            return Err(ProposalError::ReferenceError(*prior_ref));
398                        }
399                    }
400                    StepOutputIndex::Change(idx) => {
401                        if prior_step.balance().proposed_change().len() <= idx {
402                            return Err(ProposalError::ReferenceError(*prior_ref));
403                        }
404                    }
405                }
406                // check that there are no double-spends
407                if !consumed_prior_inputs.insert(*prior_ref) {
408                    return Err(ProposalError::StepDoubleSpend(*prior_ref));
409                }
410            }
411
412            for t_out in step.transparent_inputs() {
413                let output_ref = OutputRef::new(
414                    TxId::from_bytes(*t_out.outpoint().hash()),
415                    PoolType::TRANSPARENT,
416                    t_out.outpoint().n(),
417                );
418                if !consumed_chain_inputs.insert(output_ref) {
419                    return Err(ProposalError::ChainDoubleSpend(output_ref));
420                }
421            }
422
423            for s_out in step.shielded_inputs().iter().flat_map(|i| i.notes().iter()) {
424                let output_ref = OutputRef::new(
425                    *s_out.txid(),
426                    PoolType::Shielded(s_out.note().pool()),
427                    s_out.output_index().into(),
428                );
429                if !consumed_chain_inputs.insert(output_ref) {
430                    return Err(ProposalError::ChainDoubleSpend(output_ref));
431                }
432            }
433        }
434
435        Ok(Self {
436            fee_rule,
437            min_target_height,
438            confirmations_policy,
439            proposed_version: None,
440            steps,
441        })
442    }
443
444    /// Constructs a validated [`Proposal`] having only a single step from its constituent parts.
445    ///
446    /// This operation validates the proposal for balance consistency and agreement between
447    /// the `is_shielding` flag and the structure of the proposal.
448    ///
449    /// Parameters:
450    /// * `transaction_request`: The ZIP 321 transaction request describing the payments to be
451    ///   made.
452    /// * `payment_pools`: A map from payment index to pool type.
453    /// * `transparent_inputs`: The set of previous transparent outputs to be spent.
454    /// * `shielded_inputs`: The sets of previous shielded outputs to be spent.
455    /// * `anchor_height`: See [`Step::from_parts`].
456    /// * `balance`: The change outputs to be added the transaction and the fee to be paid.
457    /// * `fee_rule`: The fee rule observed by the proposed transaction.
458    /// * `min_target_height`: The minimum block height at which the transaction may be created.
459    /// * `is_shielding`: A flag that identifies whether this is a wallet-internal shielding
460    ///   transaction.
461    /// * `ironwood_active`: See [`Step::from_parts`].
462    #[allow(clippy::too_many_arguments)]
463    pub fn single_step(
464        transaction_request: TransactionRequest,
465        payment_pools: BTreeMap<usize, PoolType>,
466        transparent_inputs: Vec<WalletTransparentOutput<()>>,
467        shielded_inputs: Option<ShieldedInputs<NoteRef>>,
468        anchor_height: BlockHeight,
469        balance: TransactionBalance,
470        fee_rule: FeeRuleT,
471        min_target_height: TargetHeight,
472        confirmations_policy: ConfirmationsPolicy,
473        is_shielding: bool,
474        #[cfg(feature = "orchard")] ironwood_active: bool,
475    ) -> Result<Self, ProposalError> {
476        Ok(Self {
477            fee_rule,
478            min_target_height,
479            confirmations_policy,
480            proposed_version: None,
481            steps: NonEmpty::singleton(Step::from_parts(
482                &[],
483                transaction_request,
484                payment_pools,
485                transparent_inputs,
486                shielded_inputs,
487                Some(anchor_height),
488                vec![],
489                balance,
490                is_shielding,
491                #[cfg(feature = "orchard")]
492                ironwood_active,
493            )?),
494        })
495    }
496
497    /// Returns the fee rule to be used by the transaction builder.
498    pub fn fee_rule(&self) -> &FeeRuleT {
499        &self.fee_rule
500    }
501
502    /// Returns the target height for which the proposal was prepared.
503    ///
504    /// The chain must contain at least this many blocks in order for the proposal to
505    /// be executed.
506    pub fn min_target_height(&self) -> TargetHeight {
507        self.min_target_height
508    }
509
510    /// Returns the confirmations policy under which the proposal was constructed. It is used to
511    /// resolve the anchor for a step that carries no explicit anchor height (a purely transparent
512    /// step; see [`Step::anchor_height`]).
513    pub fn confirmations_policy(&self) -> ConfirmationsPolicy {
514        self.confirmations_policy
515    }
516
517    /// Returns the transaction version explicitly requested when the proposal was constructed, if
518    /// any. When `None`, the transaction is built at the version implied by the target height
519    /// (version 6 from NU6.3 onward).
520    pub fn proposed_version(&self) -> Option<TxVersion> {
521        self.proposed_version
522    }
523
524    /// Returns this proposal with its requested transaction version set to the given value.
525    ///
526    /// This records the version passed to proposal construction so that it is carried through to
527    /// transaction building; it does not re-validate the proposal against the version.
528    pub fn with_proposed_version(mut self, proposed_version: Option<TxVersion>) -> Self {
529        self.proposed_version = proposed_version;
530        self
531    }
532
533    /// Returns the steps of the proposal. Each step corresponds to an independent transaction to
534    /// be generated as a result of this proposal.
535    pub fn steps(&self) -> &NonEmpty<Step<NoteRef>> {
536        &self.steps
537    }
538
539    /// Returns the total number of inputs across all steps of this proposal that belong to the
540    /// given pool.
541    ///
542    /// For a shielded pool this is the number of spent notes of that pool (Ironwood notes are
543    /// counted for [`PoolType::IRONWOOD`], not [`PoolType::ORCHARD`]); for
544    /// [`PoolType::Transparent`] it is the number of transparent inputs. See
545    /// [`Step::input_count_in_pool`].
546    pub fn input_count_in_pool(&self, pool_type: PoolType) -> usize {
547        self.steps
548            .iter()
549            .map(|step| step.input_count_in_pool(pool_type))
550            .sum()
551    }
552
553    /// Returns a conservative upper-bound estimate of the total serialized byte size
554    /// of all transactions this proposal describes, by summing the per-step estimates
555    /// (see [`Step::estimated_serialized_size`]). Each step is estimated independently,
556    /// so the total is the sum of the per-step serialized sizes, not the size of a
557    /// single transaction.
558    ///
559    /// The estimate is conservative (it never under-counts). See
560    /// [`Step::estimated_serialized_size`] for the per-element costs and the
561    /// mineability caveat.
562    pub fn estimated_serialized_size(&self) -> usize {
563        let steps = self.steps();
564        steps
565            .iter()
566            .map(|step| {
567                step.estimated_serialized_size(&|s_ref| {
568                    Self::resolve_prior_output_pool(steps, s_ref)
569                })
570            })
571            .sum()
572    }
573
574    /// Checks whether every step's estimated serialized size fits within the maximum
575    /// a Zcash block may carry, returning [`ProposalError::TransactionTooLarge`] for
576    /// the first step that does not.
577    ///
578    /// This is called by the public `propose_transfer` / `propose_shielding` wrappers
579    /// and the `create_proposed_transactions` / `create_pczt_from_proposal` build
580    /// entry points, so it protects all input selectors (including third-party ones)
581    /// and all build paths. It is not called during proposal deserialization, so
582    /// previously-persisted proposals remain recoverable.
583    pub fn check_transaction_size(&self) -> Result<(), ProposalError> {
584        let steps = self.steps();
585        for step in steps.iter() {
586            let estimated_size = step
587                .estimated_serialized_size(&|s_ref| Self::resolve_prior_output_pool(steps, s_ref));
588            if estimated_size > MAX_BLOCK_BYTES {
589                // Count per-pool shielded inputs (both the step's own and prior-step)
590                // so the counts match what the estimate charged.
591                let sapling_input_count = step.input_count_in_pool(PoolType::SAPLING)
592                    + step
593                        .prior_step_inputs()
594                        .iter()
595                        .filter(|s_ref| {
596                            Self::resolve_prior_output_pool(steps, s_ref) == PoolType::SAPLING
597                        })
598                        .count();
599                #[cfg(feature = "orchard")]
600                let orchard_input_count = step.input_count_in_pool(PoolType::ORCHARD)
601                    + step
602                        .prior_step_inputs()
603                        .iter()
604                        .filter(|s_ref| {
605                            Self::resolve_prior_output_pool(steps, s_ref) == PoolType::ORCHARD
606                        })
607                        .count();
608                #[cfg(feature = "orchard")]
609                let ironwood_input_count = step.input_count_in_pool(PoolType::IRONWOOD)
610                    + step
611                        .prior_step_inputs()
612                        .iter()
613                        .filter(|s_ref| {
614                            Self::resolve_prior_output_pool(steps, s_ref) == PoolType::IRONWOOD
615                        })
616                        .count();
617                return Err(ProposalError::TransactionTooLarge {
618                    estimated_size,
619                    limit: MAX_BLOCK_BYTES,
620                    sapling_input_count,
621                    #[cfg(feature = "orchard")]
622                    orchard_input_count,
623                    #[cfg(feature = "orchard")]
624                    ironwood_input_count,
625                });
626            }
627        }
628        Ok(())
629    }
630
631    /// Resolves the pool of a prior-step output referenced by `s_ref`, by looking up
632    /// the referenced step in the proposal's step list. Shared by
633    /// [`estimated_serialized_size`](Self::estimated_serialized_size) and
634    /// [`check_transaction_size`](Self::check_transaction_size) to avoid duplicating the
635    /// `StepOutput`-to-`PoolType` resolution logic.
636    fn resolve_prior_output_pool(steps: &NonEmpty<Step<NoteRef>>, s_ref: &StepOutput) -> PoolType {
637        let prior = &steps[s_ref.step_index()];
638        match s_ref.output_index() {
639            StepOutputIndex::Payment(i) => *prior
640                .payment_pools()
641                .get(&i)
642                .expect("payment pool exists for referenced payment"),
643            StepOutputIndex::Change(i) => prior.balance().proposed_change()[i].output_pool(),
644        }
645    }
646}
647
648impl<FeeRuleT: Debug, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
649    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
650        f.debug_struct("Proposal")
651            .field("fee_rule", &self.fee_rule)
652            .field("min_target_height", &self.min_target_height)
653            .field("proposed_version", &self.proposed_version)
654            .field("steps", &self.steps)
655            .finish()
656    }
657}
658
659/// A reference to either a payment or change output within a step.
660#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
661pub enum StepOutputIndex {
662    Payment(usize),
663    Change(usize),
664}
665
666/// A reference to the output of a step in a proposal.
667#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
668pub struct StepOutput {
669    step_index: usize,
670    output_index: StepOutputIndex,
671}
672
673impl StepOutput {
674    /// Constructs a new [`StepOutput`] from its constituent parts.
675    pub fn new(step_index: usize, output_index: StepOutputIndex) -> Self {
676        Self {
677            step_index,
678            output_index,
679        }
680    }
681
682    /// Returns the step index to which this reference refers.
683    pub fn step_index(&self) -> usize {
684        self.step_index
685    }
686
687    /// Returns the identifier for the payment or change output within
688    /// the referenced step.
689    pub fn output_index(&self) -> StepOutputIndex {
690        self.output_index
691    }
692}
693
694/// The inputs to be consumed and outputs to be produced in a proposed transaction.
695#[derive(Clone, PartialEq, Eq)]
696pub struct Step<NoteRef> {
697    transaction_request: TransactionRequest,
698    payment_pools: BTreeMap<usize, PoolType>,
699    transparent_inputs: Vec<WalletTransparentOutput<()>>,
700    shielded_inputs: Option<ShieldedInputs<NoteRef>>,
701    /// The anchor height that binds every shielded-tree lookup performed while building this
702    /// step's transaction — both shielded-input witnesses and shielded-output anchors.
703    ///
704    /// This is `Some` for any step that produces a shielded bundle — one that spends shielded
705    /// notes, pays to a shielded pool, or returns shielded change. The anchor is selected from the
706    /// wallet's checkpoints at proposal construction time and applied to every shielded-tree
707    /// lookup, so a transaction with only routed shielded outputs (for example an Orchard-receiver
708    /// payment routed into the Ironwood bundle post-NU6.3) remains indistinguishable from one that
709    /// spends real shielded notes. Only a purely transparent step may carry `None`.
710    anchor_height: Option<BlockHeight>,
711    prior_step_inputs: Vec<StepOutput>,
712    balance: TransactionBalance,
713    is_shielding: bool,
714}
715
716/// Returns whether a step produces any shielded bundle — it spends shielded notes, pays to a
717/// shielded pool, or returns change to a shielded pool. Such a step performs shielded-tree lookups
718/// (including the dummy spends that pad an output-only bundle so it is indistinguishable from one
719/// that spends real notes), so it must bind a concrete anchor against which those lookups are made.
720pub(crate) fn produces_shielded_bundle(
721    has_shielded_inputs: bool,
722    payment_pools: &BTreeMap<usize, PoolType>,
723    balance: &TransactionBalance,
724) -> bool {
725    has_shielded_inputs
726        || payment_pools
727            .values()
728            .any(|pool| matches!(pool, PoolType::Shielded(_)))
729        || balance
730            .proposed_change()
731            .iter()
732            .any(|change| matches!(change.output_pool(), PoolType::Shielded(_)))
733}
734
735impl<NoteRef> Step<NoteRef> {
736    /// Constructs a validated [`Step`] from its constituent parts.
737    ///
738    /// This operation validates the proposal for balance consistency and agreement between
739    /// the `is_shielding` flag and the structure of the proposal.
740    ///
741    /// Parameters:
742    /// * `transaction_request`: The ZIP 321 transaction request describing the payments
743    ///   to be made.
744    /// * `payment_pools`: A map from payment index to pool type. The set of payment indices
745    ///   provided here must exactly match the set of payment indices in the [`TransactionRequest`],
746    ///   and the selected pool for an index must correspond to a valid receiver of the
747    ///   address at that index (or the address itself in the case of bare transparent or Sapling
748    ///   addresses).
749    /// * `transparent_inputs`: The set of previous transparent outputs to be spent.
750    /// * `shielded_inputs`: The sets of previous shielded outputs to be spent.
751    /// * `anchor_height`: The anchor height that binds every shielded-tree lookup performed
752    ///   while building this step's transaction — both shielded-input witnesses and
753    ///   shielded-output anchors. A step that produces a shielded bundle (spends shielded notes,
754    ///   pays to a shielded pool, or returns shielded change) must provide `Some`; only a purely
755    ///   transparent step may pass `None`.
756    /// * `balance`: The change outputs to be added the transaction and the fee to be paid.
757    /// * `is_shielding`: A flag that identifies whether this is a wallet-internal shielding
758    ///   transaction.
759    /// * `ironwood_active`: Whether the Ironwood pool is active at the target height for
760    ///   which this step is proposed. When active, the step is checked against the
761    ///   Orchard turnstile: no payment may be directed to the Orchard pool, and change
762    ///   may be returned to it only when strictly less value returns than the step's
763    ///   Orchard inputs remove.
764    #[allow(clippy::too_many_arguments)]
765    pub fn from_parts(
766        prior_steps: &[Step<NoteRef>],
767        transaction_request: TransactionRequest,
768        payment_pools: BTreeMap<usize, PoolType>,
769        transparent_inputs: Vec<WalletTransparentOutput<()>>,
770        shielded_inputs: Option<ShieldedInputs<NoteRef>>,
771        anchor_height: Option<BlockHeight>,
772        prior_step_inputs: Vec<StepOutput>,
773        balance: TransactionBalance,
774        is_shielding: bool,
775        #[cfg(feature = "orchard")] ironwood_active: bool,
776    ) -> Result<Self, ProposalError> {
777        // Verify that the set of payment pools matches exactly a set of valid payment recipients
778        if transaction_request.payments().len() != payment_pools.len() {
779            return Err(ProposalError::PaymentPoolsMismatch);
780        }
781        for (idx, pool) in &payment_pools {
782            if let Some(payment) = transaction_request.payments().get(idx) {
783                // Ironwood notes are Orchard-shaped and delivered to the recipient's Orchard
784                // receiver, so an Ironwood-pool payment is valid whenever the recipient can
785                // receive Orchard.
786                let deliverable = payment.recipient_address().can_receive_as(*pool)
787                    || (*pool == PoolType::IRONWOOD
788                        && payment
789                            .recipient_address()
790                            .can_receive_as(PoolType::ORCHARD));
791                if !deliverable {
792                    return Err(ProposalError::PaymentPoolsMismatch);
793                }
794                if payment.amount().is_none() {
795                    return Err(ProposalError::PaymentAmountMissing(*idx));
796                }
797            } else {
798                return Err(ProposalError::PaymentPoolsMismatch);
799            }
800        }
801
802        let transparent_input_total = transparent_inputs
803            .iter()
804            .map(|out| out.txout().value())
805            .try_fold(Zatoshis::ZERO, |acc, a| {
806                (acc + a).ok_or(ProposalError::Overflow)
807            })?;
808
809        let shielded_input_total = shielded_inputs
810            .iter()
811            .flat_map(|s_in| s_in.notes().iter())
812            .map(|out| out.note().value())
813            .try_fold(Zatoshis::ZERO, |acc, a| acc + a)
814            .ok_or(ProposalError::Overflow)?;
815
816        let prior_step_input_total = prior_step_inputs
817            .iter()
818            .map(|s_ref| {
819                let step = prior_steps
820                    .get(s_ref.step_index)
821                    .ok_or(ProposalError::ReferenceError(*s_ref))?;
822                Ok(match s_ref.output_index {
823                    StepOutputIndex::Payment(i) => step
824                        .transaction_request
825                        .payments()
826                        .get(&i)
827                        .ok_or(ProposalError::ReferenceError(*s_ref))?
828                        .amount()
829                        .ok_or(ProposalError::PaymentAmountMissing(i))?,
830                    StepOutputIndex::Change(i) => step
831                        .balance
832                        .proposed_change()
833                        .get(i)
834                        .ok_or(ProposalError::ReferenceError(*s_ref))?
835                        .value(),
836                })
837            })
838            .collect::<Result<Vec<_>, _>>()?
839            .into_iter()
840            .try_fold(Zatoshis::ZERO, |acc, a| acc + a)
841            .ok_or(ProposalError::Overflow)?;
842
843        let input_total = (transparent_input_total + shielded_input_total + prior_step_input_total)
844            .ok_or(ProposalError::Overflow)?;
845
846        let request_total = transaction_request
847            .total()
848            .map_err(|_| ProposalError::RequestTotalInvalid)?
849            .expect("all payments previously checked to have amount values");
850        let output_total = (request_total + balance.total()).ok_or(ProposalError::Overflow)?;
851
852        if is_shielding
853            && (transparent_input_total == Zatoshis::ZERO
854                || shielded_input_total > Zatoshis::ZERO
855                || request_total > Zatoshis::ZERO)
856        {
857            return Err(ProposalError::ShieldingInvalid);
858        }
859
860        // After Ironwood activation, the Orchard turnstile only permits value to leave
861        // the pool: payments may not be directed to the Orchard pool, and change may be
862        // returned to it only when strictly less value returns than the step's Orchard
863        // inputs remove.
864        #[cfg(feature = "orchard")]
865        if ironwood_active {
866            // With Ironwood active, payment classification routes every Orchard-receiver
867            // payment to the Ironwood pool before a step is constructed, so a payment
868            // directed to the Orchard pool here can only arise through direct misuse of
869            // this public constructor; reject it rather than construct a step whose
870            // transaction could never be mined. The only Orchard-pool outputs a step may
871            // create are change, which is validated below. The untrusted decode path
872            // rejects such a payment pool before calling `from_parts` (see
873            // `try_into_standard_proposal`).
874            if let Some((index, _)) = payment_pools
875                .iter()
876                .find(|(_, pool)| **pool == PoolType::ORCHARD)
877            {
878                return Err(ProposalError::OrchardPoolPayment(*index));
879            }
880
881            let orchard_input_total = shielded_inputs
882                .iter()
883                .flat_map(|s_in| s_in.notes().iter())
884                .filter(|n| n.note().pool() == ShieldedPool::Orchard)
885                .map(|n| n.note().value())
886                .try_fold(Zatoshis::ZERO, |acc, a| acc + a)
887                .ok_or(ProposalError::Overflow)?;
888
889            let orchard_change_total = balance
890                .proposed_change()
891                .iter()
892                .filter(|c| c.output_pool() == PoolType::ORCHARD)
893                .map(|c| c.value())
894                .try_fold(Zatoshis::ZERO, |acc, a| acc + a)
895                .ok_or(ProposalError::Overflow)?;
896
897            if orchard_change_total.is_positive() && orchard_change_total >= orchard_input_total {
898                return Err(ProposalError::OrchardPoolValueCreation {
899                    input_total: orchard_input_total,
900                    output_total: orchard_change_total,
901                });
902            }
903        }
904
905        // A step that produces any shielded bundle must bind a concrete anchor: every shielded-tree
906        // lookup it performs — including the dummy spends that pad an output-only bundle so it is
907        // indistinguishable from one that spends real notes — is made against that anchor. Only a
908        // purely transparent step may omit it. The untrusted decode path rejects this same
909        // combination at the parse boundary (see `try_into_standard_proposal`).
910        if anchor_height.is_none()
911            && produces_shielded_bundle(shielded_inputs.is_some(), &payment_pools, &balance)
912        {
913            return Err(ProposalError::MissingShieldedAnchor);
914        }
915
916        if input_total == output_total {
917            Ok(Self {
918                transaction_request,
919                payment_pools,
920                transparent_inputs,
921                shielded_inputs,
922                anchor_height,
923                prior_step_inputs,
924                balance,
925                is_shielding,
926            })
927        } else {
928            Err(ProposalError::BalanceError {
929                input_total,
930                output_total,
931            })
932        }
933    }
934
935    /// Returns the transaction request that describes the payments to be made.
936    pub fn transaction_request(&self) -> &TransactionRequest {
937        &self.transaction_request
938    }
939    /// Returns the map from payment index to the pool that has been selected
940    /// for the output that will fulfill that payment.
941    pub fn payment_pools(&self) -> &BTreeMap<usize, PoolType> {
942        &self.payment_pools
943    }
944    /// Returns the transparent inputs that have been selected to fund the transaction.
945    pub fn transparent_inputs(&self) -> &[WalletTransparentOutput<()>] {
946        &self.transparent_inputs
947    }
948    /// Returns the shielded inputs that have been selected to fund the transaction.
949    pub fn shielded_inputs(&self) -> Option<&ShieldedInputs<NoteRef>> {
950        self.shielded_inputs.as_ref()
951    }
952    /// Returns the anchor height that binds every shielded-tree lookup performed while building
953    /// this step's transaction, or `None` for a purely transparent step that performs no such
954    /// lookup.
955    pub fn anchor_height(&self) -> Option<BlockHeight> {
956        self.anchor_height
957    }
958    /// Returns the inputs that should be obtained from the outputs of the transaction
959    /// created to satisfy a previous step of the proposal.
960    pub fn prior_step_inputs(&self) -> &[StepOutput] {
961        self.prior_step_inputs.as_ref()
962    }
963    /// Returns the change outputs to be added to the transaction and the fee to be paid.
964    pub fn balance(&self) -> &TransactionBalance {
965        &self.balance
966    }
967    /// Returns a flag indicating whether or not the proposed transaction
968    /// is exclusively wallet-internal (if it does not involve any external
969    /// recipients).
970    pub fn is_shielding(&self) -> bool {
971        self.is_shielding
972    }
973
974    /// Returns whether or not this proposal requires interaction with the specified pool.
975    pub fn involves(&self, pool_type: PoolType) -> bool {
976        self.input_in_pool(pool_type)
977            || self.output_in_pool(pool_type)
978            || self.change_in_pool(pool_type)
979    }
980
981    /// Returns whether or not this step spends any inputs from the given pool.
982    ///
983    /// For a shielded pool this is true when a note of that protocol is spent; for
984    /// [`PoolType::Transparent`] it is true when the step is a shielding step or has any
985    /// transparent inputs.
986    pub fn input_in_pool(&self, pool_type: PoolType) -> bool {
987        match pool_type {
988            PoolType::Transparent => self.is_shielding() || !self.transparent_inputs().is_empty(),
989            PoolType::SAPLING => self.shielded_inputs().iter().any(|s_in| {
990                s_in.notes()
991                    .iter()
992                    .any(|note| matches!(note.note().pool(), ShieldedPool::Sapling))
993            }),
994            PoolType::ORCHARD => self.shielded_inputs().iter().any(|s_in| {
995                s_in.notes()
996                    .iter()
997                    .any(|note| matches!(note.note().pool(), ShieldedPool::Orchard))
998            }),
999            PoolType::IRONWOOD => self.shielded_inputs().iter().any(|s_in| {
1000                s_in.notes()
1001                    .iter()
1002                    .any(|note| matches!(note.note().pool(), ShieldedPool::Ironwood))
1003            }),
1004        }
1005    }
1006
1007    /// Returns whether or not this step directs any payment output to the given pool.
1008    ///
1009    /// This does not consider change outputs; use [`Step::change_in_pool`] for those.
1010    pub fn output_in_pool(&self, pool_type: PoolType) -> bool {
1011        self.payment_pools().values().any(|pool| *pool == pool_type)
1012    }
1013
1014    /// Returns whether or not this step directs any change output to the given pool.
1015    pub fn change_in_pool(&self, pool_type: PoolType) -> bool {
1016        self.balance()
1017            .proposed_change()
1018            .iter()
1019            .any(|c| c.output_pool() == pool_type)
1020    }
1021
1022    /// Returns the number of inputs to this step that belong to the given pool.
1023    ///
1024    /// For a shielded pool this is the number of spent notes of that protocol; for
1025    /// [`PoolType::Transparent`] it is the number of transparent inputs.
1026    pub fn input_count_in_pool(&self, pool_type: PoolType) -> usize {
1027        match pool_type {
1028            PoolType::Transparent => self.transparent_inputs().len(),
1029            PoolType::SAPLING => self
1030                .shielded_inputs()
1031                .iter()
1032                .flat_map(|s_in| s_in.notes())
1033                .filter(|note| note.note().pool() == ShieldedPool::Sapling)
1034                .count(),
1035            PoolType::ORCHARD => self
1036                .shielded_inputs()
1037                .iter()
1038                .flat_map(|s_in| s_in.notes())
1039                .filter(|note| note.note().pool() == ShieldedPool::Orchard)
1040                .count(),
1041            PoolType::IRONWOOD => self
1042                .shielded_inputs()
1043                .iter()
1044                .flat_map(|s_in| s_in.notes())
1045                .filter(|note| note.note().pool() == ShieldedPool::Ironwood)
1046                .count(),
1047        }
1048    }
1049
1050    /// Returns the number of payment outputs of this step that are directed to the given pool.
1051    ///
1052    /// This does not include change outputs; use [`Step::change_count_in_pool`] for those.
1053    pub fn output_count_in_pool(&self, pool_type: PoolType) -> usize {
1054        self.payment_pools()
1055            .values()
1056            .filter(|pool| **pool == pool_type)
1057            .count()
1058    }
1059
1060    /// Returns the number of change outputs of this step that are directed to the given pool.
1061    pub fn change_count_in_pool(&self, pool_type: PoolType) -> usize {
1062        self.balance()
1063            .proposed_change()
1064            .iter()
1065            .filter(|c| c.output_pool() == pool_type)
1066            .count()
1067    }
1068
1069    /// The value of this step's sole Ironwood PAYMENT output, or `None` when the step has any
1070    /// number of Ironwood payment outputs other than exactly one, or when that output's value is
1071    /// not determined (a ZIP 321 payment may carry no amount).
1072    ///
1073    /// Change is deliberately not considered. The fee model must reach the same verdict as
1074    /// [`Step::is_canonical_crossing`] from the same data, and at the point it decides an Ironwood
1075    /// change value is precisely the unknown it is solving for. That costs nothing: a step with
1076    /// Ironwood change alongside an Ironwood payment has two real Ironwood outputs, so its bundle
1077    /// already meets the default action floor and the padding makes no difference.
1078    #[cfg(feature = "orchard")]
1079    fn sole_ironwood_payment_value(&self) -> Option<Zatoshis> {
1080        let mut payments = self
1081            .payment_pools()
1082            .iter()
1083            .filter(|(_, pool)| **pool == PoolType::IRONWOOD)
1084            .map(|(index, _)| {
1085                self.transaction_request()
1086                    .payments()
1087                    .get(index)
1088                    .and_then(|payment| payment.amount())
1089            });
1090
1091        match (payments.next(), payments.next()) {
1092            (Some(value), None) => value,
1093            _ => None,
1094        }
1095    }
1096
1097    /// Returns whether this step is a *canonical crossing*: a step whose whole shape is
1098    /// indistinguishable from a [ZIP 318] migration transfer, and which is therefore built with a
1099    /// single unpadded Ironwood action so that it joins that anonymity set. See
1100    /// [`Step::ironwood_bundle_padding`].
1101    ///
1102    /// A migration transfer spends one Orchard note, pads its Orchard bundle to the default
1103    /// two-action floor (the spend plus its change, or a dummy when the note's value exactly
1104    /// covers the crossing and its fee), outputs one canonical denomination into the Ironwood
1105    /// pool, and is proved against a boundary of the anchor bucket grid. Every one of those is
1106    /// required here:
1107    ///
1108    /// - exactly one Orchard input and at most one Orchard change output, so that the Orchard
1109    ///   bundle is exactly two actions;
1110    /// - no change in any other pool;
1111    /// - no Ironwood spends, a step funded from Ironwood crossing nothing;
1112    /// - no Ironwood change, leaving a single Ironwood output;
1113    /// - that output's value is a canonical denomination under `params`;
1114    /// - the step's anchor lies on `params`' anchor bucket grid;
1115    /// - the fee equals `canonical_fee`, which the caller obtains from
1116    ///   [`fees::canonical_crossing_fee`] for the consensus parameters and target height the
1117    ///   transaction will actually be built against.
1118    ///
1119    /// [`fees::canonical_crossing_fee`]: crate::fees::canonical_crossing_fee
1120    ///
1121    /// Dropping the Ironwood padding without the rest would not buy anonymity but destroy it: an
1122    /// unpadded Ironwood bundle beside an Orchard bundle of the wrong size, or against a
1123    /// chain-tip anchor, is a shape nothing else on the network emits — a fingerprint rather than
1124    /// a disguise.
1125    ///
1126    /// [ZIP 318]: https://zips.z.cash/zip-0318
1127    #[cfg(feature = "orchard")]
1128    pub fn is_canonical_crossing<P: PoolMigrationConstants>(
1129        &self,
1130        params: &P,
1131        canonical_fee: Zatoshis,
1132    ) -> bool {
1133        self.input_count_in_pool(PoolType::ORCHARD) == 1
1134            && self.input_count_in_pool(PoolType::IRONWOOD) == 0
1135            && self.change_count_in_pool(PoolType::IRONWOOD) == 0
1136            // At most ONE Orchard change output. The Orchard bundle must be exactly two actions,
1137            // and from NU6.3 a spend and an output no longer share one, so its count is
1138            // `spends + outputs`: a second change output makes three. A multi-output change
1139            // strategy produces exactly that from a single large note.
1140            && self.change_count_in_pool(PoolType::ORCHARD) <= 1
1141            // Change anywhere else adds a bundle no migration transfer carries.
1142            && self.change_count_in_pool(PoolType::SAPLING) == 0
1143            && self.change_count_in_pool(PoolType::TRANSPARENT) == 0
1144            && self
1145                .sole_ironwood_payment_value()
1146                .is_some_and(|value| params.is_canonical_denomination(value))
1147            && self
1148                .anchor_height()
1149                .is_some_and(|anchor| params.anchor_grid().is_boundary(anchor))
1150            // ZIP 318 forbids a non-standard fee, which would partition the anonymity set as
1151            // surely as a non-standard shape. A `ChangeStrategy` may be built on any fee rule,
1152            // including a fixed non-standard one, so a structurally perfect crossing can still
1153            // carry a distinguishing fee.
1154            //
1155            // This condition is deliberately absent from `ironwood_bundle_padding`: the fee model
1156            // decides that padding WHILE computing the fee, so it cannot test the result against a
1157            // target. Keeping the fee here makes this predicate a gate on whether to keep a
1158            // proposal at all, rather than an input the builder must reproduce.
1159            && self.balance().fee_required() == canonical_fee
1160    }
1161
1162    /// The transactional bundle padding the transaction builder must use for this step's Ironwood
1163    /// bundle: the padding the [`ChangeStrategy`] recorded when it computed the fee.
1164    ///
1165    /// This is READ, not re-derived. The builder must produce exactly the action count the fee was
1166    /// charged against, and a second derivation could disagree with the first — which is precisely
1167    /// what happens for a condition only one of them can evaluate, such as whether the resulting
1168    /// fee is itself canonical.
1169    ///
1170    /// [`ChangeStrategy`]: crate::fees::ChangeStrategy
1171    #[cfg(feature = "orchard")]
1172    pub fn ironwood_bundle_padding(&self) -> BundlePadding {
1173        let Some(dummy_outputs) = self.balance().dummy_outputs() else {
1174            return BundlePadding::DEFAULT;
1175        };
1176
1177        let real_outputs = self.output_count_in_pool(PoolType::IRONWOOD)
1178            + self.change_count_in_pool(PoolType::IRONWOOD);
1179        let target_actions = real_outputs + dummy_outputs.ironwood();
1180
1181        // `pad_to_minimum` is only an 8-bit floor. If the requested transaction is larger than
1182        // that, its real spends and outputs already force the required action count, so no floor
1183        // is needed.
1184        u8::try_from(target_actions).map_or(BundlePadding::UNPADDED, |minimum| BundlePadding {
1185            bundle_required: target_actions > 0
1186                && self.input_count_in_pool(PoolType::IRONWOOD) == 0
1187                && real_outputs == 0,
1188            pad_to_minimum: Some(minimum.max(1)),
1189        })
1190    }
1191
1192    #[cfg(feature = "orchard")]
1193    fn orchard_style_action_count(
1194        &self,
1195        pool_type: PoolType,
1196        padding: BundlePadding,
1197        bundle_version: ::orchard::bundle::BundleVersion,
1198    ) -> Result<usize, &'static str> {
1199        crate::fees::orchard::transactional_action_count(
1200            padding.bundle_type(),
1201            bundle_version,
1202            self.input_count_in_pool(pool_type),
1203            self.output_count_in_pool(pool_type) + self.change_count_in_pool(pool_type),
1204        )
1205    }
1206
1207    /// Returns the number of actions the transaction builder will produce for this step's
1208    /// Orchard-pool bundle, given the bundle padding and version it will be configured with.
1209    ///
1210    /// The count depends upon the bundle version: prior to NU6.3, an action may carry both a
1211    /// spend and an output, so a bundle requires `max(spends, outputs)` actions; from NU6.3
1212    /// onwards the Orchard pool disables cross-address transfers, and each requested spend and
1213    /// output is instead paired with a fabricated zero-valued counterpart, so a bundle requires
1214    /// `spends + outputs` actions. The padding determines the floor applied on top of that count.
1215    ///
1216    /// The caller must pass the same padding and version the transaction builder will be
1217    /// configured with, otherwise the count will not match the bundle that is built.
1218    /// [`bundle_version_for_branch`] returns the version applicable to a given consensus branch
1219    /// and pool. The Orchard bundle is always padded to the default floor; only the Ironwood
1220    /// bundle's padding varies, and [`Step::ironwood_bundle_padding`] derives it.
1221    ///
1222    /// A step describes a wallet spend, which is never a coinbase transaction, so this takes a
1223    /// [`BundlePadding`] rather than a full bundle type: coinbase construction is a property of the
1224    /// whole transaction and is not representable here.
1225    ///
1226    /// # Errors
1227    ///
1228    /// Returns an error if this step's Orchard spend and output counts are incompatible with
1229    /// the given padding and version.
1230    ///
1231    /// [`bundle_version_for_branch`]: zcash_primitives::transaction::components::orchard::bundle_version_for_branch
1232    #[cfg(feature = "orchard")]
1233    pub fn orchard_action_count(
1234        &self,
1235        padding: BundlePadding,
1236        bundle_version: ::orchard::bundle::BundleVersion,
1237    ) -> Result<usize, &'static str> {
1238        self.orchard_style_action_count(PoolType::ORCHARD, padding, bundle_version)
1239    }
1240
1241    /// Returns the number of actions the transaction builder will produce for this step's
1242    /// Ironwood-pool bundle, given the bundle padding and version it will be configured with.
1243    ///
1244    /// See [`Step::orchard_action_count`] for how the count is determined; the Ironwood pool
1245    /// permits cross-address transfers, so an Ironwood bundle requires
1246    /// `max(spends, outputs)` actions before padding.
1247    ///
1248    /// # Errors
1249    ///
1250    /// Returns an error if this step's Ironwood spend and output counts are incompatible with
1251    /// the given padding and version.
1252    #[cfg(feature = "orchard")]
1253    pub fn ironwood_action_count(
1254        &self,
1255        padding: BundlePadding,
1256        bundle_version: ::orchard::bundle::BundleVersion,
1257    ) -> Result<usize, &'static str> {
1258        self.orchard_style_action_count(PoolType::IRONWOOD, padding, bundle_version)
1259    }
1260
1261    /// Returns a conservative upper-bound estimate of the serialized byte size of the
1262    /// transaction this step describes.
1263    ///
1264    /// `prior_steps` is the slice of all steps in the proposal (the same slice
1265    /// [`Step::from_parts`] receives), used to resolve the pool of each prior-step
1266    /// output this step spends. The caller — typically [`Proposal::check_transaction_size`]
1267    /// — passes the full proposal's step list so `step_index()` always resolves.
1268    ///
1269    /// The estimate sums the per-element byte costs of every input, output, and change
1270    /// output the step carries — including prior-step inputs and dummy outputs — plus
1271    /// the per-bundle overhead that each present bundle contributes (header fields,
1272    /// binding signatures, and the Orchard/Ironwood bundle proof). Each per-element cost
1273    /// is an upper bound:
1274    /// - Transparent inputs use their actual known serialized size (so P2SH inputs,
1275    ///   which can be larger than the P2PKH standard, are not under-counted), with the
1276    ///   consensus-maximum input size for inputs whose script size is unknown.
1277    /// - Sapling uses the v4 serialized form, which carries proofs and signatures
1278    ///   inline — a conservative upper bound, since v5 moves these to bundle-level
1279    ///   fields.
1280    /// - Orchard and Ironwood use [`ACTION_SIZE`] for each action description, plus
1281    ///   the per-action spend authorization signature and the per-action proof share,
1282    ///   computed via [`orchard::Proof::expected_proof_size`].
1283    ///
1284    /// The per-bundle overhead is added only when a bundle is present, so a
1285    /// purely-transparent transaction is not charged for Sapling or Orchard proof
1286    /// data it does not carry. Action counts account for both the default padding floor
1287    /// (`spends + outputs + 2`) and any dummy outputs the change strategy recorded via
1288    /// [`TransactionBalance::dummy_outputs`], taking the larger so a nonzero dummy count
1289    /// is never under-counted.
1290    ///
1291    /// The estimate is conservative (it never under-counts), so comparing it against
1292    /// [`MAX_BLOCK_BYTES`] is a safe size bound: a step whose estimate fits within a
1293    /// block will fit when built, and a step whose estimate exceeds a block may or may
1294    /// not fit, but rejecting the latter at proposal time prevents input selection from
1295    /// constructing a transaction that can never be mined. Passing the gate is necessary
1296    /// but not sufficient for mineability: `MAX_BLOCK_BYTES` is the *block* size limit,
1297    /// and a transaction that fits just under it still cannot be mined because the block
1298    /// must also carry its header and coinbase transaction. The builder is the final
1299    /// authority.
1300    ///
1301    /// [`ACTION_SIZE`]: zcash_primitives::transaction::components::orchard::ACTION_SIZE
1302    /// [`MAX_BLOCK_BYTES`]: zcash_protocol::constants::MAX_BLOCK_BYTES
1303    pub(crate) fn estimated_serialized_size(
1304        &self,
1305        resolve_prior_pool: &impl Fn(&StepOutput) -> PoolType,
1306    ) -> usize {
1307        // The maximum padding the default Orchard/Ironwood bundle type applies.
1308        #[cfg(feature = "orchard")]
1309        const ORCHARD_PADDING_ACTIONS: usize = 2;
1310
1311        // Single-pass tally of shielded inputs by pool, counting both the step's own
1312        // shielded inputs and prior-step inputs that spend shielded outputs.
1313        let mut sapling_spends = 0usize;
1314        let mut orchard_spends = 0usize;
1315        let mut ironwood_spends = 0usize;
1316
1317        if let Some(shielded) = self.shielded_inputs() {
1318            for note in shielded.notes().iter() {
1319                match note.note().pool() {
1320                    ShieldedPool::Sapling => sapling_spends += 1,
1321                    ShieldedPool::Orchard => orchard_spends += 1,
1322                    ShieldedPool::Ironwood => ironwood_spends += 1,
1323                }
1324            }
1325        }
1326
1327        // Prior-step inputs: each references an output of a preceding step. The closure
1328        // resolves the pool of the referenced output. For transparent Payment-sourced
1329        // prior inputs, use the consensus maximum input size (the recipient could be
1330        // P2SH with a large scriptSig); for Change-sourced transparent prior inputs,
1331        // use the P2PKH standard (ephemeral change outputs are wallet-internal P2PKH).
1332        let mut prior_transparent_input_bytes = 0usize;
1333        for s_ref in self.prior_step_inputs() {
1334            let pool = resolve_prior_pool(s_ref);
1335            match pool {
1336                PoolType::Transparent => {
1337                    let size = match s_ref.output_index() {
1338                        StepOutputIndex::Payment(_) => MAX_TRANSPARENT_INPUT_SIZE,
1339                        StepOutputIndex::Change(_) => P2PKH_STANDARD_INPUT_SIZE,
1340                    };
1341                    prior_transparent_input_bytes += size;
1342                }
1343                PoolType::SAPLING => sapling_spends += 1,
1344                PoolType::ORCHARD => orchard_spends += 1,
1345                PoolType::IRONWOOD => ironwood_spends += 1,
1346            }
1347        }
1348
1349        // Transparent inputs: use each input's actual serialized size where known, so
1350        // that P2SH inputs are not under-counted. For inputs whose script size is
1351        // unknown, use the consensus-maximum input size.
1352        let transparent_input_bytes = self
1353            .transparent_inputs()
1354            .iter()
1355            .map(
1356                |input| match transparent_fees::InputView::serialized_size(input) {
1357                    transparent_fees::InputSize::Known(size) => size,
1358                    transparent_fees::InputSize::Unknown(_) => MAX_TRANSPARENT_INPUT_SIZE,
1359                },
1360            )
1361            .sum::<usize>();
1362
1363        // Transparent outputs.
1364        let transparent_payment_outputs =
1365            self.output_count_in_pool(PoolType::Transparent) * P2PKH_STANDARD_OUTPUT_SIZE;
1366        let transparent_change_outputs =
1367            self.change_count_in_pool(PoolType::Transparent) * P2PKH_STANDARD_OUTPUT_SIZE;
1368        let transparent_present = !self.transparent_inputs().is_empty()
1369            || prior_transparent_input_bytes > 0
1370            || transparent_payment_outputs > 0
1371            || transparent_change_outputs > 0;
1372
1373        // Dummy outputs recorded by the change strategy. These pad the shielded bundles
1374        // beyond the default `spends + outputs + 2` floor; the estimate must charge for
1375        // them so a nonzero dummy count is never under-counted.
1376        let dummy = self.balance().dummy_outputs();
1377
1378        // Sapling spends and outputs. The default bundle type pads to at least two
1379        // outputs when there are any spends or outputs; adding 2 to each count covers
1380        // the padding floor. If the change strategy recorded dummy Sapling outputs,
1381        // charge for those too.
1382        let sapling_outputs = self.output_count_in_pool(PoolType::SAPLING)
1383            + self.change_count_in_pool(PoolType::SAPLING);
1384        let sapling_dummy = dummy.map_or(0, |d| d.sapling());
1385        let sapling_bundle_present = sapling_spends > 0 || sapling_outputs > 0 || sapling_dummy > 0;
1386        let sapling_spend_bytes = if sapling_bundle_present {
1387            (sapling_spends + 2) * SPEND_DESCRIPTION_SIZE
1388        } else {
1389            0
1390        };
1391        let sapling_output_bytes = if sapling_bundle_present {
1392            (sapling_outputs + 2 + sapling_dummy) * OUTPUT_DESCRIPTION_SIZE
1393        } else {
1394            0
1395        };
1396
1397        // Orchard and Ironwood bundles. Each action costs `ACTION_SIZE` plus the
1398        // per-action spend authorization signature; the bundle proof is
1399        // `Proof::expected_proof_size(num_actions)` (a fixed base plus a per-action
1400        // share), computed via the orchard crate's own `pub const fn`.
1401        #[cfg(feature = "orchard")]
1402        let orchard_bundle_bytes = orchard_style_bundle_bytes(
1403            orchard_spends,
1404            self.output_count_in_pool(PoolType::ORCHARD)
1405                + self.change_count_in_pool(PoolType::ORCHARD),
1406            dummy.map_or(0, |d| d.orchard()),
1407            ORCHARD_PADDING_ACTIONS,
1408        );
1409        #[cfg(not(feature = "orchard"))]
1410        let orchard_bundle_bytes: usize = 0;
1411        #[cfg(not(feature = "orchard"))]
1412        let _ = (orchard_spends, ironwood_spends);
1413
1414        #[cfg(feature = "orchard")]
1415        let ironwood_bundle_bytes = orchard_style_bundle_bytes(
1416            ironwood_spends,
1417            self.output_count_in_pool(PoolType::IRONWOOD)
1418                + self.change_count_in_pool(PoolType::IRONWOOD),
1419            dummy.map_or(0, |d| d.ironwood()),
1420            ORCHARD_PADDING_ACTIONS,
1421        );
1422        #[cfg(not(feature = "orchard"))]
1423        let ironwood_bundle_bytes: usize = 0;
1424
1425        TX_HEADER_SIZE
1426            + (transparent_present as usize) * TRANSPARENT_BUNDLE_OVERHEAD
1427            + transparent_input_bytes
1428            + prior_transparent_input_bytes
1429            + transparent_payment_outputs
1430            + transparent_change_outputs
1431            + (sapling_bundle_present as usize) * SAPLING_BUNDLE_OVERHEAD
1432            + sapling_spend_bytes
1433            + sapling_output_bytes
1434            + orchard_bundle_bytes
1435            + ironwood_bundle_bytes
1436    }
1437}
1438
1439/// Computes the estimated byte size of an Orchard-family bundle (Orchard or Ironwood)
1440/// for the given pool, given the number of shielded spends, real outputs, and dummy
1441/// outputs the step directs to that pool. The action count is
1442/// `max(spends + outputs + padding, outputs + dummy)`, so a nonzero dummy count is
1443/// never under-counted.
1444#[cfg(feature = "orchard")]
1445fn orchard_style_bundle_bytes(
1446    spends: usize,
1447    outputs: usize,
1448    dummy_outputs: usize,
1449    padding_actions: usize,
1450) -> usize {
1451    let default_count = spends + outputs + padding_actions;
1452    let dummy_count = outputs + dummy_outputs;
1453    let num_actions = default_count.max(dummy_count);
1454    if num_actions > 0 {
1455        ORCHARD_BUNDLE_OVERHEAD
1456            + Proof::expected_proof_size(num_actions)
1457            + num_actions * (ORCHARD_ACTION_SIZE + ORCHARD_AUTH_SIG_SIZE)
1458    } else {
1459        0
1460    }
1461}
1462
1463impl<NoteRef> Debug for Step<NoteRef> {
1464    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1465        f.debug_struct("Step")
1466            .field("transaction_request", &self.transaction_request)
1467            .field("transparent_inputs", &self.transparent_inputs)
1468            .field(
1469                "shielded_inputs",
1470                &self.shielded_inputs().map(|i| i.notes.len()),
1471            )
1472            .field("prior_step_inputs", &self.prior_step_inputs)
1473            .field("anchor_height", &self.anchor_height)
1474            .field("balance", &self.balance)
1475            .field("is_shielding", &self.is_shielding)
1476            .finish_non_exhaustive()
1477    }
1478}
1479
1480#[cfg(all(test, feature = "orchard"))]
1481mod tests {
1482    use std::collections::BTreeMap;
1483
1484    use zcash_keys::address::{Address, UnifiedAddress};
1485
1486    use incrementalmerkletree::Position;
1487    use nonempty::NonEmpty;
1488    use zcash_address::ZcashAddress;
1489    use zcash_primitives::transaction::{
1490        TxId,
1491        builder::BundlePadding,
1492        components::orchard::{ACTION_SIZE, bundle_version_for_branch},
1493    };
1494
1495    use crate::{
1496        data_api::{
1497            anchor_retention::{AnchorRetentionInterval, PoolMigrationParams},
1498            wallet::{ConfirmationsPolicy, TargetHeight},
1499        },
1500        fees::{ChangeValue, DummyOutputCounts, TransactionBalance},
1501        wallet::Note,
1502    };
1503    use zcash_protocol::{
1504        PoolType, ShieldedPool,
1505        consensus::{BlockHeight, BranchId, Network, NetworkUpgrade, Parameters},
1506        constants::MAX_BLOCK_BYTES,
1507        value::{COIN, Zatoshis},
1508        zip318::PoolMigrationConstants,
1509    };
1510    use zip321::{Payment, TransactionRequest};
1511
1512    use orchard::{
1513        ValuePool,
1514        bundle::BundleVersion,
1515        keys::{FullViewingKey, SpendingKey},
1516        note::{Note as OrchardNote, NoteVersion, RandomSeed, Rho},
1517        value::NoteValue,
1518    };
1519    use proptest::prelude::*;
1520
1521    use super::{Proposal, ProposalError, ShieldedInputs, Step};
1522
1523    // Builds an Orchard note of the given version and value. The recipient, rho, and rseed are
1524    // fixed; only the version and value vary, which is all `Note::pool`/`Note::protocol` depend on.
1525    fn orchard_note(value: u64, version: NoteVersion) -> Option<OrchardNote> {
1526        let sk: SpendingKey = Option::from(SpendingKey::from_bytes([0x2a; 32]))?;
1527        let recipient = FullViewingKey::from(&sk).address_at(0u32, zip32::Scope::External);
1528        let rho = Option::from(Rho::from_bytes(&[0; 32]))?;
1529        let rseed = Option::from(RandomSeed::from_bytes([0x1b; 32], &rho))?;
1530        Option::from(OrchardNote::from_parts(
1531            recipient,
1532            NoteValue::from_raw(value),
1533            rho,
1534            rseed,
1535            version,
1536        ))
1537    }
1538
1539    // Wraps a list of notes as the shielded inputs of a step.
1540    fn shielded_inputs_for(notes: Vec<Note>) -> Option<ShieldedInputs<u32>> {
1541        let received = notes
1542            .into_iter()
1543            .enumerate()
1544            .map(|(i, note)| {
1545                crate::wallet::ReceivedNote::from_parts(
1546                    i as u32,
1547                    TxId::from_bytes([0; 32]),
1548                    i as u16,
1549                    note,
1550                    zip32::Scope::External,
1551                    Position::from(i as u64),
1552                    Some(BlockHeight::from_u32(100)),
1553                    None,
1554                )
1555            })
1556            .collect::<Vec<_>>();
1557        NonEmpty::from_vec(received).map(ShieldedInputs::from_parts)
1558    }
1559
1560    // Wraps a list of notes into a single `Step` whose only inputs are those shielded notes.
1561    fn step_with_notes(notes: Vec<Note>) -> Step<u32> {
1562        step_with_notes_and_change(notes, vec![])
1563    }
1564
1565    // Wraps a list of notes into a single `Step` spending those shielded notes and returning the
1566    // given change outputs. The step is constructed directly rather than via `Step::from_parts`,
1567    // so the change values need not be covered by the input values.
1568    fn step_with_notes_and_change(notes: Vec<Note>, change: Vec<ChangeValue>) -> Step<u32> {
1569        let shielded_inputs = shielded_inputs_for(notes);
1570        Step {
1571            transaction_request: TransactionRequest::empty(),
1572            payment_pools: BTreeMap::new(),
1573            transparent_inputs: vec![],
1574            shielded_inputs,
1575            anchor_height: Some(BlockHeight::from_u32(100)),
1576            prior_step_inputs: vec![],
1577            balance: TransactionBalance::new(change, Zatoshis::ZERO).unwrap(),
1578            is_shielding: false,
1579        }
1580    }
1581
1582    // Constructs a validated step spending the given notes, with no payments.
1583    fn validated_step(
1584        notes: Vec<Note>,
1585        balance: TransactionBalance,
1586        ironwood_active: bool,
1587    ) -> Result<Step<u32>, ProposalError> {
1588        Step::from_parts(
1589            &[],
1590            TransactionRequest::empty(),
1591            BTreeMap::new(),
1592            vec![],
1593            shielded_inputs_for(notes),
1594            Some(BlockHeight::from_u32(100)),
1595            vec![],
1596            balance,
1597            false,
1598            ironwood_active,
1599        )
1600    }
1601
1602    /// The canonical crossing fee under the mainnet parameters these tests use.
1603    fn canonical_fee() -> Zatoshis {
1604        crate::fees::canonical_crossing_fee(
1605            &zcash_protocol::consensus::MAIN_NETWORK,
1606            BlockHeight::from_u32(2_000_000),
1607        )
1608        .expect("the canonical shape is a valid input to the ZIP 317 rule")
1609    }
1610
1611    /// The ZIP 318 parameters a wallet retaining the specified grid would report. Tests take their
1612    /// parameters from a wallet-shaped value, as production code does; there is deliberately no
1613    /// implementation of `PoolMigrationConstants` for a network type.
1614    fn zip318() -> PoolMigrationParams {
1615        PoolMigrationParams::new(
1616            zcash_protocol::zip318::BlockSpacing::UNIFORM,
1617            AnchorRetentionInterval::ZIP_318,
1618        )
1619    }
1620
1621    fn shielded_change(pool: ShieldedPool, value: u64) -> ChangeValue {
1622        ChangeValue::shielded(pool, Zatoshis::const_from_u64(value), None)
1623    }
1624
1625    // A step paying `value` into the Ironwood pool: one payment, routed to Ironwood, funded by
1626    // Orchard notes, with the given change. This is the shape of an ordinary post-NU6.3 payment
1627    // to an Orchard receiver, which is delivered through the Ironwood bundle.
1628    fn ironwood_payment_step(value: u64, change: Vec<ChangeValue>) -> Step<u32> {
1629        let recipient: ZcashAddress =
1630            "u1qpatys4zruk99pg59gcscrt7y6akvl9vrhcfyhm9yxvxz7h87q6n8cgrzzpe9zru68uq39uhmlpp5uefxu0su5uqyqfe5zp3tycn0ecl"
1631                .parse()
1632                .expect("a valid unified address");
1633        let request = TransactionRequest::new(vec![Payment::without_memo(
1634            recipient,
1635            Zatoshis::const_from_u64(value),
1636        )])
1637        .expect("a valid transaction request");
1638
1639        Step {
1640            transaction_request: request,
1641            payment_pools: BTreeMap::from([(0usize, PoolType::IRONWOOD)]),
1642            transparent_inputs: vec![],
1643            shielded_inputs: shielded_inputs_for(orchard_and_ironwood_notes(
1644                (1, value + 100_000),
1645                (0, 0),
1646            )),
1647            // A boundary of the ZIP 318 grid: a canonical crossing must be anchored to one.
1648            anchor_height: Some(BlockHeight::from_u32(144)),
1649            prior_step_inputs: vec![],
1650            balance: TransactionBalance::new(change, canonical_fee())
1651                .unwrap()
1652                .with_dummy_outputs(DummyOutputCounts::new(0, 0, 0)),
1653            is_shielding: false,
1654        }
1655    }
1656
1657    /// A crossing whose value is a canonical ZIP 318 denomination is built with a single unpadded
1658    /// Ironwood action, matching the shape of a migration transfer.
1659    #[test]
1660    fn canonical_crossing_is_unpadded() {
1661        for value in [COIN, COIN / 2, COIN / 100, 20 * COIN, 10_000 * COIN] {
1662            let step = ironwood_payment_step(value, vec![]);
1663            assert!(
1664                step.is_canonical_crossing(&zip318(), canonical_fee()),
1665                "{value} zatoshi should be a canonical crossing"
1666            );
1667            assert_eq!(step.ironwood_bundle_padding(), BundlePadding::UNPADDED);
1668            assert_eq!(
1669                step.ironwood_action_count(
1670                    step.ironwood_bundle_padding(),
1671                    BundleVersion::ironwood_v3()
1672                ),
1673                Ok(1)
1674            );
1675        }
1676    }
1677
1678    /// One zatoshi off a canonical denomination, or outside the ZIP's bounds, is not a canonical
1679    /// crossing: it stays padded, since it would join no migration anonymity set.
1680    #[test]
1681    fn near_canonical_crossing_stays_padded() {
1682        for value in [COIN + 1, COIN - 1, 3 * COIN, 100_000, 20_000 * COIN] {
1683            let step = ironwood_payment_step(value, vec![]);
1684            assert!(
1685                !step.is_canonical_crossing(&zip318(), canonical_fee()),
1686                "{value} zatoshi should not be a canonical crossing"
1687            );
1688            assert_eq!(
1689                step.ironwood_action_count(BundlePadding::DEFAULT, BundleVersion::ironwood_v3()),
1690                Ok(2)
1691            );
1692        }
1693    }
1694
1695    /// Ironwood change alongside a canonical payment is NOT a canonical crossing. The fee model
1696    /// cannot see a change value at the point it decides padding — that value is what it is
1697    /// solving for — so including change here would let the two disagree and break the builder's
1698    /// exact-balance check. It costs nothing: two real outputs already meet the default floor.
1699    #[test]
1700    fn ironwood_change_is_not_a_canonical_crossing() {
1701        let step = ironwood_payment_step(COIN, vec![shielded_change(ShieldedPool::Ironwood, 5000)]);
1702        assert!(!step.is_canonical_crossing(&zip318(), canonical_fee()));
1703    }
1704
1705    /// A payment funded from Ironwood notes crosses nothing, so it is never a canonical crossing
1706    /// however canonical its value. Its bundle has two real actions regardless.
1707    #[test]
1708    fn ironwood_funded_payment_is_not_a_canonical_crossing() {
1709        let mut step = ironwood_payment_step(COIN, vec![]);
1710        step.shielded_inputs =
1711            shielded_inputs_for(orchard_and_ironwood_notes((0, 0), (1, 2 * COIN)));
1712        assert!(!step.is_canonical_crossing(&zip318(), canonical_fee()));
1713    }
1714
1715    /// An anchor off the bucket grid is not a canonical crossing, however canonical the value and
1716    /// however migration-shaped the bundles. Unpadding here would produce an "unpadded Ironwood
1717    /// against a fresh anchor" shape that no migration transfer emits, which is a fingerprint
1718    /// rather than a disguise.
1719    #[test]
1720    fn unbucketed_anchor_is_not_a_canonical_crossing() {
1721        let mut step = ironwood_payment_step(COIN, vec![]);
1722        for height in [143u32, 145, 1, 2_000_000] {
1723            step.anchor_height = Some(BlockHeight::from_u32(height));
1724            assert!(
1725                !step.is_canonical_crossing(&zip318(), canonical_fee()),
1726                "anchor {height} is not a grid boundary"
1727            );
1728        }
1729        // The neighbouring boundaries are.
1730        for height in [144u32, 288, 1_999_872] {
1731            step.anchor_height = Some(BlockHeight::from_u32(height));
1732            assert!(step.is_canonical_crossing(&zip318(), canonical_fee()));
1733        }
1734    }
1735
1736    /// A migration transfer spends exactly one Orchard note. A payment drawing on more leaves an
1737    /// Orchard bundle of the wrong size, so unpadding its Ironwood bundle would not resemble a
1738    /// migration transfer.
1739    #[test]
1740    fn multiple_orchard_inputs_are_not_a_canonical_crossing() {
1741        let mut step = ironwood_payment_step(COIN, vec![]);
1742        assert!(step.is_canonical_crossing(&zip318(), canonical_fee()));
1743
1744        step.shielded_inputs = shielded_inputs_for(orchard_and_ironwood_notes((3, COIN), (0, 0)));
1745        assert!(!step.is_canonical_crossing(&zip318(), canonical_fee()));
1746    }
1747
1748    /// Overridden ZIP 318 parameters narrow which crossings are canonical, and the padding follows.
1749    #[test]
1750    fn canonical_crossing_respects_overridden_parameters() {
1751        #[derive(Clone)]
1752        struct SmallCap;
1753        impl PoolMigrationConstants for SmallCap {
1754            fn block_spacing(&self) -> zcash_protocol::zip318::BlockSpacing {
1755                zcash_protocol::zip318::BlockSpacing::UNIFORM
1756            }
1757
1758            fn denomination_cap(&self) -> Zatoshis {
1759                Zatoshis::const_from_u64(COIN)
1760            }
1761        }
1762
1763        let step = ironwood_payment_step(2 * COIN, vec![]);
1764        assert!(step.is_canonical_crossing(&zip318(), canonical_fee()));
1765        assert!(!step.is_canonical_crossing(&SmallCap, canonical_fee()));
1766    }
1767
1768    /// A step that produces any shielded bundle must bind a concrete anchor. Passing `None` (the
1769    /// decoded state of the wire-format zero sentinel) for such a step is rejected, whether the
1770    /// shielded bundle comes from spent notes or from shielded outputs. Only a purely transparent
1771    /// step may carry `None`.
1772    #[test]
1773    fn shielded_step_requires_anchor() {
1774        // A step that spends shielded notes with no anchor is rejected.
1775        assert_matches!(
1776            Step::from_parts(
1777                &[],
1778                TransactionRequest::empty(),
1779                BTreeMap::new(),
1780                vec![],
1781                shielded_inputs_for(orchard_and_ironwood_notes((1, 10_000), (0, 0))),
1782                None,
1783                vec![],
1784                TransactionBalance::new(vec![], Zatoshis::const_from_u64(10_000)).unwrap(),
1785                false,
1786                false,
1787            ),
1788            Err(ProposalError::MissingShieldedAnchor)
1789        );
1790
1791        // A step with a shielded output but no shielded inputs must also bind an anchor: the dummy
1792        // spends padding the output bundle commit to it.
1793        assert_matches!(
1794            Step::from_parts(
1795                &[],
1796                TransactionRequest::empty(),
1797                BTreeMap::new(),
1798                vec![],
1799                None::<ShieldedInputs<u32>>,
1800                None,
1801                vec![],
1802                TransactionBalance::new(
1803                    vec![shielded_change(ShieldedPool::Orchard, 10_000)],
1804                    Zatoshis::ZERO,
1805                )
1806                .unwrap(),
1807                false,
1808                false,
1809            ),
1810            Err(ProposalError::MissingShieldedAnchor)
1811        );
1812
1813        // A purely transparent step may carry no anchor.
1814        assert_matches!(
1815            Step::from_parts(
1816                &[],
1817                TransactionRequest::empty(),
1818                BTreeMap::new(),
1819                vec![],
1820                None::<ShieldedInputs<u32>>,
1821                None,
1822                vec![],
1823                TransactionBalance::new(vec![], Zatoshis::ZERO).unwrap(),
1824                false,
1825                false,
1826            ),
1827            Ok(step) if step.anchor_height().is_none()
1828        );
1829    }
1830
1831    /// Proposal construction conserves value: the total output value of a step (payments +
1832    /// change + fee) may never exceed its total input value. A step whose outputs exceed its
1833    /// inputs is rejected with [`ProposalError::BalanceError`] rather than being constructed.
1834    /// This is the value-conservation floor that pool-selection policy builds on: no selection
1835    /// of inputs can ever be assembled into a proposal that spends more than it takes in.
1836    #[test]
1837    fn proposal_construction_conserves_value() {
1838        // Inputs: one 10_000 Orchard note. Outputs: 8_000 change + 4_000 fee = 12_000, which
1839        // exceeds the 10_000 of input value. (ironwood_active = false so the balance check,
1840        // not the turnstile, is what rejects this.)
1841        assert_matches!(
1842            validated_step(
1843                orchard_and_ironwood_notes((1, 10_000), (0, 0)),
1844                TransactionBalance::new(
1845                    vec![shielded_change(ShieldedPool::Orchard, 8_000)],
1846                    Zatoshis::const_from_u64(4_000),
1847                )
1848                .unwrap(),
1849                false,
1850            ),
1851            Err(ProposalError::BalanceError {
1852                input_total,
1853                output_total,
1854            }) if input_total == Zatoshis::const_from_u64(10_000)
1855                && output_total == Zatoshis::const_from_u64(12_000)
1856        );
1857
1858        // The matching balanced step (6_000 change + 4_000 fee == 10_000 input) is accepted,
1859        // confirming the rejection above is due to the value imbalance and not some unrelated
1860        // constraint.
1861        assert_matches!(
1862            validated_step(
1863                orchard_and_ironwood_notes((1, 10_000), (0, 0)),
1864                TransactionBalance::new(
1865                    vec![shielded_change(ShieldedPool::Orchard, 6_000)],
1866                    Zatoshis::const_from_u64(4_000),
1867                )
1868                .unwrap(),
1869                false,
1870            ),
1871            Ok(_)
1872        );
1873    }
1874
1875    /// A proposal whose estimated serialized size exceeds the maximum a Zcash block may
1876    /// carry is rejected by `Proposal::check_transaction_size`, rather than being allowed
1877    /// to reach the builder.
1878    ///
1879    /// `Step::from_parts` accepts the step (it does not enforce the size gate), and
1880    /// `Proposal::check_transaction_size` rejects it — confirming the gate is in the
1881    /// proposal-level check, not the constructor, so the decode path is not broken.
1882    #[test]
1883    fn proposal_exceeding_the_block_size_limit_is_rejected() {
1884        let proof_per_action =
1885            orchard::Proof::expected_proof_size(2) - orchard::Proof::expected_proof_size(1);
1886        let full_action_cost = ACTION_SIZE + 64 + proof_per_action;
1887        let oversize_spend_count = MAX_BLOCK_BYTES / full_action_cost + 1;
1888
1889        let one_zato = Zatoshis::const_from_u64(1);
1890        let input_total = (one_zato * oversize_spend_count).unwrap();
1891        let change = ChangeValue::shielded(ShieldedPool::Orchard, input_total, None);
1892        let balance = TransactionBalance::new(vec![change], Zatoshis::ZERO).unwrap();
1893
1894        // `from_parts` accepts the step (no size gate).
1895        let step = validated_step(
1896            orchard_and_ironwood_notes((oversize_spend_count, 1), (0, 0)),
1897            balance,
1898            false,
1899        )
1900        .expect("from_parts accepts oversized steps");
1901
1902        let proposal = Proposal::multi_step(
1903            (),
1904            TargetHeight::from(100),
1905            ConfirmationsPolicy::default(),
1906            NonEmpty::singleton(step),
1907        )
1908        .expect("multi_step accepts a valid step");
1909
1910        // `check_transaction_size` rejects it.
1911        assert_matches!(
1912            proposal.check_transaction_size(),
1913            Err(ProposalError::TransactionTooLarge {
1914                estimated_size,
1915                limit,
1916                sapling_input_count,
1917                #[cfg(feature = "orchard")]
1918                orchard_input_count,
1919                #[cfg(feature = "orchard")]
1920                ironwood_input_count,
1921            }) if limit == MAX_BLOCK_BYTES
1922                && estimated_size > MAX_BLOCK_BYTES
1923                && sapling_input_count == 0
1924                && orchard_input_count == oversize_spend_count
1925                && ironwood_input_count == 0
1926        );
1927    }
1928
1929    /// A proposal whose estimated size fits within the block limit is accepted by
1930    /// `check_transaction_size`.
1931    #[test]
1932    fn proposal_fitting_within_the_block_size_limit_is_accepted() {
1933        let proof_per_action =
1934            orchard::Proof::expected_proof_size(2) - orchard::Proof::expected_proof_size(1);
1935        let full_action_cost = ACTION_SIZE + 64 + proof_per_action;
1936        let fitting_spend_count = MAX_BLOCK_BYTES / full_action_cost - 8;
1937
1938        let one_zato = Zatoshis::const_from_u64(1);
1939        let input_total = (one_zato * fitting_spend_count).unwrap();
1940        let change = ChangeValue::shielded(ShieldedPool::Orchard, input_total, None);
1941        let balance = TransactionBalance::new(vec![change], Zatoshis::ZERO).unwrap();
1942
1943        let step = validated_step(
1944            orchard_and_ironwood_notes((fitting_spend_count, 1), (0, 0)),
1945            balance,
1946            false,
1947        )
1948        .expect("from_parts accepts steps that fit");
1949
1950        let proposal = Proposal::multi_step(
1951            (),
1952            TargetHeight::from(100),
1953            ConfirmationsPolicy::default(),
1954            NonEmpty::singleton(step),
1955        )
1956        .expect("multi_step accepts a valid step");
1957
1958        assert_matches!(proposal.check_transaction_size(), Ok(()));
1959    }
1960
1961    #[test]
1962    fn orchard_turnstile_permits_only_strict_pool_balance_decrease() {
1963        // Post-activation, change may return to Orchard when strictly less value returns
1964        // than the step's Orchard inputs remove: 6_000 change < 10_000 input.
1965        assert_matches!(
1966            validated_step(
1967                orchard_and_ironwood_notes((1, 10_000), (0, 0)),
1968                TransactionBalance::new(
1969                    vec![shielded_change(ShieldedPool::Orchard, 6_000)],
1970                    Zatoshis::const_from_u64(4_000),
1971                )
1972                .unwrap(),
1973                true,
1974            ),
1975            Ok(_)
1976        );
1977
1978        // Post-activation, Orchard change equal to the Orchard input total would leave the
1979        // pool balance unchanged, which the turnstile forbids.
1980        assert_matches!(
1981            validated_step(
1982                orchard_and_ironwood_notes((1, 10_000), (1, 20_000)),
1983                TransactionBalance::new(
1984                    vec![
1985                        shielded_change(ShieldedPool::Orchard, 10_000),
1986                        shielded_change(ShieldedPool::Ironwood, 16_000),
1987                    ],
1988                    Zatoshis::const_from_u64(4_000),
1989                )
1990                .unwrap(),
1991                true,
1992            ),
1993            Err(ProposalError::OrchardPoolValueCreation {
1994                input_total,
1995                output_total,
1996            }) if input_total == Zatoshis::const_from_u64(10_000)
1997                && output_total == Zatoshis::const_from_u64(10_000)
1998        );
1999
2000        // Post-activation, a step that spends no Orchard notes may not create Orchard
2001        // change at all.
2002        assert_matches!(
2003            validated_step(
2004                orchard_and_ironwood_notes((0, 0), (1, 20_000)),
2005                TransactionBalance::new(
2006                    vec![shielded_change(ShieldedPool::Orchard, 16_000)],
2007                    Zatoshis::const_from_u64(4_000),
2008                )
2009                .unwrap(),
2010                true,
2011            ),
2012            Err(ProposalError::OrchardPoolValueCreation {
2013                input_total,
2014                output_total,
2015            }) if input_total == Zatoshis::ZERO
2016                && output_total == Zatoshis::const_from_u64(16_000)
2017        );
2018
2019        // Before activation, value may freely enter the Orchard pool: the same step is
2020        // valid.
2021        assert_matches!(
2022            validated_step(
2023                orchard_and_ironwood_notes((0, 0), (1, 20_000)),
2024                TransactionBalance::new(
2025                    vec![shielded_change(ShieldedPool::Orchard, 16_000)],
2026                    Zatoshis::const_from_u64(4_000),
2027                )
2028                .unwrap(),
2029                false,
2030            ),
2031            Ok(_)
2032        );
2033    }
2034
2035    // Constructs a step that spends a 10_000-zatoshi Orchard note to pay 6_000 zatoshis to
2036    // an Orchard receiver, with the payment assigned to the given pool.
2037    fn orchard_payment_step(
2038        pool: PoolType,
2039        ironwood_active: bool,
2040    ) -> Result<Step<u32>, ProposalError> {
2041        let sk: SpendingKey = Option::from(SpendingKey::from_bytes([0x2a; 32])).unwrap();
2042        let recipient = FullViewingKey::from(&sk).address_at(0u32, zip32::Scope::External);
2043        let ua = UnifiedAddress::from_receivers(Some(recipient), None, None, None, None).unwrap();
2044        let to = Address::Unified(Box::new(ua)).to_zcash_address(&Network::TestNetwork);
2045
2046        let request = TransactionRequest::new(vec![
2047            zip321::Payment::new(
2048                to,
2049                Some(Zatoshis::const_from_u64(6_000)),
2050                None,
2051                None,
2052                None,
2053                vec![],
2054            )
2055            .unwrap(),
2056        ])
2057        .unwrap();
2058
2059        Step::from_parts(
2060            &[],
2061            request,
2062            BTreeMap::from([(0usize, pool)]),
2063            vec![],
2064            shielded_inputs_for(orchard_and_ironwood_notes((1, 10_000), (0, 0))),
2065            Some(BlockHeight::from_u32(100)),
2066            vec![],
2067            TransactionBalance::new(vec![], Zatoshis::const_from_u64(4_000)).unwrap(),
2068            false,
2069            ironwood_active,
2070        )
2071    }
2072
2073    #[test]
2074    fn orchard_turnstile_permits_routed_and_pre_activation_payments() {
2075        // A payment routed through the Ironwood bundle (the post-activation representation
2076        // of an Orchard-receiver payment) is valid.
2077        assert_matches!(orchard_payment_step(PoolType::IRONWOOD, true), Ok(_));
2078
2079        // Before activation, an Orchard-pool payment is valid.
2080        assert_matches!(orchard_payment_step(PoolType::ORCHARD, false), Ok(_));
2081    }
2082
2083    // Post-activation, payment classification never assigns a payment to the Orchard pool,
2084    // so a step constructed with one can only arise through direct misuse of the public
2085    // constructor, which rejects it.
2086    #[test]
2087    fn orchard_pool_payment_with_ironwood_active_is_an_error() {
2088        assert_matches!(
2089            orchard_payment_step(PoolType::ORCHARD, true),
2090            Err(ProposalError::OrchardPoolPayment(0))
2091        );
2092    }
2093
2094    // Builds `orchard.0` version-2 (Orchard) notes of value `orchard.1`, followed by `ironwood.0`
2095    // version-3 (Ironwood) notes of value `ironwood.1`.
2096    //
2097    // Every note of a given pool is identical, so each is derived once and cloned: deriving a full
2098    // viewing key per note makes the large-transaction cases below cost seconds rather than
2099    // milliseconds. Only the count and the pool of each note matter to an action count.
2100    fn orchard_and_ironwood_notes(
2101        (n, orchard_value): (usize, u64),
2102        (m, ironwood_value): (usize, u64),
2103    ) -> Vec<Note> {
2104        let mut notes = Vec::with_capacity(n + m);
2105        if n > 0 {
2106            let note = Note::Orchard {
2107                note: orchard_note(orchard_value, NoteVersion::V2).unwrap(),
2108                pool: ValuePool::Orchard,
2109            };
2110            notes.extend(std::iter::repeat_n(note, n));
2111        }
2112        if m > 0 {
2113            let note = Note::Orchard {
2114                note: orchard_note(ironwood_value, NoteVersion::V3).unwrap(),
2115                pool: ValuePool::Ironwood,
2116            };
2117            notes.extend(std::iter::repeat_n(note, m));
2118        }
2119        notes
2120    }
2121
2122    // Note and change values, spanning a zero-valued note through a large one. An action count is a
2123    // function of how many notes and outputs a step has in each pool, never of what they are worth,
2124    // so every value here must give the same answer. The upper bound keeps the total value of even
2125    // the largest generated step well inside `MAX_MONEY`, so that `TransactionBalance` construction
2126    // cannot fail for a reason these tests are not about.
2127    fn arb_note_value() -> impl Strategy<Value = u64> {
2128        prop_oneof![
2129            1 => Just(0u64),
2130            8 => 1u64..1_000_000_000,
2131        ]
2132    }
2133
2134    // An upper bound on the actions a single bundle can contain on-chain: a transaction may not
2135    // exceed a block, and each action costs at least its action description. The true ceiling is
2136    // lower, since `ACTION_SIZE` excludes each action's spend authorization signature and the
2137    // bundle's proof, but an over-estimate is what these tests want: it exercises counts at least
2138    // as large as anything buildable.
2139    const MAX_ACTIONS_PER_BUNDLE: usize = MAX_BLOCK_BYTES / ACTION_SIZE;
2140
2141    // Spend and output counts spanning what a wallet can actually produce: ordinary payments, large
2142    // note-consolidation transactions, and bundles at the block ceiling. Each side is capped at
2143    // half the ceiling, because from NU6.3 an Orchard spend and an output no longer share an
2144    // action, so `spends + outputs` must itself fit. Small counts are weighted heavily: that is
2145    // where the padding floor binds, and where real transactions live.
2146    fn arb_note_count() -> impl Strategy<Value = usize> {
2147        prop_oneof![
2148            6 => 0usize..8,
2149            2 => 8usize..100,
2150            1 => 100usize..=MAX_ACTIONS_PER_BUNDLE / 2,
2151        ]
2152    }
2153
2154    // The network upgrades at which the Orchard pool exists but still permits cross-address
2155    // transfers, so that a requested spend and a requested output may share an action.
2156    fn arb_pre_nu6_3_upgrade() -> impl Strategy<Value = NetworkUpgrade> {
2157        prop::sample::select(vec![
2158            NetworkUpgrade::Nu5,
2159            NetworkUpgrade::Nu6,
2160            NetworkUpgrade::Nu6_1,
2161            NetworkUpgrade::Nu6_2,
2162        ])
2163    }
2164
2165    // The network upgrades from which the Orchard pool disables cross-address transfers.
2166    fn arb_nu6_3_or_later_upgrade() -> impl Strategy<Value = NetworkUpgrade> {
2167        prop::sample::select(vec![NetworkUpgrade::Nu6_3, NetworkUpgrade::Nu7])
2168    }
2169
2170    // Resolves the bundle version applicable to a pool at an upgrade's testnet activation height,
2171    // the way the fee and builder paths do: height -> consensus branch -> `BundleVersion`.
2172    fn bundle_version_at(upgrade: NetworkUpgrade, pool: ValuePool) -> BundleVersion {
2173        let height = Network::TestNetwork.activation_height(upgrade).unwrap();
2174        bundle_version_for_branch(BranchId::for_height(&Network::TestNetwork, height), pool)
2175            .unwrap()
2176    }
2177
2178    // A step that requires no actions in a pool produces no bundle in that pool, so it is charged
2179    // nothing, no matter how much padding the bundle type would otherwise apply. The exception is
2180    // a bundle type that requires a bundle: the builder then produces one consisting entirely of
2181    // dummy actions, padded to the type's minimum.
2182    #[test]
2183    fn uninvolved_pool_is_charged_nothing_unless_a_bundle_is_required() {
2184        let step = step_with_notes(vec![]);
2185        for bundle_type in [BundlePadding::DEFAULT, BundlePadding::UNPADDED] {
2186            assert_eq!(
2187                step.orchard_action_count(bundle_type, BundleVersion::orchard_v3()),
2188                Ok(0)
2189            );
2190            assert_eq!(
2191                step.ironwood_action_count(bundle_type, BundleVersion::ironwood_v3()),
2192                Ok(0)
2193            );
2194        }
2195
2196        // `bundle_required` guarantees a bundle exists, padded to the type's minimum: two
2197        // all-dummy actions by default, or one when the type opts out of the default padding.
2198        let required = BundlePadding {
2199            bundle_required: true,
2200            pad_to_minimum: None,
2201        };
2202        assert_eq!(
2203            step.orchard_action_count(required, BundleVersion::orchard_v3()),
2204            Ok(2)
2205        );
2206        let required_unpadded = BundlePadding {
2207            bundle_required: true,
2208            pad_to_minimum: Some(1),
2209        };
2210        assert_eq!(
2211            step.orchard_action_count(required_unpadded, BundleVersion::orchard_v3()),
2212            Ok(1)
2213        );
2214
2215        // A zero floor cannot suppress a required bundle: a bundle must contain at least one
2216        // action to exist at all, so the required-but-unpadded-to-zero case still yields one.
2217        let required_zero_floor = BundlePadding {
2218            bundle_required: true,
2219            pad_to_minimum: Some(0),
2220        };
2221        assert_eq!(
2222            step.orchard_action_count(required_zero_floor, BundleVersion::orchard_v3()),
2223            Ok(1)
2224        );
2225
2226        // Without `bundle_required`, a zero floor leaves an uninvolved pool with no bundle.
2227        let zero_floor = BundlePadding {
2228            bundle_required: false,
2229            pad_to_minimum: Some(0),
2230        };
2231        assert_eq!(
2232            step.orchard_action_count(zero_floor, BundleVersion::orchard_v3()),
2233            Ok(0)
2234        );
2235    }
2236
2237    // `spends_cannot_be_charged_to_a_coinbase_bundle` used to live here, asserting that a step
2238    // which spends notes is rejected when charged to a coinbase bundle of that pool. The action
2239    // counters now take a `BundlePadding`, which has no coinbase variant, so that case is
2240    // unrepresentable rather than merely rejected: a step describes a wallet spend, and a wallet
2241    // spend is never a coinbase transaction.
2242
2243    // What NU6.3 costs a large transaction depends on its shape, and a wallet estimating fees at
2244    // scale has to get the difference right.
2245    #[test]
2246    fn nu6_3_action_growth_at_scale_depends_on_step_shape() {
2247        let balanced = |n| {
2248            step_with_notes_and_change(
2249                orchard_and_ironwood_notes((n, 10_000), (0, 0)),
2250                std::iter::repeat_n(shielded_change(ShieldedPool::Orchard, 10_000), n).collect(),
2251            )
2252        };
2253
2254        // A consolidation -- sweeping many notes into a single change output -- pays for that
2255        // output's fabricated spend and nothing else: one extra action, however large it is.
2256        let consolidation = step_with_notes_and_change(
2257            orchard_and_ironwood_notes((MAX_ACTIONS_PER_BUNDLE, 10_000), (0, 0)),
2258            vec![shielded_change(ShieldedPool::Orchard, 10_000)],
2259        );
2260        // 2439 actions: the change output rides along in a spend's action, exactly filling a block.
2261        assert_eq!(
2262            consolidation
2263                .orchard_action_count(BundlePadding::UNPADDED, BundleVersion::orchard_v2()),
2264            Ok(MAX_ACTIONS_PER_BUNDLE)
2265        );
2266        // 2440 from NU6.3: one action over the ceiling, so the same sweep no longer fits.
2267        assert_eq!(
2268            consolidation
2269                .orchard_action_count(BundlePadding::UNPADDED, BundleVersion::orchard_v3()),
2270            Ok(MAX_ACTIONS_PER_BUNDLE + 1)
2271        );
2272
2273        // A balanced step -- as many outputs as spends -- doubles instead, so the largest one that
2274        // fits in a block halves at NU6.3: 2439 spends paired with 2439 outputs before, 1219 of
2275        // each after. This is why `arb_note_count` caps each side at half the ceiling.
2276        let half = MAX_ACTIONS_PER_BUNDLE / 2;
2277        assert_eq!(
2278            balanced(MAX_ACTIONS_PER_BUNDLE)
2279                .orchard_action_count(BundlePadding::UNPADDED, BundleVersion::orchard_v2()),
2280            Ok(MAX_ACTIONS_PER_BUNDLE)
2281        );
2282        // 2438: the largest balanced step that still fits.
2283        assert_eq!(
2284            balanced(half)
2285                .orchard_action_count(BundlePadding::UNPADDED, BundleVersion::orchard_v3()),
2286            Ok(2 * half)
2287        );
2288        // 2440: one note more on each side and it does not.
2289        assert_eq!(
2290            balanced(half + 1)
2291                .orchard_action_count(BundlePadding::UNPADDED, BundleVersion::orchard_v3()),
2292            Ok(2 * half + 2)
2293        );
2294    }
2295
2296    // A `Step` does not necessarily come from this wallet's own input selection: a proposal crosses
2297    // a trust boundary as a serialized message (see `proposal.proto` and
2298    // `Proposal::try_into_standard_proposal`), so a step's output and change counts are
2299    // attacker-influenced. A step claiming far more outputs than could ever fit on-chain must still
2300    // be counted exactly, so that the fee rule is handed an unaffordable action count and rejects
2301    // it, rather than a small wrapped-around one it would accept.
2302    //
2303    // The counts themselves cannot overflow `usize`: each is the length of an in-memory collection,
2304    // so reaching `usize::MAX` would require more notes than could be addressed.
2305    #[test]
2306    fn counts_beyond_the_block_limit_are_counted_exactly() {
2307        let spends = 4 * MAX_ACTIONS_PER_BUNDLE;
2308        let change = 4 * MAX_ACTIONS_PER_BUNDLE;
2309        let step = step_with_notes_and_change(
2310            orchard_and_ironwood_notes((spends, 10_000), (0, 0)),
2311            std::iter::repeat_n(shielded_change(ShieldedPool::Orchard, 10_000), change).collect(),
2312        );
2313
2314        // Roughly eight times what a block can hold, reported exactly.
2315        assert!(spends + change > 8 * MAX_ACTIONS_PER_BUNDLE - 1);
2316        assert_eq!(
2317            step.orchard_action_count(BundlePadding::UNPADDED, BundleVersion::orchard_v3()),
2318            Ok(spends + change)
2319        );
2320
2321        // Pre-NU6.3 the same step pairs, so it is charged half as much -- still far past the
2322        // ceiling, and still counted exactly.
2323        assert_eq!(
2324            step.orchard_action_count(BundlePadding::UNPADDED, BundleVersion::orchard_v2()),
2325            Ok(spends.max(change))
2326        );
2327
2328        // Padding a step this size cannot reduce it, and the ZIP 317 floor is irrelevant here.
2329        assert_eq!(
2330            step.orchard_action_count(BundlePadding::DEFAULT, BundleVersion::orchard_v3()),
2331            Ok(spends + change)
2332        );
2333    }
2334
2335    // A payment output counts towards the action count of the pool it is directed to, exactly as
2336    // a change output does. `orchard_payment_step` spends one Orchard note to make one payment.
2337    #[test]
2338    fn action_count_includes_payment_outputs() {
2339        // Pre-activation, the payment is an Orchard-pool output: one spend and one output share
2340        // an action, because the pre-NU6.3 Orchard bundle version permits cross-address transfers.
2341        let step = orchard_payment_step(PoolType::ORCHARD, false).unwrap();
2342        assert_eq!(
2343            step.orchard_action_count(BundlePadding::UNPADDED, BundleVersion::orchard_v2()),
2344            Ok(1)
2345        );
2346        assert_eq!(
2347            step.ironwood_action_count(BundlePadding::UNPADDED, BundleVersion::ironwood_v3()),
2348            Ok(0)
2349        );
2350
2351        // Post-activation, the payment is routed to the Ironwood pool: the Orchard note spend is
2352        // charged to the Orchard bundle and the payment output to the Ironwood bundle, so neither
2353        // pool can pair them into one action.
2354        let step = orchard_payment_step(PoolType::IRONWOOD, true).unwrap();
2355        assert_eq!(
2356            step.orchard_action_count(BundlePadding::UNPADDED, BundleVersion::orchard_v3()),
2357            Ok(1)
2358        );
2359        assert_eq!(
2360            step.ironwood_action_count(BundlePadding::UNPADDED, BundleVersion::ironwood_v3()),
2361            Ok(1)
2362        );
2363    }
2364
2365    proptest! {
2366        // The number of actions a step's Orchard-pool bundle requires depends upon the bundle
2367        // version: the pre-NU6.3 versions permit cross-address transfers, so a spend and an
2368        // output may share an action (`max(spends, outputs)`), while from NU6.3 onwards the
2369        // Orchard pool disables them, so each spend and output is paired with a fabricated
2370        // counterpart and occupies its own action (`spends + outputs`). The Ironwood pool
2371        // permits cross-address transfers at every version, so it always pairs.
2372        #[test]
2373        fn action_count_pairs_spends_and_outputs_only_when_cross_address_is_permitted(
2374            orchard_spends in arb_note_count(),
2375            ironwood_spends in arb_note_count(),
2376            orchard_change in arb_note_count(),
2377            ironwood_change in arb_note_count(),
2378            orchard_value in arb_note_value(),
2379            ironwood_value in arb_note_value(),
2380            change_value in arb_note_value(),
2381        ) {
2382            let change = std::iter::repeat_n(ShieldedPool::Orchard, orchard_change)
2383                .chain(std::iter::repeat_n(ShieldedPool::Ironwood, ironwood_change))
2384                .map(|pool| shielded_change(pool, change_value))
2385                .collect();
2386            let step = step_with_notes_and_change(
2387                orchard_and_ironwood_notes((orchard_spends, orchard_value), (ironwood_spends, ironwood_value)),
2388                change,
2389            );
2390
2391            // `UNPADDED` pads only to the one-action consensus minimum, so for a non-empty
2392            // bundle its action count is exactly the number of requested actions.
2393            for version in [BundleVersion::orchard_insecure_v1(), BundleVersion::orchard_v2()] {
2394                prop_assert_eq!(
2395                    step.orchard_action_count(BundlePadding::UNPADDED, version),
2396                    Ok(orchard_spends.max(orchard_change))
2397                );
2398            }
2399            prop_assert_eq!(
2400                step.orchard_action_count(BundlePadding::UNPADDED, BundleVersion::orchard_v3()),
2401                Ok(orchard_spends + orchard_change)
2402            );
2403            prop_assert_eq!(
2404                step.ironwood_action_count(BundlePadding::UNPADDED, BundleVersion::ironwood_v3()),
2405                Ok(ironwood_spends.max(ironwood_change))
2406            );
2407
2408            // The bundle type governs padding on top of that count: `DEFAULT` pads a non-empty
2409            // bundle up to the ZIP 317 two-action floor, and produces no bundle at all when the
2410            // step requires no actions in that pool.
2411            let pad = |requested: usize| if requested == 0 { 0 } else { requested.max(2) };
2412            prop_assert_eq!(
2413                step.orchard_action_count(BundlePadding::DEFAULT, BundleVersion::orchard_v3()),
2414                Ok(pad(orchard_spends + orchard_change))
2415            );
2416            prop_assert_eq!(
2417                step.ironwood_action_count(BundlePadding::DEFAULT, BundleVersion::ironwood_v3()),
2418                Ok(pad(ironwood_spends.max(ironwood_change)))
2419            );
2420        }
2421
2422        // The action count a caller obtains for a real target height, resolving the bundle
2423        // version the way the fee and builder paths do: height -> consensus branch ->
2424        // `BundleVersion`. This is the scenario the legacy hardcoded `max(spends, outputs)`
2425        // formula got wrong: at a post-NU6.3 height it understated the Orchard action count
2426        // whenever a step had both spends and outputs, and so understated the ZIP 317 fee.
2427        #[test]
2428        fn orchard_action_count_grows_at_nu6_3_activation_height(
2429            spends in arb_note_count(),
2430            change in arb_note_count(),
2431            note_value in arb_note_value(),
2432            change_value in arb_note_value(),
2433            pre_nu6_3 in arb_pre_nu6_3_upgrade(),
2434            nu6_3_or_later in arb_nu6_3_or_later_upgrade(),
2435        ) {
2436            let orchard_step = step_with_notes_and_change(
2437                orchard_and_ironwood_notes((spends, note_value), (0, 0)),
2438                std::iter::repeat_n(shielded_change(ShieldedPool::Orchard, change_value), change)
2439                    .collect(),
2440            );
2441
2442            // Pre-NU6.3, each change output shares an action with a spend: max(spends, change).
2443            prop_assert_eq!(
2444                orchard_step.orchard_action_count(
2445                    BundlePadding::UNPADDED,
2446                    bundle_version_at(pre_nu6_3, ValuePool::Orchard),
2447                ),
2448                Ok(spends.max(change))
2449            );
2450
2451            // From NU6.3, the Orchard pool disables cross-address transfers: a change output's
2452            // corresponding dummy input must be signed with the spending key for the internal
2453            // IVK the change is sent to, so it cannot share an action with a spend of a note
2454            // belonging to a different address. Hence spends + change actions.
2455            prop_assert_eq!(
2456                orchard_step.orchard_action_count(
2457                    BundlePadding::UNPADDED,
2458                    bundle_version_at(nu6_3_or_later, ValuePool::Orchard),
2459                ),
2460                Ok(spends + change)
2461            );
2462
2463            // The Ironwood pool retains cross-address transfers, so an equivalent Ironwood step
2464            // still pairs at a post-NU6.3 height.
2465            let ironwood_step = step_with_notes_and_change(
2466                orchard_and_ironwood_notes((0, 0), (spends, note_value)),
2467                std::iter::repeat_n(shielded_change(ShieldedPool::Ironwood, change_value), change)
2468                    .collect(),
2469            );
2470            prop_assert_eq!(
2471                ironwood_step.ironwood_action_count(
2472                    BundlePadding::UNPADDED,
2473                    bundle_version_at(nu6_3_or_later, ValuePool::Ironwood),
2474                ),
2475                Ok(spends.max(change))
2476            );
2477        }
2478
2479        // A non-empty bundle is charged the greater of the actions it requests and the bundle
2480        // type's padding floor: padding never reduces the count, and the floor is never
2481        // undershot. A bundle that requests nothing is not produced at all, so it is charged
2482        // nothing however high the floor. `DEFAULT` (floor 2) and `UNPADDED` (floor 1) are the
2483        // floors the wallet itself uses; the whole `u8` range is covered here, including the
2484        // degenerate zero floor, since the bundle type is the caller's to choose.
2485        #[test]
2486        fn action_count_is_never_below_the_bundle_types_padding_floor(
2487            spends in arb_note_count(),
2488            change in arb_note_count(),
2489            note_value in arb_note_value(),
2490            change_value in arb_note_value(),
2491            pad_to_minimum in 0u8..=u8::MAX,
2492        ) {
2493            let step = step_with_notes_and_change(
2494                orchard_and_ironwood_notes((spends, note_value), (0, 0)),
2495                std::iter::repeat_n(shielded_change(ShieldedPool::Orchard, change_value), change)
2496                    .collect(),
2497            );
2498            let bundle_type = BundlePadding {
2499                bundle_required: false,
2500                pad_to_minimum: Some(pad_to_minimum),
2501            };
2502
2503            // Post-NU6.3, so the requested count is `spends + change`.
2504            let requested = spends + change;
2505            let expected = if requested == 0 {
2506                0
2507            } else {
2508                requested.max(usize::from(pad_to_minimum))
2509            };
2510            prop_assert_eq!(
2511                step.orchard_action_count(bundle_type, BundleVersion::orchard_v3()),
2512                Ok(expected)
2513            );
2514        }
2515
2516        // `Note::pool` reports the `ValuePool` recorded alongside an Orchard note.
2517        #[test]
2518        fn note_pool_reports_stored_value_pool(
2519            value in 1u64..1_000_000_000u64,
2520            is_ironwood in any::<bool>(),
2521        ) {
2522            let (version, pool, expected) = if is_ironwood {
2523                (NoteVersion::V3, ValuePool::Ironwood, ShieldedPool::Ironwood)
2524            } else {
2525                (NoteVersion::V2, ValuePool::Orchard, ShieldedPool::Orchard)
2526            };
2527            let Some(note) = orchard_note(value, version) else {
2528                // A handful of (value, rho, rseed) combinations do not form a valid note; skip them.
2529                return Err(TestCaseError::reject("invalid orchard note"));
2530            };
2531            let note = Note::Orchard { note, pool };
2532            prop_assert_eq!(note.pool(), expected);
2533        }
2534
2535        // `Step::input_count_in_pool` returns the number of selected notes in each pool, splitting
2536        // Orchard (version 2) from Ironwood (version 3); Sapling is zero here. `input_in_pool`
2537        // agrees with `input_count_in_pool > 0`, and `Proposal::input_count_in_pool` sums the
2538        // per-step counts.
2539        #[test]
2540        fn step_and_proposal_input_counts_match_constructed_notes(
2541            n_orchard in 0usize..5,
2542            n_ironwood in 0usize..5,
2543            m_orchard in 0usize..5,
2544            m_ironwood in 0usize..5,
2545        ) {
2546            let step1 = step_with_notes(orchard_and_ironwood_notes((n_orchard, 10_000), (n_ironwood, 20_000)));
2547            prop_assert_eq!(step1.input_count_in_pool(PoolType::SAPLING), 0);
2548            prop_assert_eq!(step1.input_count_in_pool(PoolType::ORCHARD), n_orchard);
2549            prop_assert_eq!(step1.input_count_in_pool(PoolType::IRONWOOD), n_ironwood);
2550
2551            for pool in [ShieldedPool::Sapling, ShieldedPool::Orchard, ShieldedPool::Ironwood] {
2552                let pool_type = PoolType::Shielded(pool);
2553                prop_assert_eq!(
2554                    step1.input_in_pool(pool_type),
2555                    step1.input_count_in_pool(pool_type) > 0
2556                );
2557            }
2558
2559            let step2 = step_with_notes(orchard_and_ironwood_notes((m_orchard, 10_000), (m_ironwood, 20_000)));
2560            let proposal = Proposal::<(), u32> {
2561                fee_rule: (),
2562                min_target_height: TargetHeight::from(100u32),
2563                confirmations_policy: ConfirmationsPolicy::default(),
2564                proposed_version: None,
2565                steps: NonEmpty::from_vec(vec![step1, step2]).unwrap(),
2566            };
2567            prop_assert_eq!(proposal.input_count_in_pool(PoolType::SAPLING), 0);
2568            prop_assert_eq!(
2569                proposal.input_count_in_pool(PoolType::ORCHARD),
2570                n_orchard + m_orchard
2571            );
2572            prop_assert_eq!(
2573                proposal.input_count_in_pool(PoolType::IRONWOOD),
2574                n_ironwood + m_ironwood
2575            );
2576        }
2577    }
2578}