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