Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions mining/src/feerate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ impl FeerateEstimator {
/// Returns the feerate value for which the integral area is `frac` of the total area between `lower` and `upper`.
fn quantile(&self, lower: f64, upper: f64, frac: f64) -> f64 {
assert!((0f64..=1f64).contains(&frac));
if lower == upper {
// A collapsed interval has only one quantile; avoid formula roundoff.
return lower;
}
assert!(0.0 < lower && lower <= upper, "{lower}, {upper}");
let (c1, c2) = (self.inclusion_interval, self.total_weight);
if c1 == 0.0 || c2 == 0.0 {
Expand Down
34 changes: 30 additions & 4 deletions mining/src/manager_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1042,7 +1042,7 @@ mod tests {
consensus.add_transaction(child_tx_2.clone(), 3);

// Add to mempool a transaction that spends child_tx_2 (as high priority)
let spending_tx = create_transaction(&child_tx_2, 1_000);
let spending_tx = create_transaction(&child_tx_2, DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE);
let result = mining_manager.validate_and_insert_transaction(
consensus.as_ref(),
spending_tx.clone(),
Expand Down Expand Up @@ -1165,6 +1165,7 @@ mod tests {
validate_and_insert_mutable_transaction(&mining_manager, consensus.as_ref(), tx).unwrap();
}
assert_eq!(mining_manager.get_all_transactions(TransactionQuery::TransactionsOnly).0.len(), TX_COUNT);
let high_fee = MAX_BLOCK_MASS * DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE / 1000;

let heavy_tx_low_fee = {
let mut heavy_tx = create_transaction_with_utxo_entry(TX_COUNT as u32, 0);
Expand All @@ -1182,7 +1183,7 @@ mod tests {
let mut inner_tx = (*(heavy_tx.tx)).clone();
inner_tx.payload = vec![0u8; TX_COUNT / 2 * tx_size - inner_tx.estimate_mem_bytes()];
heavy_tx.tx = inner_tx.into();
heavy_tx.calculated_fee = Some(500_000);
heavy_tx.calculated_fee = Some(high_fee);
heavy_tx
};
validate_and_insert_mutable_transaction(&mining_manager, consensus.as_ref(), heavy_tx_high_fee.clone()).unwrap();
Expand All @@ -1194,12 +1195,37 @@ mod tests {
let mut inner_tx = (*(heavy_tx.tx)).clone();
inner_tx.payload = vec![0u8; size_limit];
heavy_tx.tx = inner_tx.into();
heavy_tx.calculated_fee = Some(500_000);
heavy_tx.calculated_fee = Some(high_fee);
heavy_tx
};
assert!(validate_and_insert_mutable_transaction(&mining_manager, consensus.as_ref(), too_big_tx.clone()).is_err());
}

#[test]
fn test_realtime_feerate_estimations_respect_minimum_standard_feerate() {
let minimum_feerate = DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE as f64 / 1000.0;

for tx_count in [0, 1, 10, 100, 500] {
let consensus = Arc::new(ConsensusMock::new());
let mining_manager = default_mining_manager();

for i in 0..tx_count {
let tx = create_transaction_with_utxo_entry(i, 0);
validate_and_insert_mutable_transaction(&mining_manager, consensus.as_ref(), tx).unwrap();
}

let estimations = mining_manager.get_realtime_feerate_estimations();
for bucket in estimations.ordered_buckets() {
assert!(
bucket.feerate >= minimum_feerate,
"mempool with {tx_count} txs returned bucket feerate {} below minimum standard feerate {}",
bucket.feerate,
minimum_feerate
);
}
}
}

fn validate_and_insert_mutable_transaction(
mining_manager: &MiningManager,
consensus: &dyn ConsensusApi,
Expand Down Expand Up @@ -1470,7 +1496,7 @@ mod tests {

fn create_child_and_parent_txs_and_add_parent_to_consensus(consensus: &Arc<ConsensusMock>) -> Transaction {
let parent_tx = create_transaction_without_input(vec![500 * SOMPI_PER_KASPA]);
let child_tx = create_transaction(&parent_tx, 1000);
let child_tx = create_transaction(&parent_tx, DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE);
consensus.add_transaction(parent_tx, 1);
child_tx
}
Expand Down
31 changes: 23 additions & 8 deletions mining/src/mempool/check_transaction_standard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ mod tests {
use std::sync::Arc;

const RELAY_FEE_TEST_MASS: u64 = 500_000;
const fn default_minimum_relay_fee_for_mass(mass: u64) -> u64 {
mass * DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE / 1000
}

#[test]
fn test_calc_min_required_tx_relay_fee() {
Expand All @@ -155,13 +158,13 @@ mod tests {
name: "100 bytes with default minimum relay fee",
size: 100,
minimum_relay_transaction_fee: DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE,
want: 100,
want: default_minimum_relay_fee_for_mass(100),
},
Test {
name: "large relay fee test mass with default minimum relay fee",
size: RELAY_FEE_TEST_MASS,
minimum_relay_transaction_fee: DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE,
want: RELAY_FEE_TEST_MASS,
want: default_minimum_relay_fee_for_mass(RELAY_FEE_TEST_MASS),
},
Test { name: "1500 bytes with 5000 relay fee", size: 1500, minimum_relay_transaction_fee: 5000, want: 7500 },
Test { name: "1500 bytes with 3000 relay fee", size: 1500, minimum_relay_transaction_fee: 3000, want: 4500 },
Expand Down Expand Up @@ -369,26 +372,38 @@ mod tests {
mtx
}

let insufficient_fee_mass = 10_000;
let insufficient_fee_minimum = default_minimum_relay_fee_for_mass(insufficient_fee_mass);
let insufficient_fee = insufficient_fee_minimum - 1;

let tests = vec![
Test {
name: "standard input with sufficient fee",
mtx: new_mtx(standard_script_public_key.clone(), NonContextualMasses::new(1_000, 500), 1_000),
mtx: new_mtx(
standard_script_public_key.clone(),
NonContextualMasses::new(1_000, 500),
DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE,
),
expected: Expected::Standard,
},
Test {
name: "non-standard input script class",
mtx: new_mtx(non_standard_script_public_key, NonContextualMasses::new(1_000, 1_000), 1_000),
mtx: new_mtx(
non_standard_script_public_key,
NonContextualMasses::new(1_000, 1_000),
DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE,
),
expected: Expected::RejectInputScriptClass,
},
Test {
name: "compute mass triggers insufficient relay fee",
mtx: new_mtx(standard_script_public_key.clone(), NonContextualMasses::new(10_000, 1), 9_999),
expected: Expected::RejectInsufficientFee { fee: 9_999, minimum_fee: 10_000 },
mtx: new_mtx(standard_script_public_key.clone(), NonContextualMasses::new(insufficient_fee_mass, 1), insufficient_fee),
expected: Expected::RejectInsufficientFee { fee: insufficient_fee, minimum_fee: insufficient_fee_minimum },
},
Test {
name: "transient mass triggers insufficient relay fee",
mtx: new_mtx(standard_script_public_key, NonContextualMasses::new(1, 10_000), 9_999),
expected: Expected::RejectInsufficientFee { fee: 9_999, minimum_fee: 10_000 },
mtx: new_mtx(standard_script_public_key, NonContextualMasses::new(1, insufficient_fee_mass), insufficient_fee),
expected: Expected::RejectInsufficientFee { fee: insufficient_fee, minimum_fee: insufficient_fee_minimum },
},
];

Expand Down
3 changes: 2 additions & 1 deletion mining/src/mempool/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ pub(crate) const DEFAULT_MAXIMUM_ORPHAN_TRANSACTION_COUNT: u64 = 500;

/// DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE specifies the minimum transaction fee for a transaction to be accepted to
/// the mempool and relayed. It is specified in sompi per 1kg (or 1000 grams) of transaction mass.
pub(crate) const DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE: u64 = 1000;
/// The default is 100 sompi per gram.
pub(crate) const DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE: u64 = 100_000;

#[derive(Clone, Debug)]
pub struct Config {
Expand Down
173 changes: 112 additions & 61 deletions mining/src/mempool/model/frontier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,9 +435,11 @@ impl Frontier {
#[cfg(test)]
mod tests {
use super::*;
use crate::mempool::config::Config;
use feerate_key::tests::build_feerate_key;
use itertools::Itertools;
use kaspa_consensus_core::{
mass::BlockMassLimits,
subnets::SubnetworkId,
tx::{Transaction, TransactionInput, TransactionOutpoint},
};
Expand Down Expand Up @@ -579,10 +581,31 @@ mod tests {

/// Epsilon used for various test comparisons
const EPS: f64 = 0.000001;
/// Test estimation floors: legacy behavior, current policy, and a higher stress floor.
const TEST_MIN_FEERATES: [f64; 3] = [1.0, 100.0, 1000.0];

fn minimum_standard_feerate() -> f64 {
Config::build_default(1_000, false, BlockMassLimits::with_shared_limit(500_000), DEFAULT_BLOCK_LANE_LIMITS).minimum_feerate()
}

fn assert_valid_feerate_buckets(estimations: &crate::feerate::FeerateEstimations, min_feerate: f64) {
for bucket in estimations.ordered_buckets() {
// Test for the absence of NaN, infinite or zero values in buckets.
assert!(
bucket.feerate.is_normal() && bucket.feerate >= min_feerate - EPS,
"bucket feerate {} must be finite and at least {}",
bucket.feerate,
min_feerate
);
assert!(
bucket.estimated_seconds.is_normal() && bucket.estimated_seconds > 0.0,
"bucket estimated seconds must be a finite number greater than zero"
);
}
}

#[test]
fn test_feerate_estimator() {
const MIN_FEERATE: f64 = 1.0;
let mut rng = thread_rng();
let cap = 2000;
let mut map = HashMap::with_capacity(cap);
Expand All @@ -607,63 +630,44 @@ mod tests {
let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
// We are testing that the build function actually returns and is not looping indefinitely
let estimator = frontier.build_feerate_estimator(args);
let estimations = estimator.calc_estimations(MIN_FEERATE);

let buckets = estimations.ordered_buckets();
// Test for the absence of NaN, infinite or zero values in buckets
for b in buckets.iter() {
assert!(
b.feerate.is_normal() && b.feerate >= MIN_FEERATE - EPS,
"bucket feerate must be a finite number greater or equal to the minimum standard feerate"
);
assert!(
b.estimated_seconds.is_normal() && b.estimated_seconds > 0.0,
"bucket estimated seconds must be a finite number greater than zero"
);
for min_feerate in TEST_MIN_FEERATES {
let estimations = estimator.calc_estimations(min_feerate);
assert_valid_feerate_buckets(&estimations, min_feerate);
dbg!(len, min_feerate, &estimator);
dbg!(estimations);
}
dbg!(len, estimator);
dbg!(estimations);
}
}

#[test]
fn test_constant_feerate_estimator() {
const MIN_FEERATE: f64 = 1.0;
let cap = 20_000;
let mut map = HashMap::with_capacity(cap);
for i in 0..cap as u64 {
let mass: u64 = 1650;
let fee = (mass as f64 * MIN_FEERATE) as u64;
let key = build_feerate_key(fee, mass, i);
map.insert(key.tx.id(), key);
}

for len in [0, 1, 10, 100, 200, 300, 500, 750, cap / 2, (cap * 2) / 3, (cap * 4) / 5, (cap * 5) / 6, cap] {
println!();
println!("Testing a frontier with {} txs...", len.min(cap));
let mut frontier = Frontier::new(1.0);
for item in map.values().take(len).cloned() {
frontier.insert(item).then_some(()).unwrap();
for min_feerate in TEST_MIN_FEERATES {
let mut map = HashMap::with_capacity(cap);
for i in 0..cap as u64 {
let mass: u64 = 1650;
let fee = (mass as f64 * min_feerate) as u64;
let key = build_feerate_key(fee, mass, i);
map.insert(key.tx.id(), key);
}

let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
// We are testing that the build function actually returns and is not looping indefinitely
let estimator = frontier.build_feerate_estimator(args);
let estimations = estimator.calc_estimations(MIN_FEERATE);
let buckets = estimations.ordered_buckets();
// Test for the absence of NaN, infinite or zero values in buckets
for b in buckets.iter() {
assert!(
b.feerate.is_normal() && b.feerate >= MIN_FEERATE - EPS,
"bucket feerate must be a finite number greater or equal to the minimum standard feerate"
);
assert!(
b.estimated_seconds.is_normal() && b.estimated_seconds > 0.0,
"bucket estimated seconds must be a finite number greater than zero"
);
for len in [0, 1, 10, 100, 200, 300, 500, 750, cap / 2, (cap * 2) / 3, (cap * 4) / 5, (cap * 5) / 6, cap] {
println!();
println!("Testing a frontier with {} txs and min feerate {}...", len.min(cap), min_feerate);
let mut frontier = Frontier::new(1.0);
for item in map.values().take(len).cloned() {
frontier.insert(item).then_some(()).unwrap();
}

let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
// We are testing that the build function actually returns and is not looping indefinitely
let estimator = frontier.build_feerate_estimator(args);
let estimations = estimator.calc_estimations(min_feerate);
assert_valid_feerate_buckets(&estimations, min_feerate);
dbg!(len, min_feerate, estimator);
dbg!(estimations);
}
dbg!(len, estimator);
dbg!(estimations);
}
}

Expand Down Expand Up @@ -719,7 +723,6 @@ mod tests {

#[test]
fn test_feerate_estimator_with_less_than_block_capacity() {
const MIN_FEERATE: f64 = 1.0;
let mut map = HashMap::new();
for i in 0..304 {
let mass: u64 = 1650;
Expand All @@ -738,23 +741,71 @@ mod tests {
let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
// We are testing that the build function actually returns and is not looping indefinitely
let estimator = frontier.build_feerate_estimator(args);
let estimations = estimator.calc_estimations(MIN_FEERATE);

let buckets = estimations.ordered_buckets();
// Test for the absence of NaN, infinite or zero values in buckets
for b in buckets.iter() {
// Expect min feerate bcs blocks are not full
assert!(
(b.feerate - MIN_FEERATE).abs() <= EPS,
"bucket feerate is expected to be equal to the minimum standard feerate"
);
for min_feerate in TEST_MIN_FEERATES {
let estimations = estimator.calc_estimations(min_feerate);
let buckets = estimations.ordered_buckets();
// Test for the absence of NaN, infinite or zero values in buckets
for b in buckets.iter() {
// Expect min feerate bcs blocks are not full
assert!(
(b.feerate - min_feerate).abs() <= EPS,
"bucket feerate is expected to be equal to the minimum standard feerate"
);
assert!(
b.estimated_seconds.is_normal() && b.estimated_seconds > 0.0 && b.estimated_seconds <= 1.0,
"bucket estimated seconds must be a finite number greater than zero & less than 1.0"
);
}
dbg!(len, min_feerate, &estimator);
dbg!(estimations);
}
}
}

#[test]
fn test_feerate_estimator_buckets_never_drop_below_minimum_standard_feerate() {
let minimum_feerate = minimum_standard_feerate();
let scenarios = [
("empty", vec![]),
("single below-floor transaction", vec![(1, 1000)]),
("single minimum-feerate transaction", vec![((minimum_feerate * 1000.0) as u64, 1000)]),
("less than one block", (0..100).map(|i| (50_000 + i, 1650)).collect_vec()),
("around one block", (0..304).map(|i| (50_000 + i, 1650)).collect_vec()),
("many low-feerate transactions", (0..2000).map(|i| (1 + i, 1650)).collect_vec()),
(
"high outliers over low-feerate backlog",
(0..2000).map(|i| if i < 10 { (50_000_000_000 + i, 1650) } else { (1 + i, 1650) }).collect_vec(),
),
(
"mixed masses over several blocks",
(0..2000)
.map(|i| {
let mass = if i % 5 == 0 { 90_000 } else { 1650 };
let fee = if i % 11 == 0 { 100_000_000 + i } else { 1 + i };
(fee, mass)
})
.collect_vec(),
),
];

for (name, txs) in scenarios {
let mut frontier = Frontier::new(1.0);
for (i, (fee, mass)) in txs.into_iter().enumerate() {
frontier.insert(build_feerate_key(fee, mass, i as u64)).then_some(()).unwrap();
}

let args = FeerateEstimatorArgs { network_blocks_per_second: 1, maximum_mass_per_block: 500_000 };
let estimator = frontier.build_feerate_estimator(args);
let estimations = estimator.calc_estimations(minimum_feerate);
assert_valid_feerate_buckets(&estimations, minimum_feerate);

for (higher, lower) in estimations.ordered_buckets().into_iter().tuple_windows() {
assert!(
b.estimated_seconds.is_normal() && b.estimated_seconds > 0.0 && b.estimated_seconds <= 1.0,
"bucket estimated seconds must be a finite number greater than zero & less than 1.0"
higher.feerate + EPS >= lower.feerate,
"{name}: buckets must remain ordered by decreasing feerate: {higher:?}, {lower:?}"
);
}
dbg!(len, estimator);
dbg!(estimations);
}
}
}
Loading
Loading