use blvm_node::config::{MempoolPolicyConfig, RbfConfig, RbfMode};
use blvm_node::node::mempool::MempoolManager;
use blvm_protocol::block::calculate_tx_id;
use blvm_protocol::{OutPoint, Transaction, TransactionInput, TransactionOutput, UTXO, UtxoSet};
use std::sync::Arc;
use tokio::sync::Mutex as TokioMutex;
fn create_rbf_tx(input_value: u64, output_value: u64) -> Transaction {
create_rbf_tx_spending(
OutPoint {
hash: [1; 32],
index: 0,
},
output_value,
)
}
fn admit_bare_op1(mempool: &MempoolManager) {
let mut policy = MempoolPolicyConfig::default();
policy.require_standard = false;
mempool.set_policy_config(Some(policy));
}
fn create_rbf_tx_spending(prevout: OutPoint, output_value: u64) -> Transaction {
Transaction {
version: 1,
inputs: blvm_protocol::tx_inputs![TransactionInput {
prevout,
script_sig: vec![],
sequence: 0xfffffffd, }],
outputs: blvm_protocol::tx_outputs![TransactionOutput {
value: output_value as i64,
script_pubkey: vec![0x51, 0x61, 0x61, 0x61, 0x61],
}],
lock_time: 0,
}
}
fn create_test_utxo_set() -> UtxoSet {
let mut utxo_set = UtxoSet::default();
utxo_set.insert(
OutPoint {
hash: [1; 32],
index: 0,
},
Arc::new(UTXO {
value: 100_000,
script_pubkey: vec![blvm_protocol::opcodes::OP_1].into(),
height: 0,
is_coinbase: false,
}),
);
utxo_set
}
#[test]
fn test_rbf_mode_disabled() {
let mempool = MempoolManager::new();
let rbf_config = RbfConfig::with_mode(RbfMode::Disabled);
mempool.set_rbf_config(Some(rbf_config));
let existing_tx = create_rbf_tx(100_000, 90_000); let new_tx = create_rbf_tx(100_000, 80_000);
let utxo_set = create_test_utxo_set();
let result = mempool.check_rbf_replacement(&new_tx, &existing_tx, &utxo_set, None);
assert!(result.is_ok());
assert!(!result.unwrap(), "RBF should be disabled");
}
#[test]
fn test_rbf_mode_conservative() {
let mempool = MempoolManager::new();
let rbf_config = RbfConfig::with_mode(RbfMode::Conservative);
mempool.set_rbf_config(Some(rbf_config));
let existing_tx = create_rbf_tx(100_000, 90_000); let new_tx = create_rbf_tx(100_000, 70_000);
let utxo_set = create_test_utxo_set();
let result = mempool.check_rbf_replacement(&new_tx, &existing_tx, &utxo_set, None);
assert!(result.is_ok());
}
#[test]
fn test_rbf_mode_standard() {
let mempool = MempoolManager::new();
let rbf_config = RbfConfig::with_mode(RbfMode::Standard);
mempool.set_rbf_config(Some(rbf_config));
let existing_tx = create_rbf_tx(100_000, 90_000); let new_tx = create_rbf_tx(100_000, 88_000);
let utxo_set = create_test_utxo_set();
let result = mempool.check_rbf_replacement(&new_tx, &existing_tx, &utxo_set, None);
assert!(result.is_ok());
}
#[test]
fn test_rbf_mode_aggressive() {
let mempool = MempoolManager::new();
let rbf_config = RbfConfig::with_mode(RbfMode::Aggressive);
mempool.set_rbf_config(Some(rbf_config));
let existing_tx = create_rbf_tx(100_000, 90_000); let new_tx = create_rbf_tx(100_000, 89_500);
let utxo_set = create_test_utxo_set();
let result = mempool.check_rbf_replacement(&new_tx, &existing_tx, &utxo_set, None);
assert!(result.is_ok());
}
#[test]
fn test_rbf_replacement_count_limit() {
let mut rbf_config = RbfConfig::with_mode(RbfMode::Standard);
rbf_config.max_replacements_per_tx = 2;
assert_eq!(rbf_config.max_replacements_per_tx, 2);
assert_eq!(rbf_config.mode, RbfMode::Standard);
}
#[test]
fn test_rbf_cooldown_period() {
let mut rbf_config = RbfConfig::with_mode(RbfMode::Standard);
rbf_config.cooldown_seconds = 60;
assert_eq!(rbf_config.cooldown_seconds, 60);
assert_eq!(rbf_config.mode, RbfMode::Standard);
}
#[test]
fn test_rbf_fee_rate_multiplier() {
let mempool = MempoolManager::new();
let mut rbf_config = RbfConfig::with_mode(RbfMode::Standard);
rbf_config.min_fee_rate_multiplier = 1.5; mempool.set_rbf_config(Some(rbf_config));
let existing_tx = create_rbf_tx(100_000, 90_000); let new_tx_insufficient = create_rbf_tx(100_000, 91_000); let new_tx_sufficient = create_rbf_tx(100_000, 85_000);
let utxo_set = create_test_utxo_set();
let result1 =
mempool.check_rbf_replacement(&new_tx_insufficient, &existing_tx, &utxo_set, None);
assert!(result1.is_ok());
let result2 = mempool.check_rbf_replacement(&new_tx_sufficient, &existing_tx, &utxo_set, None);
assert!(result2.is_ok());
}
#[test]
fn test_rbf_absolute_fee_bump() {
let mempool = MempoolManager::new();
let mut rbf_config = RbfConfig::with_mode(RbfMode::Standard);
rbf_config.min_fee_bump_satoshis = 5000; mempool.set_rbf_config(Some(rbf_config));
let existing_tx = create_rbf_tx(100_000, 90_000); let new_tx_insufficient = create_rbf_tx(100_000, 89_500); let new_tx_sufficient = create_rbf_tx(100_000, 84_000);
let utxo_set = create_test_utxo_set();
let result1 =
mempool.check_rbf_replacement(&new_tx_insufficient, &existing_tx, &utxo_set, None);
assert!(result1.is_ok());
let result2 = mempool.check_rbf_replacement(&new_tx_sufficient, &existing_tx, &utxo_set, None);
assert!(result2.is_ok());
}
#[test]
fn test_rbf_config_with_mode() {
let conservative = RbfConfig::with_mode(RbfMode::Conservative);
assert_eq!(conservative.mode, RbfMode::Conservative);
assert_eq!(conservative.min_fee_rate_multiplier, 2.0);
assert_eq!(conservative.min_fee_bump_satoshis, 5000);
assert_eq!(conservative.min_confirmations, 1);
let aggressive = RbfConfig::with_mode(RbfMode::Aggressive);
assert_eq!(aggressive.mode, RbfMode::Aggressive);
assert_eq!(aggressive.min_fee_rate_multiplier, 1.05);
assert_eq!(aggressive.min_fee_bump_satoshis, 500);
assert!(aggressive.allow_package_replacements);
let standard = RbfConfig::with_mode(RbfMode::Standard);
assert_eq!(standard.mode, RbfMode::Standard);
assert_eq!(standard.min_fee_rate_multiplier, 1.0);
assert_eq!(standard.min_fee_bump_satoshis, 1000);
}
#[test]
fn test_rbf_package_replacement_uses_descendant_fees() {
let mempool = MempoolManager::new();
admit_bare_op1(&mempool);
let rbf_config = RbfConfig::with_mode(RbfMode::Aggressive);
mempool.set_rbf_config(Some(rbf_config));
let utxo_set = create_test_utxo_set();
mempool.set_utxo_set_arc(Arc::new(TokioMutex::new(utxo_set.clone())));
let parent = create_rbf_tx(100_000, 85_000);
let parent_hash = calculate_tx_id(&parent);
assert!(mempool.add_transaction(parent.clone()).unwrap());
let child = create_rbf_tx_spending(
OutPoint {
hash: parent_hash,
index: 0,
},
80_000,
);
let child_hash = calculate_tx_id(&child);
assert!(mempool.add_transaction(child).unwrap());
assert_eq!(mempool.size(), 2);
let replacement = create_rbf_tx(100_000, 81_000);
let result = mempool.check_rbf_replacement(&replacement, &parent, &utxo_set, None);
assert!(result.is_ok());
assert!(
!result.unwrap(),
"package replacement should require fees covering displaced descendants"
);
let sufficient = create_rbf_tx(100_000, 78_000);
let result2 = mempool.check_rbf_replacement(&sufficient, &parent, &utxo_set, None);
assert!(result2.is_ok());
assert!(result2.unwrap());
assert!(mempool.add_transaction(sufficient).unwrap());
assert_eq!(mempool.size(), 1);
assert!(mempool.get_transaction(&parent_hash).is_none());
assert!(mempool.get_transaction(&child_hash).is_none());
}
#[test]
fn standard_replacement_pays_for_the_descendant() {
let mempool = MempoolManager::new();
admit_bare_op1(&mempool);
mempool.set_rbf_config(Some(RbfConfig::default()));
let utxo_set = create_test_utxo_set();
mempool.set_utxo_set_arc(Arc::new(TokioMutex::new(utxo_set.clone())));
let parent = create_rbf_tx(100_000, 85_000);
let parent_hash = calculate_tx_id(&parent);
assert!(mempool.add_transaction(parent).unwrap());
let child = create_rbf_tx_spending(
OutPoint {
hash: parent_hash,
index: 0,
},
80_000,
);
let child_hash = calculate_tx_id(&child);
assert!(mempool.add_transaction(child).unwrap());
let cheap = create_rbf_tx(100_000, 81_000);
assert!(!mempool.add_transaction(cheap).unwrap());
assert!(mempool.get_transaction(&parent_hash).is_some());
assert!(mempool.get_transaction(&child_hash).is_some());
assert_eq!(mempool.size(), 2);
let enough = create_rbf_tx(100_000, 78_000);
assert!(mempool.add_transaction(enough).unwrap());
assert_eq!(mempool.size(), 1);
assert!(mempool.get_transaction(&parent_hash).is_none());
assert!(mempool.get_transaction(&child_hash).is_none());
}
#[test]
fn sequence_fffffffe_is_replaced() {
let mempool = MempoolManager::new();
admit_bare_op1(&mempool);
let utxo_set = create_test_utxo_set();
mempool.set_utxo_set_arc(Arc::new(TokioMutex::new(utxo_set)));
let mut existing = create_rbf_tx(100_000, 90_000);
existing.inputs[0].sequence = 0xfffffffe;
let existing_hash = calculate_tx_id(&existing);
assert!(mempool.add_transaction(existing).unwrap());
let replacement = create_rbf_tx(100_000, 70_000);
assert!(mempool.add_transaction(replacement).unwrap());
assert!(mempool.get_transaction(&existing_hash).is_none());
assert_eq!(mempool.size(), 1);
}
#[test]
fn final_child_is_replaced_with_its_parent() {
let mempool = MempoolManager::new();
admit_bare_op1(&mempool);
let utxo_set = create_test_utxo_set();
mempool.set_utxo_set_arc(Arc::new(TokioMutex::new(utxo_set)));
let parent = create_rbf_tx(100_000, 85_000);
let parent_hash = calculate_tx_id(&parent);
assert!(mempool.add_transaction(parent).unwrap());
let mut child = create_rbf_tx_spending(
OutPoint {
hash: parent_hash,
index: 0,
},
80_000,
);
child.inputs[0].sequence = 0xffffffff;
let child_hash = calculate_tx_id(&child);
assert!(mempool.add_transaction(child).unwrap());
let replacement = create_rbf_tx(100_000, 70_000);
assert!(mempool.add_transaction(replacement).unwrap());
assert!(mempool.get_transaction(&parent_hash).is_none());
assert!(mempool.get_transaction(&child_hash).is_none());
assert_eq!(mempool.size(), 1);
}
#[test]
fn replacement_bump_of_300_sats_is_admitted() {
let try_replace = |replacement_output: u64| {
let mempool = MempoolManager::new();
admit_bare_op1(&mempool);
let utxo_set = create_test_utxo_set();
mempool.set_utxo_set_arc(Arc::new(TokioMutex::new(utxo_set)));
let original = create_rbf_tx(100_000, 98_000);
let original_hash = calculate_tx_id(&original);
assert!(mempool.add_transaction(original).unwrap());
let replacement = create_rbf_tx(100_000, replacement_output);
let admitted = mempool.add_transaction(replacement).unwrap();
(admitted, mempool.get_transaction(&original_hash).is_none())
};
let (bump_300, original_gone) = try_replace(97_700);
let (bump_1001, control_gone) = try_replace(96_999);
let (below_original, _) = try_replace(98_001);
eprintln!(
"bump 300 admitted={bump_300} original_removed={original_gone}; bump 1001 admitted={bump_1001} original_removed={control_gone}; fee 1999 admitted={below_original}"
);
assert!(bump_300 && original_gone);
assert!(bump_1001 && control_gone);
assert!(!below_original);
}
#[test]
fn five_percent_fee_increase_is_admitted() {
let try_replace = |replacement_output: u64| {
let mempool = MempoolManager::new();
admit_bare_op1(&mempool);
let utxo_set = create_test_utxo_set();
mempool.set_utxo_set_arc(Arc::new(TokioMutex::new(utxo_set)));
let original = create_rbf_tx(100_000, 90_000);
let original_hash = calculate_tx_id(&original);
assert!(mempool.add_transaction(original).unwrap());
let replacement = create_rbf_tx(100_000, replacement_output);
let admitted = mempool.add_transaction(replacement).unwrap();
(admitted, mempool.get_transaction(&original_hash).is_none())
};
let (five_percent, five_removed) = try_replace(89_500);
let (twenty_percent, twenty_removed) = try_replace(88_000);
let (lower_fee, _) = try_replace(90_001);
eprintln!(
"fee 10500 admitted={five_percent} original_removed={five_removed}; fee 12000 admitted={twenty_percent} original_removed={twenty_removed}; fee 9999 admitted={lower_fee}"
);
assert!(five_percent && five_removed);
assert!(twenty_percent && twenty_removed);
assert!(!lower_fee);
}