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