blvm-node 0.1.50

Bitcoin Commons BLVM: Minimal Bitcoin node implementation using blvm-protocol and blvm-consensus
//! RBF (Replace-By-Fee) Mode Tests
//!
//! Comprehensive tests for all RBF modes:
//! - Disabled: No replacements allowed
//! - Conservative: Strict rules with higher fee requirements
//! - Standard: BIP125-compliant (default)
//! - Aggressive: Relaxed rules for miners

use blvm_node::config::{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;

/// Create a test transaction with RBF signaling
fn create_rbf_tx(input_value: u64, output_value: u64) -> Transaction {
    create_rbf_tx_spending(
        OutPoint {
            hash: [1; 32],
            index: 0,
        },
        output_value,
    )
}

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: 0xfffffffe, // RBF enabled
        }],
        outputs: blvm_protocol::tx_outputs![TransactionOutput {
            value: output_value as i64,
            script_pubkey: [0x76, 0xa9, 0x14, 0x00].repeat(20), // P2PKH
        }],
        lock_time: 0,
    }
}

/// Create a test UTXO set
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: [0x76, 0xa9, 0x14, 0x00].repeat(20).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); // Fee: 10,000
    let new_tx = create_rbf_tx(100_000, 80_000); // Fee: 20,000 (higher)

    let utxo_set = create_test_utxo_set();
    let result = mempool.check_rbf_replacement(&new_tx, &existing_tx, &utxo_set, None);

    // RBF disabled - should reject
    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); // Fee: 10,000, Fee rate: ~100 sat/vB
    let new_tx = create_rbf_tx(100_000, 70_000); // Fee: 30,000, Fee rate: ~300 sat/vB (3x increase)

    let utxo_set = create_test_utxo_set();
    let result = mempool.check_rbf_replacement(&new_tx, &existing_tx, &utxo_set, None);

    // Conservative mode requires 2x fee rate multiplier and 5000 sat absolute bump
    // 300 / 100 = 3x (passes 2x requirement)
    // 30,000 - 10,000 = 20,000 (passes 5000 sat requirement)
    assert!(result.is_ok());
    // Note: This will also check BIP125 rules, so result depends on full validation
}

#[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); // Fee: 10,000
    let new_tx = create_rbf_tx(100_000, 88_000); // Fee: 12,000 (1.2x fee rate, 2000 sat bump)

    let utxo_set = create_test_utxo_set();
    let result = mempool.check_rbf_replacement(&new_tx, &existing_tx, &utxo_set, None);

    // Standard mode requires 1.1x fee rate multiplier and 1000 sat absolute bump
    // Should pass if BIP125 rules are satisfied
    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); // Fee: 10,000
    let new_tx = create_rbf_tx(100_000, 89_500); // Fee: 10,500 (1.05x fee rate, 500 sat bump)

    let utxo_set = create_test_utxo_set();
    let result = mempool.check_rbf_replacement(&new_tx, &existing_tx, &utxo_set, None);

    // Aggressive mode requires 1.05x fee rate multiplier and 500 sat absolute bump
    // Should pass if BIP125 rules are satisfied
    assert!(result.is_ok());
}

#[test]
fn test_rbf_replacement_count_limit() {
    // Test that replacement count limit is configured correctly
    let mut rbf_config = RbfConfig::with_mode(RbfMode::Standard);
    rbf_config.max_replacements_per_tx = 2; // Allow only 2 replacements

    assert_eq!(rbf_config.max_replacements_per_tx, 2);
    assert_eq!(rbf_config.mode, RbfMode::Standard);
}

#[test]
fn test_rbf_cooldown_period() {
    // Test that cooldown period is configured correctly
    let mut rbf_config = RbfConfig::with_mode(RbfMode::Standard);
    rbf_config.cooldown_seconds = 60; // 60 second cooldown

    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; // Require 50% increase
    mempool.set_rbf_config(Some(rbf_config));

    let existing_tx = create_rbf_tx(100_000, 90_000); // Fee: 10,000, ~100 sat/vB
    let new_tx_insufficient = create_rbf_tx(100_000, 91_000); // Fee: 9,000, ~90 sat/vB (less than 1.5x)
    let new_tx_sufficient = create_rbf_tx(100_000, 85_000); // Fee: 15,000, ~150 sat/vB (1.5x)

    let utxo_set = create_test_utxo_set();

    // Insufficient fee rate increase should fail
    let result1 =
        mempool.check_rbf_replacement(&new_tx_insufficient, &existing_tx, &utxo_set, None);
    assert!(result1.is_ok());
    // Should be rejected due to insufficient fee rate

    // Sufficient fee rate increase should pass (if other checks pass)
    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; // Require 5000 sat absolute bump
    mempool.set_rbf_config(Some(rbf_config));

    let existing_tx = create_rbf_tx(100_000, 90_000); // Fee: 10,000
    let new_tx_insufficient = create_rbf_tx(100_000, 89_500); // Fee: 10,500 (only 500 sat bump)
    let new_tx_sufficient = create_rbf_tx(100_000, 84_000); // Fee: 16,000 (6000 sat bump)

    let utxo_set = create_test_utxo_set();

    // Insufficient absolute fee bump should fail
    let result1 =
        mempool.check_rbf_replacement(&new_tx_insufficient, &existing_tx, &utxo_set, None);
    assert!(result1.is_ok());
    // Should be rejected due to insufficient absolute fee bump

    // Sufficient absolute fee bump should pass (if other checks pass)
    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() {
    // Test that with_mode sets appropriate defaults
    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.1);
    assert_eq!(standard.min_fee_bump_satoshis, 1000);
}

#[test]
fn test_rbf_package_replacement_uses_descendant_fees() {
    let mempool = MempoolManager::new();
    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())));

    // Parent: 15k fee; child: 5k fee (package total 20k).
    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);

    // 20_200 sat fee beats parent alone (15k) but not parent+child package (20k + 500 min bump).
    let replacement = create_rbf_tx(100_000, 79_800);
    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"
    );

    // 21k fee covers the 20k package with aggressive min bump.
    let sufficient = create_rbf_tx(100_000, 79_000);
    let result2 = mempool.check_rbf_replacement(&sufficient, &parent, &utxo_set, None);
    assert!(result2.is_ok());
    assert!(result2.unwrap());

    // Displacement removes parent and child when replacement is accepted.
    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());
}