Skip to content

Commit 71e5ef7

Browse files
Add manual block generation and direct transaction insertion for RBF testing
- Introduce `generate_block_and_insert_transactions` to manually mine a block with arbitrary transactions. - Update `bump_fee_and_broadcast` to optionally insert transactions directly into a block (bypassing the mempool) when `is_insert_block` is true. - Add integration test to insert the original (pre-RBF) transaction into a block instead of the RBF, to cover scenarios where the original transaction is confirmed and the RBF is not.
1 parent cc9f907 commit 71e5ef7

File tree

2 files changed

+198
-32
lines changed

2 files changed

+198
-32
lines changed

tests/common/mod.rs

Lines changed: 129 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,26 @@ use std::boxed::Box;
1414
use std::collections::{HashMap, HashSet};
1515
use std::env;
1616
use std::future::Future;
17+
use std::iter;
1718
use std::path::PathBuf;
1819
use std::pin::Pin;
20+
use std::str::FromStr;
1921
use std::sync::{Arc, RwLock};
20-
use std::time::Duration;
22+
use std::time::{Duration, UNIX_EPOCH};
2123

22-
use bitcoin::hashes::hex::FromHex;
23-
use bitcoin::hashes::sha256::Hash as Sha256;
24-
use bitcoin::hashes::Hash;
24+
use bitcoin::absolute::LockTime;
25+
use bitcoin::block::{Header, Version as BlockVersion};
26+
use bitcoin::hashes::{hex::FromHex, sha256::Hash as Sha256, sha256d::Hash as Sha256d, Hash};
27+
use bitcoin::merkle_tree::calculate_root;
28+
use bitcoin::script::Builder as BuilderScriptBitcoin;
2529
use bitcoin::{
26-
Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness,
30+
opcodes::all::OP_RETURN, transaction::Version, Address, Amount, Block, BlockHash,
31+
CompactTarget, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxMerkleNode, Txid,
32+
Witness, Wtxid,
33+
};
34+
use electrsd::corepc_node::{
35+
Client as BitcoindClient, Node as BitcoinD, TemplateRequest, TemplateRules,
2736
};
28-
use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD};
2937
use electrsd::{corepc_node, ElectrsD};
3038
use electrum_client::ElectrumApi;
3139
use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig};
@@ -394,6 +402,116 @@ pub(crate) fn setup_node_for_async_payments(
394402
node
395403
}
396404

405+
pub(crate) fn generate_block_and_insert_transactions<E: ElectrumApi>(
406+
bitcoind: &BitcoindClient, electrs: &E, txs: &[Transaction],
407+
) {
408+
let _ = bitcoind.create_wallet("ldk_node_test");
409+
let _ = bitcoind.load_wallet("ldk_node_test");
410+
let blockchain_info = bitcoind.get_blockchain_info().expect("failed to get blockchain info");
411+
let cur_height = blockchain_info.blocks;
412+
let address = bitcoind.new_address().expect("failed to get new address");
413+
414+
let request_block_template = TemplateRequest { rules: vec![TemplateRules::Segwit] };
415+
let bt =
416+
bitcoind.get_block_template(&request_block_template).expect("failed to get block template");
417+
418+
// === BIP 141: Witness Commitment Calculation ===
419+
let witness_root = calculate_root(
420+
iter::once(Wtxid::all_zeros()).chain(txs.iter().map(|tx| tx.compute_wtxid())),
421+
)
422+
.map(|root| TxMerkleNode::from_byte_array(root.to_byte_array()))
423+
.unwrap();
424+
425+
// BIP 141: Witness reserved value (32 zero bytes)
426+
let witness_reserved_value = [0u8; 32];
427+
428+
// === Coinbase Transaction Construction ===
429+
// BIP 141: Coinbase witness contains the witness reserved value
430+
let coinbase_witness = Witness::from(vec![witness_reserved_value.to_vec()]);
431+
432+
// BIP 141: Calculate commitment hash = Double-SHA256(witness root || witness reserved value)
433+
let commitment_hash =
434+
Sha256d::hash(&[witness_root.to_byte_array(), witness_reserved_value].concat());
435+
436+
// Format: OP_RETURN + OP_PUSHBYTES_36 + 0xaa21a9ed + 32-byte commitment hash
437+
let witness_commitment_script = BuilderScriptBitcoin::new()
438+
.push_opcode(OP_RETURN)
439+
.push_slice(&{
440+
let mut data = [0u8; 36];
441+
data[..4].copy_from_slice(&[0xaa, 0x21, 0xa9, 0xed]);
442+
data[4..].copy_from_slice(&commitment_hash.to_byte_array());
443+
data
444+
})
445+
.into_script();
446+
447+
// BIP 34: Block height in coinbase input script
448+
let block_height = bt.height;
449+
let height_script = BuilderScriptBitcoin::new()
450+
.push_int(block_height as i64) // BIP 34: block height as first item
451+
.push_int(rand::random()) // Random nonce for uniqueness
452+
.into_script();
453+
454+
// Do not use the coinbase value from the block template.
455+
// The template may include transactions not actually mined, so fees may be incorrect.
456+
let coinbase_output_value = 1_250_000_000;
457+
458+
let coinbase_tx = Transaction {
459+
version: Version::ONE,
460+
lock_time: LockTime::from_height(0).unwrap(),
461+
input: vec![bitcoin::TxIn {
462+
previous_output: OutPoint::default(), // Null outpoint for coinbase
463+
script_sig: height_script, // BIP 34: height + random data
464+
sequence: Sequence::default(),
465+
witness: coinbase_witness, // BIP 141: witness reserved value
466+
}],
467+
output: vec![
468+
// Coinbase reward output
469+
bitcoin::TxOut {
470+
value: Amount::from_sat(coinbase_output_value),
471+
script_pubkey: address.script_pubkey(),
472+
},
473+
// BIP 141: Witness commitment output (must be last output)
474+
bitcoin::TxOut { value: Amount::ZERO, script_pubkey: witness_commitment_script },
475+
],
476+
};
477+
478+
// === Block Construction ===
479+
let bits: [u8; 4] = Vec::from_hex(&bt.bits).unwrap().try_into().expect("bits must be 4 bytes");
480+
let prev_hash_block = BlockHash::from_str(&bt.previous_block_hash).expect("invalid prev hash");
481+
482+
let txdata = [coinbase_tx].into_iter().chain(txs.iter().cloned()).collect::<Vec<_>>();
483+
let mut block = Block {
484+
header: Header {
485+
version: BlockVersion::default(),
486+
prev_blockhash: prev_hash_block,
487+
merkle_root: TxMerkleNode::all_zeros(), // Will be calculated below
488+
time: Ord::max(bt.min_time, UNIX_EPOCH.elapsed().unwrap().as_secs() as u32),
489+
bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
490+
nonce: 0,
491+
},
492+
txdata,
493+
};
494+
495+
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
496+
497+
for nonce in 0..=u32::MAX {
498+
block.header.nonce = nonce;
499+
if block.header.target().is_met_by(block.block_hash()) {
500+
break;
501+
}
502+
}
503+
504+
match bitcoind.submit_block(&block) {
505+
Ok(_) => {},
506+
Err(e) => panic!("Failed to submit block: {:?}", e),
507+
}
508+
wait_for_block(electrs, cur_height as usize + 1);
509+
510+
txs.iter().for_each(|tx| {
511+
wait_for_tx(electrs, tx.compute_txid());
512+
});
513+
}
514+
397515
pub(crate) fn generate_blocks_and_wait<E: ElectrumApi>(
398516
bitcoind: &BitcoindClient, electrs: &E, num: usize,
399517
) {
@@ -575,11 +693,13 @@ pub(crate) fn bump_fee_and_broadcast<E: ElectrumApi>(
575693
let tx_bytes = Vec::<u8>::from_hex(&signed_result.hex).unwrap();
576694
tx = bitcoin::consensus::encode::deserialize::<Transaction>(&tx_bytes).unwrap();
577695

696+
if is_insert_block {
697+
generate_block_and_insert_transactions(bitcoind, electrs, &[tx.clone()]);
698+
return tx;
699+
}
700+
578701
match bitcoind.send_raw_transaction(&tx) {
579702
Ok(res) => {
580-
if is_insert_block {
581-
generate_blocks_and_wait(bitcoind, electrs, 1);
582-
}
583703
let new_txid: Txid = res.0.parse().unwrap();
584704
wait_for_tx(electrs, new_txid);
585705
return tx;

tests/integration_tests_rust.rs

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ use common::{
2020
bump_fee_and_broadcast, distribute_funds_unconfirmed, do_channel_full_cycle,
2121
expect_channel_pending_event, expect_channel_ready_event, expect_event,
2222
expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event,
23-
generate_blocks_and_wait, open_channel, open_channel_push_amt, premine_and_distribute_funds,
24-
premine_blocks, prepare_rbf, random_config, random_listening_addresses,
25-
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_node_for_async_payments,
26-
setup_node_from_config, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore,
23+
generate_block_and_insert_transactions, generate_blocks_and_wait, open_channel,
24+
open_channel_push_amt, premine_and_distribute_funds, premine_blocks, prepare_rbf,
25+
random_config, random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder,
26+
setup_node, setup_node_for_async_payments, setup_node_from_config, setup_two_nodes,
27+
wait_for_tx, TestChainSource, TestSyncStore,
2728
};
2829
use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig};
2930
use ldk_node::liquidity::LSPS2ServiceConfig;
@@ -669,19 +670,24 @@ fn onchain_wallet_recovery() {
669670
}
670671

671672
#[test]
672-
fn test_rbf_via_mempool() {
673-
run_rbf_test(false);
673+
fn test_rbf_only_in_mempool() {
674+
run_rbf_test(false, false);
674675
}
675676

676677
#[test]
677-
fn test_rbf_via_direct_block_insertion() {
678-
run_rbf_test(true);
678+
fn test_rbf_direct_block_insertion_rbf_tx() {
679+
run_rbf_test(true, false);
680+
}
681+
682+
#[test]
683+
fn test_rbf_direct_block_insertion_original_tx() {
684+
run_rbf_test(false, true);
679685
}
680686

681687
// `is_insert_block`:
682688
// - `true`: transaction is mined immediately (no mempool), testing confirmed-Tx handling.
683689
// - `false`: transaction stays in mempool until confirmation, testing unconfirmed-Tx handling.
684-
fn run_rbf_test(is_insert_block: bool) {
690+
fn run_rbf_test(is_insert_block: bool, is_insertion_original_tx: bool) {
685691
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
686692
let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(&bitcoind);
687693
let chain_source_electrsd = TestChainSource::Electrum(&electrsd);
@@ -724,58 +730,98 @@ fn run_rbf_test(is_insert_block: bool) {
724730
};
725731
}
726732

733+
macro_rules! validate_total_onchain_balance {
734+
($expected_balance_sat: expr) => {
735+
for node in &nodes {
736+
node.sync_wallets().unwrap();
737+
let balances = node.list_balances();
738+
assert_eq!(balances.total_onchain_balance_sats, $expected_balance_sat);
739+
}
740+
};
741+
}
742+
727743
let scripts_buf: HashSet<ScriptBuf> =
728744
all_addrs.iter().map(|addr| addr.script_pubkey()).collect();
729745
let mut tx;
730746
let mut fee_output_index;
731747

732-
// Modify the output to the nodes
748+
let mut final_amount_sat = 0;
749+
let mut original_tx;
750+
751+
// Step 1: Bump fee and change output address
733752
distribute_funds_all_nodes!();
734753
validate_balances!(amount_sat, false);
735754
(tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf);
755+
original_tx = tx.clone();
736756
tx.output.iter_mut().for_each(|output| {
737757
if scripts_buf.contains(&output.script_pubkey) {
738758
let new_addr = bitcoind.new_address().unwrap();
739759
output.script_pubkey = new_addr.script_pubkey();
740760
}
741761
});
742762
bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block);
743-
validate_balances!(0, is_insert_block);
763+
if is_insertion_original_tx {
764+
generate_block_and_insert_transactions(bitcoind, electrs, &[original_tx.clone()]);
765+
}
766+
if is_insertion_original_tx {
767+
final_amount_sat += amount_sat;
768+
}
769+
validate_balances!(final_amount_sat, is_insert_block || is_insertion_original_tx);
744770

745-
// Not modifying the output scripts, but still bumping the fee.
771+
// Step 2: Bump fee only
746772
distribute_funds_all_nodes!();
747-
validate_balances!(amount_sat, false);
773+
validate_total_onchain_balance!(amount_sat + final_amount_sat);
748774
(tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf);
775+
original_tx = tx.clone();
749776
bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block);
750-
validate_balances!(amount_sat, is_insert_block);
777+
if is_insertion_original_tx {
778+
generate_block_and_insert_transactions(bitcoind, electrs, &[original_tx.clone()]);
779+
}
780+
final_amount_sat += amount_sat;
781+
validate_balances!(final_amount_sat, is_insert_block || is_insertion_original_tx);
751782

752-
let mut final_amount_sat = amount_sat * 2;
783+
// Step 3: Increase output value
753784
let value_sat = 21_000;
754-
755-
// Increase the value of the nodes' outputs
756785
distribute_funds_all_nodes!();
786+
validate_total_onchain_balance!(amount_sat + final_amount_sat);
757787
(tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf);
788+
original_tx = tx.clone();
758789
tx.output.iter_mut().for_each(|output| {
759790
if scripts_buf.contains(&output.script_pubkey) {
760791
output.value = Amount::from_sat(output.value.to_sat() + value_sat);
761792
}
762793
});
794+
tx.output[fee_output_index].value -= Amount::from_sat(scripts_buf.len() as u64 * value_sat);
763795
bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block);
764-
final_amount_sat += value_sat;
765-
validate_balances!(final_amount_sat, is_insert_block);
796+
if is_insertion_original_tx {
797+
generate_block_and_insert_transactions(bitcoind, electrs, &[original_tx.clone()]);
798+
}
799+
final_amount_sat += amount_sat;
800+
if !is_insertion_original_tx {
801+
final_amount_sat += value_sat;
802+
}
803+
validate_balances!(final_amount_sat, is_insert_block || is_insertion_original_tx);
766804

767-
// Decreases the value of the nodes' outputs
805+
// Step 4: Decrease output value
768806
distribute_funds_all_nodes!();
769-
final_amount_sat += amount_sat;
807+
validate_total_onchain_balance!(amount_sat + final_amount_sat);
770808
(tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf);
809+
original_tx = tx.clone();
771810
tx.output.iter_mut().for_each(|output| {
772811
if scripts_buf.contains(&output.script_pubkey) {
773812
output.value = Amount::from_sat(output.value.to_sat() - value_sat);
774813
}
775814
});
815+
tx.output[fee_output_index].value += Amount::from_sat(scripts_buf.len() as u64 * value_sat);
776816
bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block);
777-
final_amount_sat -= value_sat;
778-
validate_balances!(final_amount_sat, is_insert_block);
817+
if is_insertion_original_tx {
818+
generate_block_and_insert_transactions(bitcoind, electrs, &[original_tx.clone()]);
819+
}
820+
final_amount_sat += amount_sat;
821+
if !is_insertion_original_tx {
822+
final_amount_sat -= value_sat;
823+
}
824+
validate_balances!(final_amount_sat, is_insert_block || is_insertion_original_tx);
779825

780826
if !is_insert_block {
781827
generate_blocks_and_wait(bitcoind, electrs, 1);

0 commit comments

Comments
 (0)