//! Unmined Zcash transaction identifiers and transactions. //! //! Transaction version 5 is uniquely identified by [`WtxId`] when unmined, and //! [`struct@Hash`] in the blockchain. The effects of a v5 transaction //! (spends and outputs) are uniquely identified by the same //! [`struct@Hash`] in both cases. //! //! Transaction versions 1-4 are uniquely identified by legacy //! [`struct@Hash`] transaction IDs, whether they have been mined or not. //! So Zebra, and the Zcash network protocol, don't use witnessed transaction //! IDs for them. //! //! Zebra's [`UnminedTxId`] and [`UnminedTx`] enums provide the correct unique //! ID for unmined transactions. They can be used to handle transactions //! regardless of version, and get the [`WtxId`] or [`struct@Hash`] when //! required. use std::{fmt, sync::Arc}; use crate::{ amount::{Amount, NonNegative}, serialization::ZcashSerialize, transaction::{ AuthDigest, Hash, Transaction::{self, *}, WtxId, }, }; use UnminedTxId::*; #[cfg(any(test, feature = "proptest-impl"))] use proptest_derive::Arbitrary; // Documentation-only #[allow(unused_imports)] use crate::block::MAX_BLOCK_BYTES; pub mod zip317; /// The minimum cost value for a transaction in the mempool. /// /// Contributes to the randomized, weighted eviction of transactions from the /// mempool when it reaches a max size, also based on the total cost. /// /// # Standard Rule /// /// > Each transaction has a cost, which is an integer defined as: /// > /// > max(memory size in bytes, 10000) /// > /// > The memory size is an estimate of the size that a transaction occupies in the /// > memory of a node. It MAY be approximated as the serialized transaction size in /// > bytes. /// > /// > ... /// > /// > The threshold 10000 for the cost function is chosen so that the size in bytes of /// > a minimal fully shielded Orchard transaction with 2 shielded actions (having a /// > serialized size of 9165 bytes) will fall below the threshold. This has the effect /// > of ensuring that such transactions are not evicted preferentially to typical /// > transparent or Sapling transactions because of their size. /// /// [ZIP-401]: https://zips.z.cash/zip-0401 pub const MEMPOOL_TRANSACTION_COST_THRESHOLD: u64 = 10_000; /// When a transaction pays a fee less than the conventional fee, /// this low fee penalty is added to its cost for mempool eviction. /// /// See [VerifiedUnminedTx::eviction_weight()] for details. /// /// [ZIP-401]: https://zips.z.cash/zip-0401 const MEMPOOL_TRANSACTION_LOW_FEE_PENALTY: u64 = 40_000; /// A unique identifier for an unmined transaction, regardless of version. /// /// "The transaction ID of a version 4 or earlier transaction is the SHA-256d hash /// of the transaction encoding in the pre-v5 format described above. /// /// The transaction ID of a version 5 transaction is as defined in [ZIP-244]. /// /// A v5 transaction also has a wtxid (used for example in the peer-to-peer protocol) /// as defined in [ZIP-239]." /// [Spec: Transaction Identifiers] /// /// [ZIP-239]: https://zips.z.cash/zip-0239 /// [ZIP-244]: https://zips.z.cash/zip-0244 /// [Spec: Transaction Identifiers]: https://zips.z.cash/protocol/protocol.pdf#txnidentifiers #[derive(Copy, Clone, Eq, PartialEq, Hash)] #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] pub enum UnminedTxId { /// A legacy unmined transaction identifier. /// /// Used to uniquely identify unmined version 1-4 transactions. /// (After v1-4 transactions are mined, they can be uniquely identified /// using the same [`struct@Hash`].) Legacy(Hash), /// A witnessed unmined transaction identifier. /// /// Used to uniquely identify unmined version 5 transactions. /// (After v5 transactions are mined, they can be uniquely identified /// using only the [`struct@Hash`] in their `WtxId.id`.) /// /// For more details, see [`WtxId`]. Witnessed(WtxId), } impl fmt::Debug for UnminedTxId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { // Logging unmined transaction IDs can leak sensitive user information, // particularly when Zebra is being used as a `lightwalletd` backend. Self::Legacy(_hash) => f.debug_tuple("Legacy").field(&self.to_string()).finish(), Self::Witnessed(_id) => f.debug_tuple("Witnessed").field(&self.to_string()).finish(), } } } impl fmt::Display for UnminedTxId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Legacy(_hash) => f .debug_tuple("transaction::Hash") .field(&"private") .finish(), Witnessed(_id) => f.debug_tuple("WtxId").field(&"private").finish(), } } } impl From for UnminedTxId { fn from(transaction: Transaction) -> Self { // use the ref implementation, to avoid cloning the transaction UnminedTxId::from(&transaction) } } impl From<&Transaction> for UnminedTxId { fn from(transaction: &Transaction) -> Self { match transaction { V1 { .. } | V2 { .. } | V3 { .. } | V4 { .. } => Legacy(transaction.into()), V5 { .. } => Witnessed(transaction.into()), } } } impl From> for UnminedTxId { fn from(transaction: Arc) -> Self { transaction.as_ref().into() } } impl From for UnminedTxId { fn from(wtx_id: WtxId) -> Self { Witnessed(wtx_id) } } impl From<&WtxId> for UnminedTxId { fn from(wtx_id: &WtxId) -> Self { (*wtx_id).into() } } impl UnminedTxId { /// Create a new [`UnminedTxId`] using a v1-v4 legacy transaction ID. /// /// # Correctness /// /// This method must only be used for v1-v4 transaction IDs. /// [`struct@Hash`] does not uniquely identify unmined v5 /// transactions. pub fn from_legacy_id(legacy_tx_id: Hash) -> UnminedTxId { Legacy(legacy_tx_id) } /// Return the unique ID that will be used if this transaction gets mined into a block. /// /// # Correctness /// /// For v1-v4 transactions, this method returns an ID which changes /// if this transaction's effects (spends and outputs) change, or /// if its authorizing data changes (signatures, proofs, and scripts). /// /// But for v5 transactions, this ID uniquely identifies the transaction's effects. pub fn mined_id(&self) -> Hash { match self { Legacy(legacy_id) => *legacy_id, Witnessed(wtx_id) => wtx_id.id, } } /// Returns a mutable reference to the unique ID /// that will be used if this transaction gets mined into a block. /// /// See [`Self::mined_id`] for details. #[cfg(any(test, feature = "proptest-impl"))] pub fn mined_id_mut(&mut self) -> &mut Hash { match self { Legacy(legacy_id) => legacy_id, Witnessed(wtx_id) => &mut wtx_id.id, } } /// Return the digest of this transaction's authorizing data, /// (signatures, proofs, and scripts), if it is a v5 transaction. pub fn auth_digest(&self) -> Option { match self { Legacy(_) => None, Witnessed(wtx_id) => Some(wtx_id.auth_digest), } } /// Returns a mutable reference to the digest of this transaction's authorizing data, /// (signatures, proofs, and scripts), if it is a v5 transaction. #[cfg(any(test, feature = "proptest-impl"))] pub fn auth_digest_mut(&mut self) -> Option<&mut AuthDigest> { match self { Legacy(_) => None, Witnessed(wtx_id) => Some(&mut wtx_id.auth_digest), } } } /// An unmined transaction, and its pre-calculated unique identifying ID. /// /// This transaction has been structurally verified. /// (But it might still need semantic or contextual verification.) #[derive(Clone, Eq, PartialEq)] pub struct UnminedTx { /// The unmined transaction itself. pub transaction: Arc, /// A unique identifier for this unmined transaction. pub id: UnminedTxId, /// The size in bytes of the serialized transaction data pub size: usize, /// The conventional fee for this transaction, as defined by [ZIP-317]. /// /// [ZIP-317]: https://zips.z.cash/zip-0317#fee-calculation pub conventional_fee: Amount, } impl fmt::Debug for UnminedTx { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Logging unmined transactions can leak sensitive user information, // particularly when Zebra is being used as a `lightwalletd` backend. f.debug_tuple("UnminedTx").field(&"private").finish() } } impl fmt::Display for UnminedTx { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("UnminedTx").field(&"private").finish() } } // Each of these conversions is implemented slightly differently, // to avoid cloning the transaction where possible. impl From for UnminedTx { fn from(transaction: Transaction) -> Self { let size = transaction.zcash_serialized_size(); let conventional_fee = zip317::conventional_fee(&transaction); // The borrow is actually needed to avoid taking ownership #[allow(clippy::needless_borrow)] Self { id: (&transaction).into(), size, conventional_fee, transaction: Arc::new(transaction), } } } impl From<&Transaction> for UnminedTx { fn from(transaction: &Transaction) -> Self { let size = transaction.zcash_serialized_size(); let conventional_fee = zip317::conventional_fee(transaction); Self { id: transaction.into(), size, conventional_fee, transaction: Arc::new(transaction.clone()), } } } impl From> for UnminedTx { fn from(transaction: Arc) -> Self { let size = transaction.zcash_serialized_size(); let conventional_fee = zip317::conventional_fee(&transaction); Self { id: transaction.as_ref().into(), size, conventional_fee, transaction, } } } impl From<&Arc> for UnminedTx { fn from(transaction: &Arc) -> Self { let size = transaction.zcash_serialized_size(); let conventional_fee = zip317::conventional_fee(transaction); Self { id: transaction.as_ref().into(), size, conventional_fee, transaction: transaction.clone(), } } } /// A verified unmined transaction, and the corresponding transaction fee. /// /// This transaction has been fully verified, in the context of the mempool. // // This struct can't be `Eq`, because it contains a `f32`. #[derive(Clone, PartialEq)] #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] pub struct VerifiedUnminedTx { /// The unmined transaction. pub transaction: UnminedTx, /// The transaction fee for this unmined transaction. pub miner_fee: Amount, /// The number of legacy signature operations in this transaction's /// transparent inputs and outputs. pub legacy_sigop_count: u64, /// The number of unpaid actions for `transaction`, /// as defined by [ZIP-317] for block production. /// /// The number of actions is limited by [`MAX_BLOCK_BYTES`], so it fits in a u32. /// /// [ZIP-317]: https://zips.z.cash/zip-0317#block-production pub unpaid_actions: u32, /// The fee weight ratio for `transaction`, as defined by [ZIP-317] for block production. /// /// This is not consensus-critical, so we use `f32` for efficient calculations /// when the mempool holds a large number of transactions. /// /// [ZIP-317]: https://zips.z.cash/zip-0317#block-production pub fee_weight_ratio: f32, } impl fmt::Debug for VerifiedUnminedTx { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Logging unmined transactions can leak sensitive user information, // particularly when Zebra is being used as a `lightwalletd` backend. f.debug_tuple("VerifiedUnminedTx") .field(&"private") .finish() } } impl fmt::Display for VerifiedUnminedTx { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("VerifiedUnminedTx") .field(&"private") .finish() } } impl VerifiedUnminedTx { /// Create a new verified unmined transaction from an unmined transaction, /// its miner fee, and its legacy sigop count. pub fn new( transaction: UnminedTx, miner_fee: Amount, legacy_sigop_count: u64, ) -> Result { let fee_weight_ratio = zip317::conventional_fee_weight_ratio(&transaction, miner_fee); let unpaid_actions = zip317::unpaid_actions(&transaction, miner_fee); zip317::mempool_checks(unpaid_actions, miner_fee, transaction.size)?; Ok(Self { transaction, miner_fee, legacy_sigop_count, fee_weight_ratio, unpaid_actions, }) } /// Returns `true` if the transaction pays at least the [ZIP-317] conventional fee. /// /// [ZIP-317]: https://zips.z.cash/zip-0317#mempool-size-limiting pub fn pays_conventional_fee(&self) -> bool { self.miner_fee >= self.transaction.conventional_fee } /// The cost in bytes of the transaction, as defined in [ZIP-401]. /// /// A reflection of the work done by the network in processing them (proof /// and signature verification; networking overheads; size of in-memory data /// structures). /// /// > Each transaction has a cost, which is an integer defined as... /// /// [ZIP-401]: https://zips.z.cash/zip-0401 pub fn cost(&self) -> u64 { std::cmp::max( u64::try_from(self.transaction.size).expect("fits in u64"), MEMPOOL_TRANSACTION_COST_THRESHOLD, ) } /// The computed _eviction weight_ of a verified unmined transaction as part /// of the mempool set, as defined in [ZIP-317] and [ZIP-401]. /// /// # Standard Rule /// /// > Each transaction also has an *eviction weight*, which is *cost* + *low_fee_penalty*, /// > where *low_fee_penalty* is 40000 if the transaction pays a fee less than the /// > conventional fee, otherwise 0. The conventional fee is currently defined in /// > [ZIP-317]. /// /// > zcashd and zebrad limit the size of the mempool as described in [ZIP-401]. /// > This specifies a low fee penalty that is added to the "eviction weight" if the transaction /// > pays a fee less than the conventional transaction fee. This threshold is /// > modified to use the new conventional fee formula. /// /// [ZIP-317]: https://zips.z.cash/zip-0317#mempool-size-limiting /// [ZIP-401]: https://zips.z.cash/zip-0401 pub fn eviction_weight(&self) -> u64 { let mut cost = self.cost(); if !self.pays_conventional_fee() { cost += MEMPOOL_TRANSACTION_LOW_FEE_PENALTY } cost } }