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