zebra_chain/transaction/unmined/
zip317.rs

1//! An implementation of the [ZIP-317] fee calculations for [UnminedTx]s:
2//! - [conventional fee](https://zips.z.cash/zip-0317#fee-calculation)
3//! - [block production transaction weight](https://zips.z.cash/zip-0317#block-production)
4
5use std::cmp::max;
6
7use num_integer::div_ceil;
8use thiserror::Error;
9
10use crate::{
11    amount::{Amount, NonNegative},
12    block::MAX_BLOCK_BYTES,
13    serialization::ZcashSerialize,
14    transaction::{Transaction, UnminedTx},
15};
16
17#[cfg(test)]
18mod tests;
19
20/// The marginal fee for the ZIP-317 fee calculation, in zatoshis per logical action.
21//
22// TODO: allow Amount<NonNegative> in constants
23const MARGINAL_FEE: u64 = 5_000;
24
25/// The number of grace logical actions allowed by the ZIP-317 fee calculation.
26const GRACE_ACTIONS: u32 = 2;
27
28/// The standard size of p2pkh inputs for the ZIP-317 fee calculation, in bytes.
29const P2PKH_STANDARD_INPUT_SIZE: usize = 150;
30
31/// The standard size of p2pkh outputs for the ZIP-317 fee calculation, in bytes.
32const P2PKH_STANDARD_OUTPUT_SIZE: usize = 34;
33
34/// The recommended weight ratio cap for ZIP-317 block production.
35/// `weight_ratio_cap` in ZIP-317.
36const BLOCK_PRODUCTION_WEIGHT_RATIO_CAP: f32 = 4.0;
37
38/// The minimum fee for the block production weight ratio calculation, in zatoshis.
39/// If a transaction has a lower fee, this value is used instead.
40///
41/// This avoids special handling for transactions with zero weight.
42const MIN_BLOCK_PRODUCTION_SUBSTITUTE_FEE: i64 = 1;
43
44/// If a tx has more than `BLOCK_UNPAID_ACTION_LIMIT` "unpaid actions", it will never be mined by
45/// the [_Recommended algorithm for block template construction_][alg-def], implemented in Zebra
46/// [here][alg-impl].
47///
48/// [alg-def]: https://zips.z.cash/zip-0317#recommended-algorithm-for-block-template-construction
49/// [alg-impl]: https://github.com/zcashfoundation/zebra/blob/95e4d0973caac075b47589f6a05f9d744acd3db3/zebra-rpc/src/methods/get_block_template_rpcs/zip317.rs#L39
50pub const BLOCK_UNPAID_ACTION_LIMIT: u32 = 0;
51
52/// The minimum fee per kilobyte for Zebra mempool transactions.
53/// Also used as the minimum fee for a mempool transaction.
54///
55/// Based on `DEFAULT_MIN_RELAY_TX_FEE` in `zcashd`:
56/// <https://github.com/zcash/zcash/blob/f512291ff20098291442e83713de89bcddc07546/src/main.h#L71-L72>
57///
58/// This is a `usize` to simplify transaction size-based calculation code.
59pub const MIN_MEMPOOL_TX_FEE_RATE: usize = 100;
60
61/// The fee cap for [`MIN_MEMPOOL_TX_FEE_RATE`] minimum required mempool fees.
62///
63/// Based on `LEGACY_DEFAULT_FEE` in `zcashd`:
64/// <https://github.com/zcash/zcash/blob/9e856cfc5b81aa2607a16a23ff5584ea10014de6/src/amount.h#L35-L36>
65///
66/// This is a `usize` to simplify transaction size-based calculation code.
67pub const MEMPOOL_TX_FEE_REQUIREMENT_CAP: usize = 1000;
68
69/// Returns the conventional fee for `transaction`, as defined by [ZIP-317].
70///
71/// [ZIP-317]: https://zips.z.cash/zip-0317#fee-calculation
72pub fn conventional_fee(transaction: &Transaction) -> Amount<NonNegative> {
73    // zcash_primitives checks for non-p2pkh inputs, but Zebra doesn't.
74    // Conventional fees are only used in the standard rules for mempool eviction
75    // and block production, so these implementations are compatible.
76    //
77    // <https://github.com/zcash/librustzcash/blob/main/zcash_primitives/src/transaction/fees/zip317.rs#L135>
78
79    let marginal_fee: Amount<NonNegative> = MARGINAL_FEE.try_into().expect("fits in amount");
80
81    // marginal_fee * max(logical_actions, GRACE_ACTIONS)
82    let conventional_fee = marginal_fee * conventional_actions(transaction).into();
83
84    conventional_fee.expect("conventional fee is positive and limited by serialized size limit")
85}
86
87/// Returns the number of unpaid actions for `transaction`, as defined by [ZIP-317].
88///
89/// [ZIP-317]: https://zips.z.cash/zip-0317#block-production
90pub fn unpaid_actions(transaction: &UnminedTx, miner_fee: Amount<NonNegative>) -> u32 {
91    // max(logical_actions, GRACE_ACTIONS)
92    let conventional_actions = conventional_actions(&transaction.transaction);
93
94    // floor(tx.fee / marginal_fee)
95    let marginal_fee_weight_ratio = miner_fee / MARGINAL_FEE;
96    let marginal_fee_weight_ratio: i64 = marginal_fee_weight_ratio
97        .expect("marginal fee is not zero")
98        .into();
99
100    // max(0, conventional_actions - marginal_fee_weight_ratio)
101    //
102    // Subtracting MAX_MONEY/5000 from a u32 can't go above i64::MAX.
103    let unpaid_actions = i64::from(conventional_actions) - marginal_fee_weight_ratio;
104
105    unpaid_actions.try_into().unwrap_or_default()
106}
107
108/// Returns the block production fee weight ratio for `transaction`, as defined by [ZIP-317].
109///
110/// This calculation will always return a positive, non-zero value.
111///
112/// [ZIP-317]: https://zips.z.cash/zip-0317#block-production
113pub fn conventional_fee_weight_ratio(
114    transaction: &UnminedTx,
115    miner_fee: Amount<NonNegative>,
116) -> f32 {
117    // Check that this function will always return a positive, non-zero value.
118    //
119    // The maximum number of logical actions in a block is actually
120    // MAX_BLOCK_BYTES / MIN_ACTION_BYTES. MIN_ACTION_BYTES is currently
121    // the minimum transparent output size, but future transaction versions could change this.
122    assert!(
123        MIN_BLOCK_PRODUCTION_SUBSTITUTE_FEE as f32 / MAX_BLOCK_BYTES as f32 > 0.0,
124        "invalid block production constants: the minimum fee ratio must not be zero"
125    );
126
127    let miner_fee = max(miner_fee.into(), MIN_BLOCK_PRODUCTION_SUBSTITUTE_FEE) as f32;
128
129    let conventional_fee = i64::from(transaction.conventional_fee) as f32;
130
131    let uncapped_weight = miner_fee / conventional_fee;
132
133    uncapped_weight.min(BLOCK_PRODUCTION_WEIGHT_RATIO_CAP)
134}
135
136/// Returns the conventional actions for `transaction`, `max(logical_actions, GRACE_ACTIONS)`,
137/// as defined by [ZIP-317].
138///
139/// [ZIP-317]: https://zips.z.cash/zip-0317#fee-calculation
140pub fn conventional_actions(transaction: &Transaction) -> u32 {
141    let tx_in_total_size: usize = transaction
142        .inputs()
143        .iter()
144        .map(|input| input.zcash_serialized_size())
145        .sum();
146
147    let tx_out_total_size: usize = transaction
148        .outputs()
149        .iter()
150        .map(|output| output.zcash_serialized_size())
151        .sum();
152
153    let n_join_split = transaction.joinsplit_count();
154    let n_spends_sapling = transaction.sapling_spends_per_anchor().count();
155    let n_outputs_sapling = transaction.sapling_outputs().count();
156    let n_actions_orchard = transaction.orchard_actions().count();
157
158    let tx_in_logical_actions = div_ceil(tx_in_total_size, P2PKH_STANDARD_INPUT_SIZE);
159    let tx_out_logical_actions = div_ceil(tx_out_total_size, P2PKH_STANDARD_OUTPUT_SIZE);
160
161    let logical_actions = max(tx_in_logical_actions, tx_out_logical_actions)
162        + 2 * n_join_split
163        + max(n_spends_sapling, n_outputs_sapling)
164        + n_actions_orchard;
165    let logical_actions: u32 = logical_actions
166        .try_into()
167        .expect("transaction items are limited by serialized size limit");
168
169    max(GRACE_ACTIONS, logical_actions)
170}
171
172/// Make ZIP-317 checks before inserting a transaction into the mempool.
173pub fn mempool_checks(
174    unpaid_actions: u32,
175    miner_fee: Amount<NonNegative>,
176    transaction_size: usize,
177) -> Result<(), Error> {
178    // # Standard Rule
179    //
180    // > If a transaction has more than `block_unpaid_action_limit` "unpaid actions" as defined by the
181    // > Recommended algorithm for block template construction, it will never be mined by that algorithm.
182    // > Nodes MAY drop these transactions.
183    //
184    // <https://zips.z.cash/zip-0317#transaction-relaying>
185    if unpaid_actions > BLOCK_UNPAID_ACTION_LIMIT {
186        return Err(Error::UnpaidActions);
187    }
188
189    // # Standard Rule
190    //
191    // > Nodes that normally relay transactions are expected to do so for transactions that pay at least the
192    // > conventional fee as specified in this ZIP.
193    //
194    // <https://zips.z.cash/zip-0317#transaction-relaying>
195    //
196    // In Zebra, we use a similar minimum fee rate to `zcashd` v5.5.0 and later.
197    // Transactions must pay a fee of at least 100 zatoshis per 1000 bytes of serialized size,
198    // with a maximum fee of 1000 zatoshis.
199    //
200    // <https://github.com/zcash/zcash/blob/9e856cfc5b81aa2607a16a23ff5584ea10014de6/src/amount.cpp#L24-L37>
201    //
202    // In zcashd this is `DEFAULT_MIN_RELAY_TX_FEE` and `LEGACY_DEFAULT_FEE`:
203    // <https://github.com/zcash/zcash/blob/f512291ff20098291442e83713de89bcddc07546/src/main.h#L71-L72>
204    // <https://github.com/zcash/zcash/blob/9e856cfc5b81aa2607a16a23ff5584ea10014de6/src/amount.h#L35-L36>
205    //
206    // ## Note
207    //
208    // If the check above for the maximum number of unpaid actions passes with
209    // [`BLOCK_UNPAID_ACTION_LIMIT`] set to zero, then there is no way for the legacy check below to
210    // fail. This renders the legacy check redundant in that case.
211
212    const KILOBYTE: usize = 1000;
213
214    // This calculation can't overflow, because transactions are limited to 2 MB,
215    // and usize is at least 4 GB.
216    assert!(
217        MIN_MEMPOOL_TX_FEE_RATE
218            < usize::MAX / usize::try_from(MAX_BLOCK_BYTES).expect("constant fits in usize"),
219        "the fee rate multiplication must never overflow",
220    );
221
222    let min_fee = (MIN_MEMPOOL_TX_FEE_RATE * transaction_size / KILOBYTE)
223        .clamp(MIN_MEMPOOL_TX_FEE_RATE, MEMPOOL_TX_FEE_REQUIREMENT_CAP);
224    let min_fee: u64 = min_fee
225        .try_into()
226        .expect("clamped value always fits in u64");
227    let min_fee: Amount<NonNegative> = min_fee.try_into().expect("clamped value is positive");
228
229    if miner_fee < min_fee {
230        return Err(Error::FeeBelowMinimumRate);
231    }
232
233    Ok(())
234}
235
236/// Errors related to ZIP-317.
237#[derive(Error, Copy, Clone, Debug, PartialEq, Eq)]
238#[allow(missing_docs)]
239pub enum Error {
240    #[error("Unpaid actions is higher than the limit")]
241    UnpaidActions,
242
243    #[error("Transaction fee is below the minimum fee rate")]
244    FeeBelowMinimumRate,
245}