Skip to content

Commit de9c632

Browse files
Add RBF test for output value change and fee bump
1 parent c878911 commit de9c632

File tree

2 files changed

+279
-4
lines changed

2 files changed

+279
-4
lines changed

tests/common/mod.rs

Lines changed: 189 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,16 +470,47 @@ where
470470
pub(crate) fn premine_and_distribute_funds<E: ElectrumApi>(
471471
bitcoind: &BitcoindClient, electrs: &E, addrs: Vec<Address>, amount: Amount,
472472
) {
473+
premine_blocks(bitcoind, electrs);
474+
475+
distribute_funds(bitcoind, electrs, addrs, amount);
476+
}
477+
478+
pub(crate) fn premine_blocks<E: ElectrumApi>(bitcoind: &BitcoindClient, electrs: &E) {
473479
let _ = bitcoind.create_wallet("ldk_node_test");
474480
let _ = bitcoind.load_wallet("ldk_node_test");
475481
generate_blocks_and_wait(bitcoind, electrs, 101);
482+
}
476483

477-
for addr in addrs {
478-
let txid = bitcoind.send_to_address(&addr, amount).unwrap().0.parse().unwrap();
479-
wait_for_tx(electrs, txid);
484+
pub(crate) fn distribute_funds_unconfirmed<E: ElectrumApi>(
485+
bitcoind: &BitcoindClient, electrs: &E, addrs: Vec<Address>, amount: Amount,
486+
) -> Txid {
487+
let mut amounts = HashMap::<String, f64>::new();
488+
for addr in &addrs {
489+
amounts.insert(addr.to_string(), amount.to_btc());
480490
}
481491

492+
let empty_account = json!("");
493+
let amounts_json = json!(amounts);
494+
let txid = bitcoind
495+
.call::<Value>("sendmany", &[empty_account, amounts_json])
496+
.unwrap()
497+
.as_str()
498+
.unwrap()
499+
.parse()
500+
.unwrap();
501+
502+
wait_for_tx(electrs, txid);
503+
504+
txid
505+
}
506+
507+
pub(crate) fn distribute_funds<E: ElectrumApi>(
508+
bitcoind: &BitcoindClient, electrs: &E, addrs: Vec<Address>, amount: Amount,
509+
) -> Txid {
510+
let address_txid_map = distribute_funds_unconfirmed(bitcoind, electrs, addrs, amount);
482511
generate_blocks_and_wait(bitcoind, electrs, 1);
512+
513+
address_txid_map
483514
}
484515

485516
pub fn open_channel(
@@ -1074,6 +1105,161 @@ pub(crate) fn do_channel_full_cycle<E: ElectrumApi>(
10741105
println!("\nB stopped");
10751106
}
10761107

1108+
pub(crate) struct SetupRBF {
1109+
pub nodes_a: Vec<TestNode>,
1110+
pub nodes_b: Vec<TestNode>,
1111+
pub addrs_a: Vec<Address>,
1112+
pub addrs_b: Vec<Address>,
1113+
}
1114+
1115+
impl SetupRBF {
1116+
pub(crate) fn new(bitcoind: &BitcoinD, electrsd: &ElectrsD) -> Self {
1117+
let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(bitcoind);
1118+
let chain_source_electrum = TestChainSource::Electrum(electrsd);
1119+
let chain_source_esplora = TestChainSource::Esplora(electrsd);
1120+
1121+
let (node_bitcoind_a, node_bitcoind_b) =
1122+
setup_two_nodes(&chain_source_bitcoind, false, false, false);
1123+
let (node_electrsd_a, node_electrsd_b) =
1124+
setup_two_nodes(&chain_source_electrum, false, false, false);
1125+
let (node_esplora_a, node_esplora_b) =
1126+
setup_two_nodes(&chain_source_esplora, false, false, false);
1127+
1128+
let nodes_a = vec![node_bitcoind_a, node_electrsd_a, node_esplora_a];
1129+
let nodes_b = vec![node_bitcoind_b, node_electrsd_b, node_esplora_b];
1130+
1131+
let addrs_a = nodes_a
1132+
.iter()
1133+
.map(|node| node.onchain_payment().new_address().unwrap())
1134+
.collect::<Vec<_>>();
1135+
let addrs_b = nodes_b
1136+
.iter()
1137+
.map(|node| node.onchain_payment().new_address().unwrap())
1138+
.collect::<Vec<_>>();
1139+
1140+
Self { nodes_a, nodes_b, addrs_a, addrs_b }
1141+
}
1142+
1143+
pub(crate) fn sync_wallets(&self) {
1144+
for node in &self.nodes_a {
1145+
node.sync_wallets().unwrap();
1146+
}
1147+
for node in &self.nodes_b {
1148+
node.sync_wallets().unwrap();
1149+
}
1150+
}
1151+
1152+
pub(crate) fn validate_balances(
1153+
&self, nodes: &[TestNode], expected_balance: u64, is_spendable: bool,
1154+
) {
1155+
let spend_balance = if is_spendable { expected_balance } else { 0 };
1156+
for node in nodes.iter() {
1157+
assert_eq!(node.list_balances().total_onchain_balance_sats, expected_balance);
1158+
assert_eq!(node.list_balances().spendable_onchain_balance_sats, spend_balance);
1159+
}
1160+
}
1161+
1162+
pub(crate) fn setup_initial_funding<E: ElectrumApi>(
1163+
&self, bitcoind: &BitcoindClient, electrs: &E, amount: u64,
1164+
) -> bitcoin::Txid {
1165+
premine_blocks(bitcoind, electrs);
1166+
let all_addrs = self.addrs_a.iter().chain(self.addrs_b.iter()).cloned().collect::<Vec<_>>();
1167+
distribute_funds_unconfirmed(bitcoind, electrs, all_addrs, Amount::from_sat(amount))
1168+
}
1169+
1170+
pub(crate) fn pump_fee_with_rbf_replacement<E: ElectrumApi>(
1171+
&self, bitcoind: &BitcoindClient, electrs: &E, original_tx: &mut Transaction,
1172+
fee_output_index: usize,
1173+
) -> Txid {
1174+
let mut pump_fee_amount = 1 * original_tx.vsize() as u64;
1175+
1176+
macro_rules! pump_fee {
1177+
() => {{
1178+
let fee_output = &mut original_tx.output[fee_output_index];
1179+
let new_fee_value = fee_output.value.to_sat().saturating_sub(pump_fee_amount);
1180+
fee_output.value = Amount::from_sat(new_fee_value);
1181+
1182+
if new_fee_value < 546 {
1183+
// dust limit
1184+
panic!("Warning: Fee output approaching dust limit ({} sats)", new_fee_value);
1185+
}
1186+
1187+
pump_fee_amount += pump_fee_amount * 5;
1188+
1189+
for input in &mut original_tx.input {
1190+
input.sequence = Sequence::ENABLE_RBF_NO_LOCKTIME;
1191+
input.script_sig = ScriptBuf::new();
1192+
input.witness = Witness::new();
1193+
}
1194+
1195+
let tx_hex = bitcoin::consensus::encode::serialize_hex(&original_tx);
1196+
let signed_result = bitcoind.sign_raw_transaction_with_wallet(&tx_hex).unwrap();
1197+
assert!(signed_result.complete, "Failed to sign RBF transaction");
1198+
1199+
let tx_bytes = Vec::<u8>::from_hex(&signed_result.hex).unwrap();
1200+
let tx = bitcoin::consensus::encode::deserialize::<Transaction>(&tx_bytes).unwrap();
1201+
1202+
tx
1203+
}};
1204+
}
1205+
1206+
for _attempt in 0..3 {
1207+
let tx = pump_fee!();
1208+
match bitcoind.send_raw_transaction(&tx) {
1209+
Ok(res) => {
1210+
let new_txid = res.0.parse().unwrap();
1211+
wait_for_tx(electrs, new_txid);
1212+
println!("New txid from the RBF: {}", new_txid);
1213+
return new_txid;
1214+
},
1215+
Err(_) => {
1216+
if original_tx.output[fee_output_index].value.to_sat() < pump_fee_amount {
1217+
panic!("Insufficient funds to increase fee");
1218+
}
1219+
},
1220+
}
1221+
}
1222+
1223+
panic!("Failed to pump fee after 3 attempts");
1224+
}
1225+
1226+
pub(crate) fn init_rbf<E: ElectrumApi>(
1227+
&self, electrs: &E, original_txid: Txid,
1228+
) -> (Transaction, HashSet<ScriptBuf>, HashSet<ScriptBuf>, usize) {
1229+
let original_tx: Transaction = electrs.transaction_get(&original_txid).unwrap();
1230+
1231+
let total_addresses_to_modify = &self.addrs_a.len() + &self.addrs_b.len();
1232+
if original_tx.output.len() <= total_addresses_to_modify {
1233+
panic!(
1234+
"Transaction must have more outputs ({}) than addresses to modify ({}) to allow fee pumping",
1235+
original_tx.output.len(),
1236+
total_addresses_to_modify
1237+
);
1238+
}
1239+
1240+
let scripts_a: HashSet<ScriptBuf> =
1241+
self.addrs_a.iter().map(|addr| addr.script_pubkey()).collect();
1242+
let scripts_b: HashSet<ScriptBuf> =
1243+
self.addrs_b.iter().map(|addr| addr.script_pubkey()).collect();
1244+
1245+
let mut fee_output_index: Option<usize> = None;
1246+
for (index, output) in original_tx.output.iter().enumerate() {
1247+
if !scripts_a.contains(&output.script_pubkey)
1248+
&& !scripts_b.contains(&output.script_pubkey)
1249+
{
1250+
fee_output_index = Some(index);
1251+
break;
1252+
}
1253+
}
1254+
1255+
let fee_output_index = fee_output_index.expect(
1256+
"No output available for fee pumping. Need at least one output not being modified.",
1257+
);
1258+
1259+
(original_tx, scripts_a, scripts_b, fee_output_index)
1260+
}
1261+
}
1262+
10771263
// A `KVStore` impl for testing purposes that wraps all our `KVStore`s and asserts their synchronicity.
10781264
pub(crate) struct TestSyncStore {
10791265
serializer: RwLock<()>,

tests/integration_tests_rust.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use common::{
1212
expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait,
1313
logging::{init_log_logger, validate_log_entry, TestLogWriter},
1414
open_channel, premine_and_distribute_funds, random_config, random_listening_addresses,
15-
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx,
15+
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx, SetupRBF,
1616
TestChainSource, TestSyncStore,
1717
};
1818

@@ -1389,3 +1389,92 @@ fn facade_logging() {
13891389
validate_log_entry(entry);
13901390
}
13911391
}
1392+
1393+
#[test]
1394+
fn test_rbf_value() {
1395+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
1396+
let setup = SetupRBF::new(&bitcoind, &electrsd);
1397+
let amount = 2_125_000;
1398+
let value = 21_000;
1399+
1400+
let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client);
1401+
let txid = setup.setup_initial_funding(bitcoind, electrs, amount);
1402+
1403+
setup.sync_wallets();
1404+
setup.validate_balances(&setup.nodes_a, amount, false);
1405+
setup.validate_balances(&setup.nodes_b, amount, false);
1406+
1407+
let (mut original_tx, add_scripts, subtract_scripts, fee_output_index) =
1408+
setup.init_rbf(electrs, txid);
1409+
original_tx.output.iter_mut().for_each(|output| {
1410+
if add_scripts.contains(&output.script_pubkey) {
1411+
output.value = Amount::from_sat(output.value.to_sat() + value);
1412+
} else if subtract_scripts.contains(&output.script_pubkey) {
1413+
output.value = Amount::from_sat(output.value.to_sat().saturating_sub(value));
1414+
}
1415+
});
1416+
1417+
setup.pump_fee_with_rbf_replacement(bitcoind, electrs, &mut original_tx, fee_output_index);
1418+
1419+
setup.sync_wallets();
1420+
setup.validate_balances(&setup.nodes_a, amount + value, false);
1421+
setup.validate_balances(&setup.nodes_b, amount - value, false);
1422+
1423+
generate_blocks_and_wait(bitcoind, electrs, 1);
1424+
1425+
setup.sync_wallets();
1426+
setup.validate_balances(&setup.nodes_a, amount + value, true);
1427+
setup.validate_balances(&setup.nodes_b, amount - value, true);
1428+
1429+
let funding = 1_500_00;
1430+
let should_announce = true;
1431+
for index_node in 0..setup.nodes_a.len() {
1432+
let node_add = &setup.nodes_a[index_node];
1433+
let node_subtract = &setup.nodes_b[index_node];
1434+
open_channel(node_add, node_subtract, funding, should_announce, &electrsd);
1435+
open_channel(node_subtract, node_add, funding, should_announce, &electrsd);
1436+
}
1437+
}
1438+
1439+
#[test]
1440+
fn test_rbf_output() {
1441+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
1442+
let setup = SetupRBF::new(&bitcoind, &electrsd);
1443+
let amount = 2_125_000;
1444+
1445+
let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client);
1446+
let txid = setup.setup_initial_funding(bitcoind, electrs, amount);
1447+
println!("\n Transacao que deve ir embora: {}\n", txid);
1448+
1449+
setup.sync_wallets();
1450+
setup.validate_balances(&setup.nodes_a, amount, false);
1451+
setup.validate_balances(&setup.nodes_b, amount, false);
1452+
1453+
let (mut original_tx, _, modify_scripts, fee_output_index) = setup.init_rbf(electrs, txid);
1454+
original_tx.output.iter_mut().for_each(|output| {
1455+
if modify_scripts.contains(&output.script_pubkey) {
1456+
let new_addr = bitcoind.new_address().unwrap();
1457+
output.script_pubkey = new_addr.script_pubkey();
1458+
}
1459+
});
1460+
1461+
setup.pump_fee_with_rbf_replacement(bitcoind, electrs, &mut original_tx, fee_output_index);
1462+
1463+
setup.sync_wallets();
1464+
setup.validate_balances(&setup.nodes_a, amount, false);
1465+
setup.validate_balances(&setup.nodes_b, 0, false);
1466+
1467+
generate_blocks_and_wait(bitcoind, electrs, 1);
1468+
1469+
setup.sync_wallets();
1470+
setup.validate_balances(&setup.nodes_a, amount, true);
1471+
setup.validate_balances(&setup.nodes_b, 0, true);
1472+
1473+
let funding = 1_500_00;
1474+
let should_announce = true;
1475+
for index_node in 0..setup.nodes_a.len() {
1476+
let node_static = &setup.nodes_a[index_node];
1477+
let node_modify = &setup.nodes_b[index_node];
1478+
open_channel(node_static, node_modify, funding, should_announce, &electrsd);
1479+
}
1480+
}

0 commit comments

Comments
 (0)