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 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 transparent signature operations in this transaction.
339 ///
340 /// This is the legacy sigop count only (`GetLegacySigOpCount()`).
341 /// The mempool adds P2SH sigops (`GetP2SHSigOpCount()`) when checking
342 /// `MAX_STANDARD_TX_SIGOPS`.
343 pub legacy_sigop_count: u32,
344
345 /// The number of P2SH redeem-script signature operations in this transaction.
346 ///
347 /// This mirrors zcashd's `GetP2SHSigOpCount()`. It must be added to `legacy_sigop_count` for
348 /// the block-level `MAX_BLOCK_SIGOPS` check and for `getblocktemplate` sigop budgeting,
349 /// matching zcashd's consensus behavior.
350 pub p2sh_sigop_count: u32,
351
352 /// The number of conventional actions for `transaction`, as defined by [ZIP-317].
353 ///
354 /// The number of actions is limited by [`MAX_BLOCK_BYTES`], so it fits in a u32.
355 ///
356 /// [ZIP-317]: https://zips.z.cash/zip-0317#block-production
357 pub conventional_actions: u32,
358
359 /// The number of unpaid actions for `transaction`,
360 /// as defined by [ZIP-317] for block production.
361 ///
362 /// The number of actions is limited by [`MAX_BLOCK_BYTES`], so it fits in a u32.
363 ///
364 /// [ZIP-317]: https://zips.z.cash/zip-0317#block-production
365 pub unpaid_actions: u32,
366
367 /// The fee weight ratio for `transaction`, as defined by [ZIP-317] for block production.
368 ///
369 /// This is not consensus-critical, so we use `f32` for efficient calculations
370 /// when the mempool holds a large number of transactions.
371 ///
372 /// [ZIP-317]: https://zips.z.cash/zip-0317#block-production
373 pub fee_weight_ratio: f32,
374
375 /// The time the transaction was added to the mempool, or None if it has not
376 /// reached the mempool yet.
377 pub time: Option<chrono::DateTime<chrono::Utc>>,
378
379 /// The tip height when the transaction was added to the mempool, or None if
380 /// it has not reached the mempool yet.
381 pub height: Option<Height>,
382
383 /// The spent outputs for this transaction's transparent inputs.
384 ///
385 /// Used by mempool policy checks (`AreInputsStandard`, `GetP2SHSigOpCount`).
386 /// Empty for transactions with no transparent inputs or in test contexts.
387 pub spent_outputs: Arc<Vec<transparent::Output>>,
388}
389
390impl fmt::Debug for VerifiedUnminedTx {
391 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392 // Logging unmined transactions can leak sensitive user information,
393 // particularly when Zebra is being used as a `lightwalletd` backend.
394 f.debug_tuple("VerifiedUnminedTx")
395 .field(&"private")
396 .finish()
397 }
398}
399
400impl fmt::Display for VerifiedUnminedTx {
401 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402 f.debug_tuple("VerifiedUnminedTx")
403 .field(&"private")
404 .finish()
405 }
406}
407
408impl VerifiedUnminedTx {
409 /// Create a new verified unmined transaction from an unmined transaction, its miner fee, its
410 /// legacy and P2SH sigop counts, and the spent outputs for its transparent inputs.
411 pub fn new(
412 transaction: UnminedTx,
413 miner_fee: Amount<NonNegative>,
414 legacy_sigop_count: u32,
415 p2sh_sigop_count: u32,
416 spent_outputs: Arc<Vec<transparent::Output>>,
417 ) -> Result<Self, zip317::Error> {
418 let fee_weight_ratio = zip317::conventional_fee_weight_ratio(&transaction, miner_fee);
419 let conventional_actions = zip317::conventional_actions(&transaction.transaction);
420 let unpaid_actions = zip317::unpaid_actions(&transaction, miner_fee);
421
422 zip317::mempool_checks(unpaid_actions, miner_fee, transaction.size)?;
423
424 Ok(Self {
425 transaction,
426 miner_fee,
427 legacy_sigop_count,
428 p2sh_sigop_count,
429 fee_weight_ratio,
430 conventional_actions,
431 unpaid_actions,
432 time: None,
433 height: None,
434 spent_outputs,
435 })
436 }
437
438 /// The total number of transparent signature operations for block-level accounting: legacy +
439 /// P2SH.
440 ///
441 /// This is the value that must be used for the consensus `MAX_BLOCK_SIGOPS` limit and for
442 /// `getblocktemplate` sigop budgeting.
443 pub fn block_sigop_count(&self) -> u32 {
444 self.legacy_sigop_count
445 .saturating_add(self.p2sh_sigop_count)
446 }
447
448 /// Returns `true` if the transaction pays at least the [ZIP-317] conventional fee.
449 ///
450 /// [ZIP-317]: https://zips.z.cash/zip-0317#mempool-size-limiting
451 pub fn pays_conventional_fee(&self) -> bool {
452 self.miner_fee >= self.transaction.conventional_fee
453 }
454
455 /// The cost in bytes of the transaction, as defined in [ZIP-401].
456 ///
457 /// A reflection of the work done by the network in processing them (proof
458 /// and signature verification; networking overheads; size of in-memory data
459 /// structures).
460 ///
461 /// > Each transaction has a cost, which is an integer defined as...
462 ///
463 /// [ZIP-401]: https://zips.z.cash/zip-0401
464 pub fn cost(&self) -> u64 {
465 std::cmp::max(
466 u64::try_from(self.transaction.size).expect("fits in u64"),
467 MEMPOOL_TRANSACTION_COST_THRESHOLD,
468 )
469 }
470
471 /// The computed _eviction weight_ of a verified unmined transaction as part
472 /// of the mempool set, as defined in [ZIP-317] and [ZIP-401].
473 ///
474 /// # Standard Rule
475 ///
476 /// > Each transaction also has an *eviction weight*, which is *cost* + *low_fee_penalty*,
477 /// > where *low_fee_penalty* is 40000 if the transaction pays a fee less than the
478 /// > conventional fee, otherwise 0. The conventional fee is currently defined in
479 /// > [ZIP-317].
480 ///
481 /// > zcashd and zebrad limit the size of the mempool as described in [ZIP-401].
482 /// > This specifies a low fee penalty that is added to the "eviction weight" if the transaction
483 /// > pays a fee less than the conventional transaction fee. This threshold is
484 /// > modified to use the new conventional fee formula.
485 ///
486 /// [ZIP-317]: https://zips.z.cash/zip-0317#mempool-size-limiting
487 /// [ZIP-401]: https://zips.z.cash/zip-0401
488 pub fn eviction_weight(&self) -> u64 {
489 let mut cost = self.cost();
490
491 if !self.pays_conventional_fee() {
492 cost += MEMPOOL_TRANSACTION_LOW_FEE_PENALTY
493 }
494
495 cost
496 }
497}