Skip to main content

zebra_chain/transaction/
unmined.rs

1//! Unmined Zcash transaction identifiers and transactions.
2//!
3//! Transaction version 5 is uniquely identified by [`WtxId`] when unmined, and
4//! [`struct@Hash`] in the blockchain. The effects of a v5 transaction
5//! (spends and outputs) are uniquely identified by the same
6//! [`struct@Hash`] in both cases.
7//!
8//! Transaction versions 1-4 are uniquely identified by legacy
9//! [`struct@Hash`] transaction IDs, whether they have been mined or not.
10//! So Zebra, and the Zcash network protocol, don't use witnessed transaction
11//! IDs for them.
12//!
13//! Zebra's [`UnminedTxId`] and [`UnminedTx`] enums provide the correct unique
14//! ID for unmined transactions. They can be used to handle transactions
15//! regardless of version, and get the [`WtxId`] or [`struct@Hash`] when
16//! required.
17
18use std::{fmt, sync::Arc};
19
20use crate::{
21    amount::{Amount, NonNegative},
22    block::Height,
23    serialization::ZcashSerialize,
24    transaction::{
25        AuthDigest, Hash,
26        Transaction::{self, *},
27        WtxId,
28    },
29    transparent,
30};
31
32use UnminedTxId::*;
33
34#[cfg(any(test, feature = "proptest-impl"))]
35use proptest_derive::Arbitrary;
36
37// Documentation-only
38#[allow(unused_imports)]
39use crate::block::MAX_BLOCK_BYTES;
40
41pub mod zip317;
42
43/// The minimum cost value for a transaction in the mempool.
44///
45/// Contributes to the randomized, weighted eviction of transactions from the
46/// mempool when it reaches a max size, also based on the total cost.
47///
48/// # Standard Rule
49///
50/// > Each transaction has a cost, which is an integer defined as:
51/// >
52/// > max(memory size in bytes, 10000)
53/// >
54/// > The memory size is an estimate of the size that a transaction occupies in the
55/// > memory of a node. It MAY be approximated as the serialized transaction size in
56/// > bytes.
57/// >
58/// > ...
59/// >
60/// > The threshold 10000 for the cost function is chosen so that the size in bytes of
61/// > a minimal fully shielded Orchard transaction with 2 shielded actions (having a
62/// > serialized size of 9165 bytes) will fall below the threshold. This has the effect
63/// > of ensuring that such transactions are not evicted preferentially to typical
64/// > transparent or Sapling transactions because of their size.
65///
66/// [ZIP-401]: https://zips.z.cash/zip-0401
67pub const MEMPOOL_TRANSACTION_COST_THRESHOLD: u64 = 10_000;
68
69/// When a transaction pays a fee less than the conventional fee,
70/// this low fee penalty is added to its cost for mempool eviction.
71///
72/// See [VerifiedUnminedTx::eviction_weight()] for details.
73///
74/// [ZIP-401]: https://zips.z.cash/zip-0401
75const MEMPOOL_TRANSACTION_LOW_FEE_PENALTY: u64 = 40_000;
76
77/// A unique identifier for an unmined transaction, regardless of version.
78///
79/// "The transaction ID of a version 4 or earlier transaction is the SHA-256d hash
80/// of the transaction encoding in the pre-v5 format described above.
81///
82/// The transaction ID of a version 5 transaction is as defined in [ZIP-244].
83///
84/// A v5 transaction also has a wtxid (used for example in the peer-to-peer protocol)
85/// as defined in [ZIP-239]."
86/// [Spec: Transaction Identifiers]
87///
88/// [ZIP-239]: https://zips.z.cash/zip-0239
89/// [ZIP-244]: https://zips.z.cash/zip-0244
90/// [Spec: Transaction Identifiers]: https://zips.z.cash/protocol/protocol.pdf#txnidentifiers
91#[derive(Copy, Clone, Eq, PartialEq, Hash)]
92#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
93pub enum UnminedTxId {
94    /// A legacy unmined transaction identifier.
95    ///
96    /// Used to uniquely identify unmined version 1-4 transactions.
97    /// (After v1-4 transactions are mined, they can be uniquely identified
98    /// using the same [`struct@Hash`].)
99    Legacy(Hash),
100
101    /// A witnessed unmined transaction identifier.
102    ///
103    /// Used to uniquely identify unmined version 5 transactions.
104    /// (After v5 transactions are mined, they can be uniquely identified
105    /// using only the [`struct@Hash`] in their `WtxId.id`.)
106    ///
107    /// For more details, see [`WtxId`].
108    Witnessed(WtxId),
109}
110
111impl fmt::Debug for UnminedTxId {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            // Logging unmined transaction IDs can leak sensitive user information,
115            // particularly when Zebra is being used as a `lightwalletd` backend.
116            Self::Legacy(_hash) => f.debug_tuple("Legacy").field(&self.to_string()).finish(),
117            Self::Witnessed(_id) => f.debug_tuple("Witnessed").field(&self.to_string()).finish(),
118        }
119    }
120}
121
122impl fmt::Display for UnminedTxId {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        match self {
125            Legacy(_hash) => f
126                .debug_tuple("transaction::Hash")
127                .field(&"private")
128                .finish(),
129            Witnessed(_id) => f.debug_tuple("WtxId").field(&"private").finish(),
130        }
131    }
132}
133
134impl From<Transaction> for UnminedTxId {
135    fn from(transaction: Transaction) -> Self {
136        // use the ref implementation, to avoid cloning the transaction
137        UnminedTxId::from(&transaction)
138    }
139}
140
141impl From<&Transaction> for UnminedTxId {
142    fn from(transaction: &Transaction) -> Self {
143        match transaction {
144            V1 { .. } | V2 { .. } | V3 { .. } | V4 { .. } => Legacy(transaction.into()),
145            V5 { .. } => Witnessed(transaction.into()),
146            #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
147            V6 { .. } => Witnessed(transaction.into()),
148        }
149    }
150}
151
152impl From<Arc<Transaction>> for UnminedTxId {
153    fn from(transaction: Arc<Transaction>) -> Self {
154        transaction.as_ref().into()
155    }
156}
157
158impl From<WtxId> for UnminedTxId {
159    fn from(wtx_id: WtxId) -> Self {
160        Witnessed(wtx_id)
161    }
162}
163
164impl From<&WtxId> for UnminedTxId {
165    fn from(wtx_id: &WtxId) -> Self {
166        (*wtx_id).into()
167    }
168}
169
170impl UnminedTxId {
171    /// Create a new [`UnminedTxId`] using a v1-v4 legacy transaction ID.
172    ///
173    /// # Correctness
174    ///
175    /// This method must only be used for v1-v4 transaction IDs.
176    /// [`struct@Hash`] does not uniquely identify unmined v5
177    /// transactions.
178    pub fn from_legacy_id(legacy_tx_id: Hash) -> UnminedTxId {
179        Legacy(legacy_tx_id)
180    }
181
182    /// Return the unique ID that will be used if this transaction gets mined into a block.
183    ///
184    /// # Correctness
185    ///
186    /// For v1-v4 transactions, this method returns an ID which changes
187    /// if this transaction's effects (spends and outputs) change, or
188    /// if its authorizing data changes (signatures, proofs, and scripts).
189    ///
190    /// But for v5 transactions, this ID uniquely identifies the transaction's effects.
191    pub fn mined_id(&self) -> Hash {
192        match self {
193            Legacy(legacy_id) => *legacy_id,
194            Witnessed(wtx_id) => wtx_id.id,
195        }
196    }
197
198    /// Returns a mutable reference to the unique ID
199    /// that will be used if this transaction gets mined into a block.
200    ///
201    /// See [`Self::mined_id`] for details.
202    #[cfg(any(test, feature = "proptest-impl"))]
203    pub fn mined_id_mut(&mut self) -> &mut Hash {
204        match self {
205            Legacy(legacy_id) => legacy_id,
206            Witnessed(wtx_id) => &mut wtx_id.id,
207        }
208    }
209
210    /// Return the digest of this transaction's authorizing data,
211    /// (signatures, proofs, and scripts), if it is a v5 transaction.
212    pub fn auth_digest(&self) -> Option<AuthDigest> {
213        match self {
214            Legacy(_) => None,
215            Witnessed(wtx_id) => Some(wtx_id.auth_digest),
216        }
217    }
218
219    /// Returns a mutable reference to the digest of this transaction's authorizing data,
220    /// (signatures, proofs, and scripts), if it is a v5 transaction.
221    #[cfg(any(test, feature = "proptest-impl"))]
222    pub fn auth_digest_mut(&mut self) -> Option<&mut AuthDigest> {
223        match self {
224            Legacy(_) => None,
225            Witnessed(wtx_id) => Some(&mut wtx_id.auth_digest),
226        }
227    }
228}
229
230/// An unmined transaction, and its pre-calculated unique identifying ID.
231///
232/// This transaction has been structurally verified.
233/// (But it might still need semantic or contextual verification.)
234#[derive(Clone, Eq, PartialEq)]
235pub struct UnminedTx {
236    /// The unmined transaction itself.
237    pub transaction: Arc<Transaction>,
238
239    /// A unique identifier for this unmined transaction.
240    pub id: UnminedTxId,
241
242    /// The size in bytes of the serialized transaction data
243    pub size: usize,
244
245    /// The conventional fee for this transaction, as defined by [ZIP-317].
246    ///
247    /// [ZIP-317]: https://zips.z.cash/zip-0317#fee-calculation
248    pub conventional_fee: Amount<NonNegative>,
249}
250
251impl fmt::Debug for UnminedTx {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        // Logging unmined transactions can leak sensitive user information,
254        // particularly when Zebra is being used as a `lightwalletd` backend.
255        f.debug_tuple("UnminedTx").field(&"private").finish()
256    }
257}
258
259impl fmt::Display for UnminedTx {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        f.debug_tuple("UnminedTx").field(&"private").finish()
262    }
263}
264
265// Each of these conversions is implemented slightly differently,
266// to avoid cloning the transaction where possible.
267
268impl From<Transaction> for UnminedTx {
269    fn from(transaction: Transaction) -> Self {
270        let size = transaction.zcash_serialized_size();
271        let conventional_fee = zip317::conventional_fee(&transaction);
272
273        // The borrow is actually needed to avoid taking ownership
274        #[allow(clippy::needless_borrow)]
275        Self {
276            id: (&transaction).into(),
277            size,
278            conventional_fee,
279            transaction: Arc::new(transaction),
280        }
281    }
282}
283
284impl From<&Transaction> for UnminedTx {
285    fn from(transaction: &Transaction) -> Self {
286        let size = transaction.zcash_serialized_size();
287        let conventional_fee = zip317::conventional_fee(transaction);
288
289        Self {
290            id: transaction.into(),
291            size,
292            conventional_fee,
293            transaction: Arc::new(transaction.clone()),
294        }
295    }
296}
297
298impl From<Arc<Transaction>> for UnminedTx {
299    fn from(transaction: Arc<Transaction>) -> Self {
300        let size = transaction.zcash_serialized_size();
301        let conventional_fee = zip317::conventional_fee(&transaction);
302
303        Self {
304            id: transaction.as_ref().into(),
305            size,
306            conventional_fee,
307            transaction,
308        }
309    }
310}
311
312impl From<&Arc<Transaction>> for UnminedTx {
313    fn from(transaction: &Arc<Transaction>) -> Self {
314        let size = transaction.zcash_serialized_size();
315        let conventional_fee = zip317::conventional_fee(transaction);
316
317        Self {
318            id: transaction.as_ref().into(),
319            size,
320            conventional_fee,
321            transaction: transaction.clone(),
322        }
323    }
324}
325
326/// A verified unmined transaction, and the corresponding transaction fee.
327///
328/// This transaction has been fully verified, in the context of the mempool.
329//
330// This struct can't be `Eq`, because it contains a `f32`.
331#[derive(Clone, PartialEq)]
332pub struct VerifiedUnminedTx {
333    /// The unmined transaction.
334    pub transaction: UnminedTx,
335
336    /// The transaction fee for this unmined transaction.
337    pub miner_fee: Amount<NonNegative>,
338
339    /// The number of legacy transparent signature operations in this transaction.
340    ///
341    /// This is the legacy sigop count only (`GetLegacySigOpCount()`).
342    /// The mempool adds P2SH sigops (`GetP2SHSigOpCount()`) when checking
343    /// `MAX_STANDARD_TX_SIGOPS`.
344    pub legacy_sigop_count: u32,
345
346    /// The number of conventional actions for `transaction`, as defined by [ZIP-317].
347    ///
348    /// The number of actions is limited by [`MAX_BLOCK_BYTES`], so it fits in a u32.
349    ///
350    /// [ZIP-317]: https://zips.z.cash/zip-0317#block-production
351    pub conventional_actions: u32,
352
353    /// The number of unpaid actions for `transaction`,
354    /// as defined by [ZIP-317] for block production.
355    ///
356    /// The number of actions is limited by [`MAX_BLOCK_BYTES`], so it fits in a u32.
357    ///
358    /// [ZIP-317]: https://zips.z.cash/zip-0317#block-production
359    pub unpaid_actions: u32,
360
361    /// The fee weight ratio for `transaction`, as defined by [ZIP-317] for block production.
362    ///
363    /// This is not consensus-critical, so we use `f32` for efficient calculations
364    /// when the mempool holds a large number of transactions.
365    ///
366    /// [ZIP-317]: https://zips.z.cash/zip-0317#block-production
367    pub fee_weight_ratio: f32,
368
369    /// The time the transaction was added to the mempool, or None if it has not
370    /// reached the mempool yet.
371    pub time: Option<chrono::DateTime<chrono::Utc>>,
372
373    /// The tip height when the transaction was added to the mempool, or None if
374    /// it has not reached the mempool yet.
375    pub height: Option<Height>,
376
377    /// The spent outputs for this transaction's transparent inputs.
378    ///
379    /// Used by mempool policy checks (`AreInputsStandard`, `GetP2SHSigOpCount`).
380    /// Empty for transactions with no transparent inputs or in test contexts.
381    pub spent_outputs: Arc<Vec<transparent::Output>>,
382}
383
384impl fmt::Debug for VerifiedUnminedTx {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        // Logging unmined transactions can leak sensitive user information,
387        // particularly when Zebra is being used as a `lightwalletd` backend.
388        f.debug_tuple("VerifiedUnminedTx")
389            .field(&"private")
390            .finish()
391    }
392}
393
394impl fmt::Display for VerifiedUnminedTx {
395    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396        f.debug_tuple("VerifiedUnminedTx")
397            .field(&"private")
398            .finish()
399    }
400}
401
402impl VerifiedUnminedTx {
403    /// Create a new verified unmined transaction from an unmined transaction,
404    /// its miner fee, its legacy sigop count, and the spent outputs for its transparent inputs.
405    pub fn new(
406        transaction: UnminedTx,
407        miner_fee: Amount<NonNegative>,
408        legacy_sigop_count: u32,
409        spent_outputs: Arc<Vec<transparent::Output>>,
410    ) -> Result<Self, zip317::Error> {
411        let fee_weight_ratio = zip317::conventional_fee_weight_ratio(&transaction, miner_fee);
412        let conventional_actions = zip317::conventional_actions(&transaction.transaction);
413        let unpaid_actions = zip317::unpaid_actions(&transaction, miner_fee);
414
415        zip317::mempool_checks(unpaid_actions, miner_fee, transaction.size)?;
416
417        Ok(Self {
418            transaction,
419            miner_fee,
420            legacy_sigop_count,
421            fee_weight_ratio,
422            conventional_actions,
423            unpaid_actions,
424            time: None,
425            height: None,
426            spent_outputs,
427        })
428    }
429
430    /// Returns `true` if the transaction pays at least the [ZIP-317] conventional fee.
431    ///
432    /// [ZIP-317]: https://zips.z.cash/zip-0317#mempool-size-limiting
433    pub fn pays_conventional_fee(&self) -> bool {
434        self.miner_fee >= self.transaction.conventional_fee
435    }
436
437    /// The cost in bytes of the transaction, as defined in [ZIP-401].
438    ///
439    /// A reflection of the work done by the network in processing them (proof
440    /// and signature verification; networking overheads; size of in-memory data
441    /// structures).
442    ///
443    /// > Each transaction has a cost, which is an integer defined as...
444    ///
445    /// [ZIP-401]: https://zips.z.cash/zip-0401
446    pub fn cost(&self) -> u64 {
447        std::cmp::max(
448            u64::try_from(self.transaction.size).expect("fits in u64"),
449            MEMPOOL_TRANSACTION_COST_THRESHOLD,
450        )
451    }
452
453    /// The computed _eviction weight_ of a verified unmined transaction as part
454    /// of the mempool set, as defined in [ZIP-317] and [ZIP-401].
455    ///
456    /// # Standard Rule
457    ///
458    /// > Each transaction also has an *eviction weight*, which is *cost* + *low_fee_penalty*,
459    /// > where *low_fee_penalty* is 40000 if the transaction pays a fee less than the
460    /// > conventional fee, otherwise 0. The conventional fee is currently defined in
461    /// > [ZIP-317].
462    ///
463    /// > zcashd and zebrad limit the size of the mempool as described in [ZIP-401].
464    /// > This specifies a low fee penalty that is added to the "eviction weight" if the transaction
465    /// > pays a fee less than the conventional transaction fee. This threshold is
466    /// > modified to use the new conventional fee formula.
467    ///
468    /// [ZIP-317]: https://zips.z.cash/zip-0317#mempool-size-limiting
469    /// [ZIP-401]: https://zips.z.cash/zip-0401
470    pub fn eviction_weight(&self) -> u64 {
471        let mut cost = self.cost();
472
473        if !self.pays_conventional_fee() {
474            cost += MEMPOOL_TRANSACTION_LOW_FEE_PENALTY
475        }
476
477        cost
478    }
479}