Skip to content

Commit f3aab90

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 f3aab90

File tree

2 files changed

+226
-12
lines changed

2 files changed

+226
-12
lines changed

tests/common/mod.rs

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ use lightning_types::payment::{PaymentHash, PaymentPreimage};
3030
use lightning_persister::fs_store::FilesystemStore;
3131

3232
use bitcoin::hashes::sha256::Hash as Sha256;
33-
use bitcoin::hashes::Hash;
34-
use bitcoin::{Address, Amount, Network, OutPoint, Txid};
33+
use bitcoin::hashes::{hex::FromHex, Hash};
34+
use bitcoin::{
35+
Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness,
36+
};
3537

3638
use electrsd::corepc_node::Client as BitcoindClient;
3739
use electrsd::corepc_node::Node as BitcoinD;
@@ -42,7 +44,7 @@ use rand::distributions::Alphanumeric;
4244
use rand::{thread_rng, Rng};
4345
use serde_json::{json, Value};
4446

45-
use std::collections::HashMap;
47+
use std::collections::{HashMap, HashSet};
4648
use std::env;
4749
use std::path::PathBuf;
4850
use std::sync::{Arc, RwLock};
@@ -487,12 +489,25 @@ where
487489
pub(crate) fn premine_and_distribute_funds<E: ElectrumApi>(
488490
bitcoind: &BitcoindClient, electrs: &E, addrs: Vec<Address>, amount: Amount,
489491
) {
492+
premine_blocks(bitcoind, electrs);
493+
494+
distribute_funds_unconfirmed(bitcoind, electrs, addrs, amount);
495+
generate_blocks_and_wait(bitcoind, electrs, 1);
496+
}
497+
498+
pub(crate) fn premine_blocks<E: ElectrumApi>(bitcoind: &BitcoindClient, electrs: &E) {
490499
let _ = bitcoind.create_wallet("ldk_node_test");
491500
let _ = bitcoind.load_wallet("ldk_node_test");
492501
generate_blocks_and_wait(bitcoind, electrs, 101);
502+
}
493503

494-
let amounts: HashMap<String, f64> =
495-
addrs.iter().map(|addr| (addr.to_string(), amount.to_btc())).collect();
504+
pub(crate) fn distribute_funds_unconfirmed<E: ElectrumApi>(
505+
bitcoind: &BitcoindClient, electrs: &E, addrs: Vec<Address>, amount: Amount,
506+
) -> Txid {
507+
let mut amounts = HashMap::<String, f64>::new();
508+
for addr in &addrs {
509+
amounts.insert(addr.to_string(), amount.to_btc());
510+
}
496511

497512
let empty_account = json!("");
498513
let amounts_json = json!(amounts);
@@ -505,7 +520,70 @@ pub(crate) fn premine_and_distribute_funds<E: ElectrumApi>(
505520
.unwrap();
506521

507522
wait_for_tx(electrs, txid);
508-
generate_blocks_and_wait(bitcoind, electrs, 1);
523+
524+
txid
525+
}
526+
527+
pub(crate) fn prepare_rbf<E: ElectrumApi>(
528+
electrs: &E, txid: Txid, scripts_buf: &HashSet<ScriptBuf>,
529+
) -> (Transaction, usize) {
530+
let tx = electrs.transaction_get(&txid).unwrap();
531+
532+
let fee_output_index = tx
533+
.output
534+
.iter()
535+
.position(|output| !scripts_buf.contains(&output.script_pubkey))
536+
.expect("No output available for fee bumping");
537+
538+
(tx, fee_output_index)
539+
}
540+
541+
pub(crate) fn bump_fee_and_broadcast<E: ElectrumApi>(
542+
bitcoind: &BitcoindClient, electrs: &E, mut tx: Transaction, fee_output_index: usize,
543+
is_insert_block: bool,
544+
) -> Transaction {
545+
let mut bump_fee_amount_sat = tx.vsize() as u64;
546+
let attempts = 5;
547+
548+
for _ in 0..attempts {
549+
let fee_output = &mut tx.output[fee_output_index];
550+
let new_fee_value = fee_output.value.to_sat().saturating_sub(bump_fee_amount_sat);
551+
if new_fee_value < 546 {
552+
panic!("Warning: Fee output approaching dust limit ({} sats)", new_fee_value);
553+
}
554+
fee_output.value = Amount::from_sat(new_fee_value);
555+
556+
for input in &mut tx.input {
557+
input.sequence = Sequence::ENABLE_RBF_NO_LOCKTIME;
558+
input.script_sig = ScriptBuf::new();
559+
input.witness = Witness::new();
560+
}
561+
562+
let signed_result = bitcoind.sign_raw_transaction_with_wallet(&tx).unwrap();
563+
assert!(signed_result.complete, "Failed to sign RBF transaction");
564+
565+
let tx_bytes = Vec::<u8>::from_hex(&signed_result.hex).unwrap();
566+
tx = bitcoin::consensus::encode::deserialize::<Transaction>(&tx_bytes).unwrap();
567+
568+
match bitcoind.send_raw_transaction(&tx) {
569+
Ok(res) => {
570+
if is_insert_block {
571+
generate_blocks_and_wait(bitcoind, electrs, 1);
572+
}
573+
let new_txid: Txid = res.0.parse().unwrap();
574+
wait_for_tx(electrs, new_txid);
575+
return tx;
576+
},
577+
Err(_) => {
578+
bump_fee_amount_sat += bump_fee_amount_sat * 5;
579+
if tx.output[fee_output_index].value.to_sat() < bump_fee_amount_sat {
580+
panic!("Insufficient funds to increase fee");
581+
}
582+
},
583+
}
584+
}
585+
586+
panic!("Failed to bump fee after {} attempts", attempts);
509587
}
510588

511589
pub fn open_channel(

tests/integration_tests_rust.rs

Lines changed: 142 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
mod common;
99

1010
use common::{
11-
do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event, expect_event,
11+
bump_fee_and_broadcast, distribute_funds_unconfirmed, do_channel_full_cycle,
12+
expect_channel_pending_event, expect_channel_ready_event, expect_event,
1213
expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event,
1314
generate_blocks_and_wait,
1415
logging::{init_log_logger, validate_log_entry, TestLogWriter},
15-
open_channel, premine_and_distribute_funds, random_config, random_listening_addresses,
16-
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx,
17-
TestChainSource, TestSyncStore,
16+
open_channel, premine_and_distribute_funds, premine_blocks, prepare_rbf, random_config,
17+
random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node,
18+
setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore,
1819
};
1920

2021
use ldk_node::config::EsploraSyncConfig;
@@ -35,10 +36,10 @@ use lightning_types::payment::{PaymentHash, PaymentPreimage};
3536
use bitcoin::address::NetworkUnchecked;
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};
4040
use log::LevelFilter;
4141

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

@@ -670,6 +671,141 @@ fn onchain_wallet_recovery() {
670671
);
671672
}
672673

674+
#[test]
675+
fn test_rbf_via_mempool() {
676+
run_rbf_test(false);
677+
}
678+
679+
#[test]
680+
fn test_rbf_via_direct_block_insertion() {
681+
run_rbf_test(true);
682+
}
683+
684+
// `is_insert_block`:
685+
// - `true`: transaction is mined immediately (no mempool), testing confirmed-Tx handling.
686+
// - `false`: transaction stays in mempool until confirmation, testing unconfirmed-Tx handling.
687+
fn run_rbf_test(is_insert_block: bool) {
688+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
689+
let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(&bitcoind);
690+
let chain_source_electrsd = TestChainSource::Electrum(&electrsd);
691+
let chain_source_esplora = TestChainSource::Esplora(&electrsd);
692+
693+
macro_rules! config_node {
694+
($chain_source: expr, $anchor_channels: expr) => {{
695+
let config_a = random_config($anchor_channels);
696+
let node = setup_node(&$chain_source, config_a, None);
697+
node
698+
}};
699+
}
700+
let anchor_channels = false;
701+
let nodes = vec![
702+
config_node!(chain_source_electrsd, anchor_channels),
703+
config_node!(chain_source_bitcoind, anchor_channels),
704+
config_node!(chain_source_esplora, anchor_channels),
705+
];
706+
707+
let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client);
708+
premine_blocks(bitcoind, electrs);
709+
710+
// Helpers declaration before starting the test
711+
let all_addrs =
712+
nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect::<Vec<_>>();
713+
let amount_sat = 2_100_000;
714+
let mut txid;
715+
macro_rules! distribute_funds_all_nodes {
716+
() => {
717+
txid = distribute_funds_unconfirmed(
718+
bitcoind,
719+
electrs,
720+
all_addrs.clone(),
721+
Amount::from_sat(amount_sat),
722+
);
723+
};
724+
}
725+
macro_rules! validate_balances {
726+
($expected_balance_sat: expr, $is_spendable: expr) => {
727+
let spend_balance = if $is_spendable { $expected_balance_sat } else { 0 };
728+
for node in &nodes {
729+
node.sync_wallets().unwrap();
730+
let balances = node.list_balances();
731+
assert_eq!(balances.spendable_onchain_balance_sats, spend_balance);
732+
assert_eq!(balances.total_onchain_balance_sats, $expected_balance_sat);
733+
}
734+
};
735+
}
736+
737+
let scripts_buf: HashSet<ScriptBuf> =
738+
all_addrs.iter().map(|addr| addr.script_pubkey()).collect();
739+
let mut tx;
740+
let mut fee_output_index;
741+
742+
// Modify the output to the nodes
743+
distribute_funds_all_nodes!();
744+
validate_balances!(amount_sat, false);
745+
(tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf);
746+
tx.output.iter_mut().for_each(|output| {
747+
if scripts_buf.contains(&output.script_pubkey) {
748+
let new_addr = bitcoind.new_address().unwrap();
749+
output.script_pubkey = new_addr.script_pubkey();
750+
}
751+
});
752+
bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block);
753+
validate_balances!(0, is_insert_block);
754+
755+
// Not modifying the output scripts, but still bumping the fee.
756+
distribute_funds_all_nodes!();
757+
validate_balances!(amount_sat, false);
758+
(tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf);
759+
bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block);
760+
validate_balances!(amount_sat, is_insert_block);
761+
762+
let mut final_amount_sat = amount_sat * 2;
763+
let value_sat = 21_000;
764+
765+
// Increase the value of the nodes' outputs
766+
distribute_funds_all_nodes!();
767+
(tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf);
768+
tx.output.iter_mut().for_each(|output| {
769+
if scripts_buf.contains(&output.script_pubkey) {
770+
output.value = Amount::from_sat(output.value.to_sat() + value_sat);
771+
}
772+
});
773+
bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block);
774+
final_amount_sat += value_sat;
775+
validate_balances!(final_amount_sat, is_insert_block);
776+
777+
// Decreases the value of the nodes' outputs
778+
distribute_funds_all_nodes!();
779+
final_amount_sat += amount_sat;
780+
(tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf);
781+
tx.output.iter_mut().for_each(|output| {
782+
if scripts_buf.contains(&output.script_pubkey) {
783+
output.value = Amount::from_sat(output.value.to_sat() - value_sat);
784+
}
785+
});
786+
bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block);
787+
final_amount_sat -= value_sat;
788+
validate_balances!(final_amount_sat, is_insert_block);
789+
790+
if !is_insert_block {
791+
generate_blocks_and_wait(bitcoind, electrs, 1);
792+
validate_balances!(final_amount_sat, true);
793+
}
794+
795+
// Check if it is possible to send all funds from the node
796+
let mut txids = Vec::new();
797+
let addr = bitcoind.new_address().unwrap();
798+
nodes.iter().for_each(|node| {
799+
let txid = node.onchain_payment().send_all_to_address(&addr, true, None).unwrap();
800+
txids.push(txid);
801+
});
802+
txids.iter().for_each(|txid| {
803+
wait_for_tx(electrs, *txid);
804+
});
805+
generate_blocks_and_wait(bitcoind, electrs, 6);
806+
validate_balances!(0, true);
807+
}
808+
673809
#[test]
674810
fn sign_verify_msg() {
675811
let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)