use std::cmp::max;
use num_integer::div_ceil;
use thiserror::Error;
use crate::{
amount::{Amount, NonNegative},
block::MAX_BLOCK_BYTES,
serialization::ZcashSerialize,
transaction::{Transaction, UnminedTx},
};
#[cfg(test)]
mod tests;
const MARGINAL_FEE: u64 = 5_000;
const GRACE_ACTIONS: u32 = 2;
const P2PKH_STANDARD_INPUT_SIZE: usize = 150;
const P2PKH_STANDARD_OUTPUT_SIZE: usize = 34;
const BLOCK_PRODUCTION_WEIGHT_RATIO_CAP: f32 = 4.0;
const MIN_BLOCK_PRODUCTION_SUBSTITUTE_FEE: i64 = 1;
pub const BLOCK_UNPAID_ACTION_LIMIT: u32 = 0;
pub const MIN_MEMPOOL_TX_FEE_RATE: usize = 100;
pub const MEMPOOL_TX_FEE_REQUIREMENT_CAP: usize = 1000;
pub fn conventional_fee(transaction: &Transaction) -> Amount<NonNegative> {
let marginal_fee: Amount<NonNegative> = MARGINAL_FEE.try_into().expect("fits in amount");
let conventional_fee = marginal_fee * conventional_actions(transaction).into();
conventional_fee.expect("conventional fee is positive and limited by serialized size limit")
}
pub fn unpaid_actions(transaction: &UnminedTx, miner_fee: Amount<NonNegative>) -> u32 {
let conventional_actions = conventional_actions(&transaction.transaction);
let marginal_fee_weight_ratio = miner_fee / MARGINAL_FEE;
let marginal_fee_weight_ratio: i64 = marginal_fee_weight_ratio
.expect("marginal fee is not zero")
.into();
let unpaid_actions = i64::from(conventional_actions) - marginal_fee_weight_ratio;
unpaid_actions.try_into().unwrap_or_default()
}
pub fn conventional_fee_weight_ratio(
transaction: &UnminedTx,
miner_fee: Amount<NonNegative>,
) -> f32 {
assert!(
MIN_BLOCK_PRODUCTION_SUBSTITUTE_FEE as f32 / MAX_BLOCK_BYTES as f32 > 0.0,
"invalid block production constants: the minimum fee ratio must not be zero"
);
let miner_fee = max(miner_fee.into(), MIN_BLOCK_PRODUCTION_SUBSTITUTE_FEE) as f32;
let conventional_fee = i64::from(transaction.conventional_fee) as f32;
let uncapped_weight = miner_fee / conventional_fee;
uncapped_weight.min(BLOCK_PRODUCTION_WEIGHT_RATIO_CAP)
}
pub fn conventional_actions(transaction: &Transaction) -> u32 {
let tx_in_total_size: usize = transaction
.inputs()
.iter()
.map(|input| input.zcash_serialized_size())
.sum();
let tx_out_total_size: usize = transaction
.outputs()
.iter()
.map(|output| output.zcash_serialized_size())
.sum();
let n_join_split = transaction.joinsplit_count();
let n_spends_sapling = transaction.sapling_spends_per_anchor().count();
let n_outputs_sapling = transaction.sapling_outputs().count();
let n_actions_orchard = transaction.orchard_actions().count();
let tx_in_logical_actions = div_ceil(tx_in_total_size, P2PKH_STANDARD_INPUT_SIZE);
let tx_out_logical_actions = div_ceil(tx_out_total_size, P2PKH_STANDARD_OUTPUT_SIZE);
let logical_actions = max(tx_in_logical_actions, tx_out_logical_actions)
+ 2 * n_join_split
+ max(n_spends_sapling, n_outputs_sapling)
+ n_actions_orchard;
let logical_actions: u32 = logical_actions
.try_into()
.expect("transaction items are limited by serialized size limit");
max(GRACE_ACTIONS, logical_actions)
}
pub fn mempool_checks(
unpaid_actions: u32,
miner_fee: Amount<NonNegative>,
transaction_size: usize,
) -> Result<(), Error> {
if unpaid_actions > BLOCK_UNPAID_ACTION_LIMIT {
return Err(Error::UnpaidActions);
}
const KILOBYTE: usize = 1000;
let max_block_size = usize::try_from(MAX_BLOCK_BYTES).map_err(|_| Error::InvalidMinFee)?;
assert!(MIN_MEMPOOL_TX_FEE_RATE < usize::MAX / max_block_size);
let min_fee: u64 = (MIN_MEMPOOL_TX_FEE_RATE * transaction_size / KILOBYTE)
.clamp(MIN_MEMPOOL_TX_FEE_RATE, MEMPOOL_TX_FEE_REQUIREMENT_CAP)
.try_into()
.map_err(|_| Error::InvalidMinFee)?;
let min_fee = Amount::<NonNegative>::try_from(min_fee).map_err(|_| Error::InvalidMinFee)?;
if miner_fee < min_fee {
return Err(Error::FeeBelowMinimumRate);
}
Ok(())
}
#[derive(Error, Clone, Debug, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum Error {
#[error("Unpaid actions is higher than the limit")]
UnpaidActions,
#[error("Transaction fee is below the minimum fee rate")]
FeeBelowMinimumRate,
#[error("Minimum fee could not be calculated")]
InvalidMinFee,
}