Skip to content

Commit 456b3ab

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 5fe06c8 commit 456b3ab

File tree

2 files changed

+247
-7
lines changed

2 files changed

+247
-7
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: 217 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +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_claimable_event, expect_payment_received_event, expect_payment_successful_event,
13-
generate_blocks_and_wait,
13+
generate_blocks_and_wait, get_transaction,
1414
logging::{init_log_logger, validate_log_entry, TestLogWriter},
1515
open_channel, premine_and_distribute_funds, random_config, random_listening_addresses,
1616
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx,
@@ -33,15 +33,18 @@ use lightning_invoice::{Bolt11InvoiceDescription, Description};
3333
use lightning_types::payment::{PaymentHash, PaymentPreimage};
3434

3535
use bitcoin::address::NetworkUnchecked;
36+
use bitcoin::hashes::hex::FromHex;
3637
use bitcoin::hashes::sha256::Hash as Sha256Hash;
3738
use bitcoin::hashes::Hash;
38-
use bitcoin::Address;
39-
use bitcoin::Amount;
39+
use bitcoin::{Address, Amount, ScriptBuf, Sequence, Transaction, Witness};
4040
use log::LevelFilter;
4141

42+
use std::collections::HashSet;
4243
use std::str::FromStr;
4344
use std::sync::Arc;
4445

46+
use crate::common::{distribute_funds_unconfirmed, premine_blocks};
47+
4548
#[test]
4649
fn channel_full_cycle() {
4750
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
@@ -670,6 +673,217 @@ fn onchain_wallet_recovery() {
670673
);
671674
}
672675

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+
757+
match bitcoind.send_raw_transaction(&tx) {
758+
Ok(res) => {
759+
// Mine a block immediately so the transaction is confirmed
760+
// before any node identifies it as a transaction that was in the mempool.
761+
if is_insert_block {
762+
generate_blocks_and_wait(bitcoind, electrs, 1);
763+
}
764+
let new_txid = res.0.parse().unwrap();
765+
wait_for_tx(electrs, new_txid);
766+
break;
767+
},
768+
Err(_) => {
769+
if _attempt == attempts - 1 {
770+
panic!("Failed to pump fee after {} attempts", attempts);
771+
}
772+
773+
bump_fee_amount_sat += bump_fee_amount_sat * 5;
774+
if tx.output[fee_output_index].value.to_sat() < bump_fee_amount_sat {
775+
panic!("Insufficient funds to increase fee");
776+
}
777+
},
778+
}
779+
}
780+
};
781+
}
782+
783+
macro_rules! bump_fee {
784+
() => {
785+
let fee_output = &mut tx.output[fee_output_index];
786+
let new_fee_value = fee_output.value.to_sat().saturating_sub(bump_fee_amount_sat);
787+
fee_output.value = Amount::from_sat(new_fee_value);
788+
789+
// dust limit
790+
if new_fee_value < 546 {
791+
panic!("Warning: Fee output approaching dust limit ({} sats)", new_fee_value);
792+
}
793+
794+
for input in &mut tx.input {
795+
input.sequence = Sequence::ENABLE_RBF_NO_LOCKTIME;
796+
input.script_sig = ScriptBuf::new();
797+
input.witness = Witness::new();
798+
}
799+
800+
let signed_result = bitcoind.sign_raw_transaction_with_wallet(&tx).unwrap();
801+
assert!(signed_result.complete, "Failed to sign RBF transaction");
802+
803+
let tx_bytes = Vec::<u8>::from_hex(&signed_result.hex).unwrap();
804+
tx = bitcoin::consensus::encode::deserialize::<Transaction>(&tx_bytes).unwrap();
805+
};
806+
}
807+
808+
macro_rules! validate_balances {
809+
($expected_balance_sat: expr, $is_spendable: expr) => {
810+
let spend_balance = if $is_spendable { $expected_balance_sat } else { 0 };
811+
for node in &nodes {
812+
node.sync_wallets().unwrap();
813+
let balances = node.list_balances();
814+
assert_eq!(balances.spendable_onchain_balance_sats, spend_balance);
815+
assert_eq!(balances.total_onchain_balance_sats, $expected_balance_sat);
816+
}
817+
};
818+
}
819+
820+
// Modify the output to the nodes
821+
distribute_funds_all_nodes!();
822+
validate_balances!(amount_sat, false);
823+
prepare_rbf!();
824+
tx.output.iter_mut().for_each(|output| {
825+
if scripts_buf.contains(&output.script_pubkey) {
826+
let new_addr = bitcoind.new_address().unwrap();
827+
output.script_pubkey = new_addr.script_pubkey();
828+
}
829+
});
830+
bump_fee_rbf_and_public_transaction!();
831+
validate_balances!(0, is_insert_block);
832+
833+
// Not modifying the output scripts, but still bumping the fee.
834+
distribute_funds_all_nodes!();
835+
validate_balances!(amount_sat, false);
836+
prepare_rbf!();
837+
bump_fee_rbf_and_public_transaction!();
838+
validate_balances!(amount_sat, is_insert_block);
839+
840+
let mut final_amount_sat = amount_sat * 2;
841+
let value_sat = 21_000;
842+
843+
// Increase the value of the nodes' outputs
844+
distribute_funds_all_nodes!();
845+
prepare_rbf!();
846+
tx.output.iter_mut().for_each(|output| {
847+
if scripts_buf.contains(&output.script_pubkey) {
848+
output.value = Amount::from_sat(output.value.to_sat() + value_sat);
849+
}
850+
});
851+
bump_fee_rbf_and_public_transaction!();
852+
final_amount_sat += value_sat;
853+
validate_balances!(final_amount_sat, is_insert_block);
854+
855+
// Decreases the value of the nodes' outputs
856+
distribute_funds_all_nodes!();
857+
final_amount_sat += amount_sat;
858+
prepare_rbf!();
859+
tx.output.iter_mut().for_each(|output| {
860+
if scripts_buf.contains(&output.script_pubkey) {
861+
output.value = Amount::from_sat(output.value.to_sat() - value_sat);
862+
}
863+
});
864+
bump_fee_rbf_and_public_transaction!();
865+
final_amount_sat -= value_sat;
866+
validate_balances!(final_amount_sat, is_insert_block);
867+
868+
if !is_insert_block {
869+
generate_blocks_and_wait(bitcoind, electrs, 1);
870+
validate_balances!(final_amount_sat, true);
871+
}
872+
873+
// Check if it is possible to send all funds from the node
874+
let mut txids = Vec::new();
875+
let addr = bitcoind.new_address().unwrap();
876+
nodes.iter().for_each(|node| {
877+
let txid = node.onchain_payment().send_all_to_address(&addr, true, None).unwrap();
878+
txids.push(txid);
879+
});
880+
txids.iter().for_each(|txid| {
881+
wait_for_tx(electrs, *txid);
882+
});
883+
generate_blocks_and_wait(bitcoind, electrs, 6);
884+
validate_balances!(0, true);
885+
}
886+
673887
#[test]
674888
fn sign_verify_msg() {
675889
let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)