Skip to content

Commit f9e0767

Browse files
Add RBF integration tests with multi-node setup
- Test mempool-only RBF handling and balance adjustments. - Test RBF transactions confirmed in block, ensuring stale unconfirmed txs are removed. - Introduce `distribute_funds_unconfirmed` for creating unconfirmed outputs.
1 parent 98ca264 commit f9e0767

File tree

2 files changed

+249
-6
lines changed

2 files changed

+249
-6
lines changed

tests/common/mod.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use lightning_persister::fs_store::FilesystemStore;
3131

3232
use bitcoin::hashes::sha256::Hash as Sha256;
3333
use bitcoin::hashes::Hash;
34-
use bitcoin::{Address, Amount, Network, OutPoint, Txid};
34+
use bitcoin::{Address, Amount, Network, OutPoint, Transaction, Txid};
3535

3636
use electrsd::corepc_node::Client as BitcoindClient;
3737
use electrsd::corepc_node::Node as BitcoinD;
@@ -487,12 +487,33 @@ where
487487
pub(crate) fn premine_and_distribute_funds<E: ElectrumApi>(
488488
bitcoind: &BitcoindClient, electrs: &E, addrs: Vec<Address>, amount: Amount,
489489
) {
490+
premine_blocks(bitcoind, electrs);
491+
492+
distribute_funds(bitcoind, electrs, addrs, amount);
493+
}
494+
495+
pub(crate) fn premine_blocks<E: ElectrumApi>(bitcoind: &BitcoindClient, electrs: &E) {
490496
let _ = bitcoind.create_wallet("ldk_node_test");
491497
let _ = bitcoind.load_wallet("ldk_node_test");
492498
generate_blocks_and_wait(bitcoind, electrs, 101);
499+
}
493500

494-
let amounts: HashMap<String, f64> =
495-
addrs.iter().map(|addr| (addr.to_string(), amount.to_btc())).collect();
501+
pub(crate) fn distribute_funds<E: ElectrumApi>(
502+
bitcoind: &BitcoindClient, electrs: &E, addrs: Vec<Address>, amount: Amount,
503+
) -> Txid {
504+
let address_txid_map = distribute_funds_unconfirmed(bitcoind, electrs, addrs, amount);
505+
generate_blocks_and_wait(bitcoind, electrs, 1);
506+
507+
address_txid_map
508+
}
509+
510+
pub(crate) fn distribute_funds_unconfirmed<E: ElectrumApi>(
511+
bitcoind: &BitcoindClient, electrs: &E, addrs: Vec<Address>, amount: Amount,
512+
) -> Txid {
513+
let mut amounts = HashMap::<String, f64>::new();
514+
for addr in &addrs {
515+
amounts.insert(addr.to_string(), amount.to_btc());
516+
}
496517

497518
let empty_account = json!("");
498519
let amounts_json = json!(amounts);
@@ -505,7 +526,12 @@ pub(crate) fn premine_and_distribute_funds<E: ElectrumApi>(
505526
.unwrap();
506527

507528
wait_for_tx(electrs, txid);
508-
generate_blocks_and_wait(bitcoind, electrs, 1);
529+
530+
txid
531+
}
532+
533+
pub(crate) fn get_transaction<E: ElectrumApi>(electrs: &E, txid: Txid) -> Transaction {
534+
electrs.transaction_get(&txid).unwrap()
509535
}
510536

511537
pub fn open_channel(

tests/integration_tests_rust.rs

Lines changed: 219 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod common;
1010
use common::{
1111
do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event, expect_event,
1212
expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait,
13+
get_transaction,
1314
logging::{init_log_logger, validate_log_entry, TestLogWriter},
1415
open_channel, premine_and_distribute_funds, random_config, random_listening_addresses,
1516
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx,
@@ -32,15 +33,18 @@ use lightning_invoice::{Bolt11InvoiceDescription, Description};
3233
use lightning_types::payment::PaymentPreimage;
3334

3435
use bitcoin::address::NetworkUnchecked;
36+
use bitcoin::hashes::hex::FromHex;
3537
use bitcoin::hashes::sha256::Hash as Sha256Hash;
3638
use bitcoin::hashes::Hash;
37-
use bitcoin::Address;
38-
use bitcoin::Amount;
39+
use bitcoin::{Address, Amount, ScriptBuf, Sequence, Transaction, Witness};
3940
use log::LevelFilter;
4041

42+
use std::collections::HashSet;
4143
use std::str::FromStr;
4244
use std::sync::Arc;
4345

46+
use crate::common::{distribute_funds_unconfirmed, premine_blocks};
47+
4448
#[test]
4549
fn channel_full_cycle() {
4650
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
@@ -669,6 +673,219 @@ fn onchain_wallet_recovery() {
669673
);
670674
}
671675

676+
#[test]
677+
fn test_rbf_via_mempool() {
678+
run_rbf_test(false);
679+
}
680+
681+
#[test]
682+
fn test_rbf_via_direct_block_insertion() {
683+
run_rbf_test(true);
684+
}
685+
686+
// `is_insert_block`:
687+
// - `true`: transaction is mined immediately (no mempool), testing confirmed-Tx handling.
688+
// - `false`: transaction stays in mempool until confirmation, testing unconfirmed-Tx handling.
689+
fn run_rbf_test(is_insert_block: bool) {
690+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
691+
let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(&bitcoind);
692+
let chain_source_electrsd = TestChainSource::Electrum(&electrsd);
693+
let chain_source_esplora = TestChainSource::Esplora(&electrsd);
694+
695+
macro_rules! config_node {
696+
($chain_source: expr, $anchor_channels: expr) => {{
697+
let config_a = random_config($anchor_channels);
698+
let node = setup_node(&$chain_source, config_a, None);
699+
node
700+
}};
701+
}
702+
let anchor_channels = false;
703+
let nodes = vec![
704+
config_node!(chain_source_electrsd, anchor_channels),
705+
config_node!(chain_source_bitcoind, anchor_channels),
706+
config_node!(chain_source_esplora, anchor_channels),
707+
];
708+
709+
let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client);
710+
premine_blocks(bitcoind, electrs);
711+
712+
// Helpers declaration before starting the test
713+
let all_addrs =
714+
nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect::<Vec<_>>();
715+
let amount_sat = 2_100_000;
716+
let mut txid;
717+
macro_rules! distribute_funds_all_nodes {
718+
() => {
719+
txid = distribute_funds_unconfirmed(
720+
bitcoind,
721+
electrs,
722+
all_addrs.clone(),
723+
Amount::from_sat(amount_sat),
724+
);
725+
};
726+
}
727+
728+
let mut tx;
729+
let scripts_buf: HashSet<ScriptBuf> =
730+
all_addrs.iter().map(|addr| addr.script_pubkey()).collect();
731+
let mut fee_output_index;
732+
macro_rules! prepare_rbf {
733+
() => {
734+
tx = get_transaction(electrs, txid);
735+
736+
let mut option_fee_output_index = None;
737+
for (index, output) in tx.output.iter().enumerate() {
738+
if !scripts_buf.contains(&output.script_pubkey) {
739+
option_fee_output_index = Some(index);
740+
break;
741+
}
742+
}
743+
fee_output_index = option_fee_output_index.expect(
744+
"No output available for fee pumping. Need at least one output not being modified.",
745+
);
746+
};
747+
}
748+
749+
let mut bump_fee_amount_sat;
750+
macro_rules! bump_fee_rbf_and_public_transaction {
751+
() => {
752+
bump_fee_amount_sat = tx.vsize() as u64;
753+
let attempts = 5;
754+
for _attempt in 0..attempts {
755+
bump_fee!();
756+
println!("Bumping fee to {} sats", bump_fee_amount_sat);
757+
println!("Transaction ID: {}", tx.compute_txid());
758+
match bitcoind.send_raw_transaction(&tx) {
759+
Ok(res) => {
760+
// Mine a block immediately so the transaction is confirmed
761+
// before any node identifies it as a transaction that was in the mempool.
762+
if is_insert_block {
763+
generate_blocks_and_wait(bitcoind, electrs, 1);
764+
}
765+
let new_txid = res.0.parse().unwrap();
766+
wait_for_tx(electrs, new_txid);
767+
break;
768+
},
769+
Err(_) => {
770+
if _attempt == attempts - 1 {
771+
panic!("Failed to pump fee after {} attempts", attempts);
772+
}
773+
774+
bump_fee_amount_sat += bump_fee_amount_sat * 5;
775+
if tx.output[fee_output_index].value.to_sat() < bump_fee_amount_sat {
776+
panic!("Insufficient funds to increase fee");
777+
}
778+
},
779+
}
780+
}
781+
};
782+
}
783+
784+
macro_rules! bump_fee {
785+
() => {
786+
let fee_output = &mut tx.output[fee_output_index];
787+
let new_fee_value = fee_output.value.to_sat().saturating_sub(bump_fee_amount_sat);
788+
fee_output.value = Amount::from_sat(new_fee_value);
789+
println!("New fee value: {} sats", new_fee_value);
790+
791+
// dust limit
792+
if new_fee_value < 546 {
793+
panic!("Warning: Fee output approaching dust limit ({} sats)", new_fee_value);
794+
}
795+
796+
for input in &mut tx.input {
797+
input.sequence = Sequence::ENABLE_RBF_NO_LOCKTIME;
798+
input.script_sig = ScriptBuf::new();
799+
input.witness = Witness::new();
800+
}
801+
802+
let signed_result = bitcoind.sign_raw_transaction_with_wallet(&tx).unwrap();
803+
assert!(signed_result.complete, "Failed to sign RBF transaction");
804+
805+
let tx_bytes = Vec::<u8>::from_hex(&signed_result.hex).unwrap();
806+
tx = bitcoin::consensus::encode::deserialize::<Transaction>(&tx_bytes).unwrap();
807+
};
808+
}
809+
810+
macro_rules! validate_balances {
811+
($expected_balance_sat: expr, $is_spendable: expr) => {
812+
let spend_balance = if $is_spendable { $expected_balance_sat } else { 0 };
813+
for node in &nodes {
814+
node.sync_wallets().unwrap();
815+
let balances = node.list_balances();
816+
assert_eq!(balances.spendable_onchain_balance_sats, spend_balance);
817+
assert_eq!(balances.total_onchain_balance_sats, $expected_balance_sat);
818+
}
819+
};
820+
}
821+
822+
// Modify the output to the nodes
823+
distribute_funds_all_nodes!();
824+
validate_balances!(amount_sat, false);
825+
prepare_rbf!();
826+
tx.output.iter_mut().for_each(|output| {
827+
if scripts_buf.contains(&output.script_pubkey) {
828+
let new_addr = bitcoind.new_address().unwrap();
829+
output.script_pubkey = new_addr.script_pubkey();
830+
}
831+
});
832+
bump_fee_rbf_and_public_transaction!();
833+
validate_balances!(0, is_insert_block);
834+
835+
// Not modifying the output scripts, but still bumping the fee.
836+
distribute_funds_all_nodes!();
837+
validate_balances!(amount_sat, false);
838+
prepare_rbf!();
839+
bump_fee_rbf_and_public_transaction!();
840+
validate_balances!(amount_sat, is_insert_block);
841+
842+
let mut final_amount_sat = amount_sat * 2;
843+
let value_sat = 21_000;
844+
845+
// Increase the value of the nodes' outputs
846+
distribute_funds_all_nodes!();
847+
prepare_rbf!();
848+
tx.output.iter_mut().for_each(|output| {
849+
if scripts_buf.contains(&output.script_pubkey) {
850+
output.value = Amount::from_sat(output.value.to_sat() + value_sat);
851+
}
852+
});
853+
bump_fee_rbf_and_public_transaction!();
854+
final_amount_sat += value_sat;
855+
validate_balances!(final_amount_sat, is_insert_block);
856+
857+
// Decreases the value of the nodes' outputs
858+
distribute_funds_all_nodes!();
859+
final_amount_sat += amount_sat;
860+
prepare_rbf!();
861+
tx.output.iter_mut().for_each(|output| {
862+
if scripts_buf.contains(&output.script_pubkey) {
863+
output.value = Amount::from_sat(output.value.to_sat() - value_sat);
864+
}
865+
});
866+
bump_fee_rbf_and_public_transaction!();
867+
final_amount_sat -= value_sat;
868+
validate_balances!(final_amount_sat, is_insert_block);
869+
870+
if !is_insert_block {
871+
generate_blocks_and_wait(bitcoind, electrs, 1);
872+
validate_balances!(final_amount_sat, true);
873+
}
874+
875+
// Check if it is possible to send all funds from the node
876+
let mut txids = Vec::new();
877+
let addr = bitcoind.new_address().unwrap();
878+
nodes.iter().for_each(|node| {
879+
let txid = node.onchain_payment().send_all_to_address(&addr, true, None).unwrap();
880+
txids.push(txid);
881+
});
882+
txids.iter().for_each(|txid| {
883+
wait_for_tx(electrs, *txid);
884+
});
885+
generate_blocks_and_wait(bitcoind, electrs, 6);
886+
validate_balances!(0, true);
887+
}
888+
672889
#[test]
673890
fn sign_verify_msg() {
674891
let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)