Skip to content

Commit fb1c52c

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 fb1c52c

File tree

2 files changed

+212
-30
lines changed

2 files changed

+212
-30
lines changed

tests/common/mod.rs

Lines changed: 143 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,22 @@ use std::env;
1616
use std::future::Future;
1717
use std::path::PathBuf;
1818
use std::pin::Pin;
19+
use std::str::FromStr;
1920
use std::sync::{Arc, RwLock};
2021
use std::time::Duration;
2122

22-
use bitcoin::hashes::hex::FromHex;
23-
use bitcoin::hashes::sha256::Hash as Sha256;
24-
use bitcoin::hashes::Hash;
23+
use bitcoin::absolute::LockTime;
24+
use bitcoin::block::{Header, Version as BlockVersion};
25+
use bitcoin::hashes::{hex::FromHex, sha256::Hash as Sha256, Hash};
26+
use bitcoin::merkle_tree::calculate_root;
27+
use bitcoin::opcodes::all::OP_RETURN;
28+
use bitcoin::script::Builder as BuilderScriptBitcoin;
29+
use bitcoin::transaction::Version;
2530
use bitcoin::{
26-
Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness,
31+
Address, Amount, Block, BlockHash, CompactTarget, Network, OutPoint, ScriptBuf, Sequence,
32+
Transaction, TxMerkleNode, Txid, Witness, Wtxid,
2733
};
34+
use electrsd::corepc_node::TemplateRules;
2835
use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD};
2936
use electrsd::{corepc_node, ElectrsD};
3037
use electrum_client::ElectrumApi;
@@ -394,6 +401,133 @@ pub(crate) fn setup_node_for_async_payments(
394401
node
395402
}
396403

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

712+
if is_insert_block {
713+
generate_block_and_insert_transactions(bitcoind, electrs, &[tx.clone()]);
714+
return tx;
715+
}
716+
578717
match bitcoind.send_raw_transaction(&tx) {
579718
Ok(res) => {
580-
if is_insert_block {
581-
generate_blocks_and_wait(bitcoind, electrs, 1);
582-
}
583719
let new_txid: Txid = res.0.parse().unwrap();
584720
wait_for_tx(electrs, new_txid);
585721
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)