Skip to content

Commit 92f072e

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 4eb8745 commit 92f072e

File tree

2 files changed

+208
-29
lines changed

2 files changed

+208
-29
lines changed

tests/common/mod.rs

Lines changed: 139 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ pub(crate) mod logging;
1313
use std::collections::{HashMap, HashSet};
1414
use std::env;
1515
use std::path::PathBuf;
16+
use std::str::FromStr;
1617
use std::sync::{Arc, RwLock};
1718
use std::time::Duration;
1819

19-
use bitcoin::hashes::hex::FromHex;
2020
use bitcoin::hashes::sha256::Hash as Sha256;
21-
use bitcoin::hashes::Hash;
21+
use bitcoin::hashes::{hex::FromHex, Hash};
2222
use bitcoin::{
23-
Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness,
23+
Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxMerkleNode, Txid,
24+
Witness, Wtxid,
2425
};
2526
use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD};
2627
use electrsd::{corepc_node, ElectrsD};
@@ -390,6 +391,136 @@ pub(crate) fn setup_node_for_async_payments(
390391
node
391392
}
392393

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

705+
if is_insert_block {
706+
generate_block_and_insert_transactions(bitcoind, electrs, &[tx.clone()]);
707+
return tx;
708+
}
709+
574710
match bitcoind.send_raw_transaction(&tx) {
575711
Ok(res) => {
576-
if is_insert_block {
577-
generate_blocks_and_wait(bitcoind, electrs, 1);
578-
}
579712
let new_txid: Txid = res.0.parse().unwrap();
580713
wait_for_tx(electrs, new_txid);
581714
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, new_node, open_channel, open_channel_push_amt,
24-
premine_and_distribute_funds, premine_blocks, prepare_rbf, random_config,
25-
random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node,
26-
setup_node_for_async_payments, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore,
23+
generate_block_and_insert_transactions, generate_blocks_and_wait, new_node, 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_two_nodes, wait_for_tx, TestChainSource,
27+
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)