Skip to content

Commit 51db964

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 431ac3b commit 51db964

File tree

2 files changed

+203
-24
lines changed

2 files changed

+203
-24
lines changed

tests/common/mod.rs

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ use lightning_persister::fs_store::FilesystemStore;
3232
use bitcoin::hashes::sha256::Hash as Sha256;
3333
use bitcoin::hashes::{hex::FromHex, Hash};
3434
use bitcoin::{
35-
Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness,
35+
Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxMerkleNode, Txid,
36+
Witness, Wtxid,
3637
};
3738

3839
use electrsd::corepc_node::Client as BitcoindClient;
@@ -47,6 +48,7 @@ use serde_json::{json, Value};
4748
use std::collections::{HashMap, HashSet};
4849
use std::env;
4950
use std::path::PathBuf;
51+
use std::str::FromStr;
5052
use std::sync::{Arc, RwLock};
5153
use std::time::Duration;
5254

@@ -388,6 +390,136 @@ pub(crate) fn setup_node(
388390
node
389391
}
390392

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

705+
if is_insert_block {
706+
generate_block_and_insert_transactions(bitcoind, electrs, &[tx.clone()]);
707+
return tx;
708+
}
709+
573710
match bitcoind.send_raw_transaction(&tx) {
574711
Ok(res) => {
575-
if is_insert_block {
576-
generate_blocks_and_wait(bitcoind, electrs, 1);
577-
}
578712
let new_txid: Txid = res.0.parse().unwrap();
579713
wait_for_tx(electrs, new_txid);
580714
return tx;

tests/integration_tests_rust.rs

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use common::{
1111
bump_fee_and_broadcast, distribute_funds_unconfirmed, do_channel_full_cycle,
1212
expect_channel_pending_event, expect_channel_ready_event, expect_event,
1313
expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event,
14-
generate_blocks_and_wait,
14+
generate_block_and_insert_transactions, generate_blocks_and_wait,
1515
logging::{init_log_logger, validate_log_entry, TestLogWriter},
1616
new_node, open_channel, premine_and_distribute_funds, premine_blocks, prepare_rbf,
1717
random_config, random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder,
@@ -672,19 +672,24 @@ fn onchain_wallet_recovery() {
672672
}
673673

674674
#[test]
675-
fn test_rbf_via_mempool() {
676-
run_rbf_test(false);
675+
fn test_rbf_only_in_mempool() {
676+
run_rbf_test(false, false);
677677
}
678678

679679
#[test]
680-
fn test_rbf_via_direct_block_insertion() {
681-
run_rbf_test(true);
680+
fn test_rbf_direct_block_insertion_rbf_tx() {
681+
run_rbf_test(true, false);
682+
}
683+
684+
#[test]
685+
fn test_rbf_direct_block_insertion_original_tx() {
686+
run_rbf_test(false, true);
682687
}
683688

684689
// `is_insert_block`:
685690
// - `true`: transaction is mined immediately (no mempool), testing confirmed-Tx handling.
686691
// - `false`: transaction stays in mempool until confirmation, testing unconfirmed-Tx handling.
687-
fn run_rbf_test(is_insert_block: bool) {
692+
fn run_rbf_test(is_insert_block: bool, is_insertion_original_tx: bool) {
688693
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
689694
let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(&bitcoind);
690695
let chain_source_electrsd = TestChainSource::Electrum(&electrsd);
@@ -727,58 +732,98 @@ fn run_rbf_test(is_insert_block: bool) {
727732
};
728733
}
729734

735+
macro_rules! validate_total_onchain_balance {
736+
($expected_balance_sat: expr) => {
737+
for node in &nodes {
738+
node.sync_wallets().unwrap();
739+
let balances = node.list_balances();
740+
assert_eq!(balances.total_onchain_balance_sats, $expected_balance_sat);
741+
}
742+
};
743+
}
744+
730745
let scripts_buf: HashSet<ScriptBuf> =
731746
all_addrs.iter().map(|addr| addr.script_pubkey()).collect();
732747
let mut tx;
733748
let mut fee_output_index;
734749

735-
// Modify the output to the nodes
750+
let mut final_amount_sat = 0;
751+
let mut original_tx;
752+
753+
// Step 1: Bump fee and change output address
736754
distribute_funds_all_nodes!();
737755
validate_balances!(amount_sat, false);
738756
(tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf);
757+
original_tx = tx.clone();
739758
tx.output.iter_mut().for_each(|output| {
740759
if scripts_buf.contains(&output.script_pubkey) {
741760
let new_addr = bitcoind.new_address().unwrap();
742761
output.script_pubkey = new_addr.script_pubkey();
743762
}
744763
});
745764
bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block);
746-
validate_balances!(0, is_insert_block);
765+
if is_insertion_original_tx {
766+
generate_block_and_insert_transactions(bitcoind, electrs, &[original_tx.clone()]);
767+
}
768+
if is_insertion_original_tx {
769+
final_amount_sat += amount_sat;
770+
}
771+
validate_balances!(final_amount_sat, is_insert_block || is_insertion_original_tx);
747772

748-
// Not modifying the output scripts, but still bumping the fee.
773+
// Step 2: Bump fee only
749774
distribute_funds_all_nodes!();
750-
validate_balances!(amount_sat, false);
775+
validate_total_onchain_balance!(amount_sat + final_amount_sat);
751776
(tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf);
777+
original_tx = tx.clone();
752778
bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block);
753-
validate_balances!(amount_sat, is_insert_block);
779+
if is_insertion_original_tx {
780+
generate_block_and_insert_transactions(bitcoind, electrs, &[original_tx.clone()]);
781+
}
782+
final_amount_sat += amount_sat;
783+
validate_balances!(final_amount_sat, is_insert_block || is_insertion_original_tx);
754784

755-
let mut final_amount_sat = amount_sat * 2;
785+
// Step 3: Increase output value
756786
let value_sat = 21_000;
757-
758-
// Increase the value of the nodes' outputs
759787
distribute_funds_all_nodes!();
788+
validate_total_onchain_balance!(amount_sat + final_amount_sat);
760789
(tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf);
790+
original_tx = tx.clone();
761791
tx.output.iter_mut().for_each(|output| {
762792
if scripts_buf.contains(&output.script_pubkey) {
763793
output.value = Amount::from_sat(output.value.to_sat() + value_sat);
764794
}
765795
});
796+
tx.output[fee_output_index].value -= Amount::from_sat(scripts_buf.len() as u64 * value_sat);
766797
bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block);
767-
final_amount_sat += value_sat;
768-
validate_balances!(final_amount_sat, is_insert_block);
798+
if is_insertion_original_tx {
799+
generate_block_and_insert_transactions(bitcoind, electrs, &[original_tx.clone()]);
800+
}
801+
final_amount_sat += amount_sat;
802+
if !is_insertion_original_tx {
803+
final_amount_sat += value_sat;
804+
}
805+
validate_balances!(final_amount_sat, is_insert_block || is_insertion_original_tx);
769806

770-
// Decreases the value of the nodes' outputs
807+
// Step 4: Decrease output value
771808
distribute_funds_all_nodes!();
772-
final_amount_sat += amount_sat;
809+
validate_total_onchain_balance!(amount_sat + final_amount_sat);
773810
(tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf);
811+
original_tx = tx.clone();
774812
tx.output.iter_mut().for_each(|output| {
775813
if scripts_buf.contains(&output.script_pubkey) {
776814
output.value = Amount::from_sat(output.value.to_sat() - value_sat);
777815
}
778816
});
817+
tx.output[fee_output_index].value += Amount::from_sat(scripts_buf.len() as u64 * value_sat);
779818
bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block);
780-
final_amount_sat -= value_sat;
781-
validate_balances!(final_amount_sat, is_insert_block);
819+
if is_insertion_original_tx {
820+
generate_block_and_insert_transactions(bitcoind, electrs, &[original_tx.clone()]);
821+
}
822+
final_amount_sat += amount_sat;
823+
if !is_insertion_original_tx {
824+
final_amount_sat -= value_sat;
825+
}
826+
validate_balances!(final_amount_sat, is_insert_block || is_insertion_original_tx);
782827

783828
if !is_insert_block {
784829
generate_blocks_and_wait(bitcoind, electrs, 1);

0 commit comments

Comments
 (0)