Skip to content

Commit 947acf3

Browse files
authored
Merge pull request #605 from moisesPompilio/fix-evicted-transation
Fix evicted transation
2 parents 1c02114 + f3aab90 commit 947acf3

File tree

3 files changed

+405
-26
lines changed

3 files changed

+405
-26
lines changed

src/chain/bitcoind.rs

Lines changed: 179 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,11 @@ impl BitcoindChainSource {
370370
let cur_height = channel_manager.current_best_block().height;
371371

372372
let now = SystemTime::now();
373-
let unconfirmed_txids = self.onchain_wallet.get_unconfirmed_txids();
374-
match self.api_client.get_updated_mempool_transactions(cur_height, unconfirmed_txids).await
373+
let bdk_unconfirmed_txids = self.onchain_wallet.get_unconfirmed_txids();
374+
match self
375+
.api_client
376+
.get_updated_mempool_transactions(cur_height, bdk_unconfirmed_txids)
377+
.await
375378
{
376379
Ok((unconfirmed_txs, evicted_txids)) => {
377380
log_trace!(
@@ -754,7 +757,7 @@ impl BitcoindClient {
754757
async fn get_raw_transaction_rpc(
755758
rpc_client: Arc<RpcClient>, txid: &Txid,
756759
) -> std::io::Result<Option<Transaction>> {
757-
let txid_hex = bitcoin::consensus::encode::serialize_hex(txid);
760+
let txid_hex = txid.to_string();
758761
let txid_json = serde_json::json!(txid_hex);
759762
match rpc_client
760763
.call_method::<GetRawTransactionResponse>("getrawtransaction", &[txid_json])
@@ -792,7 +795,7 @@ impl BitcoindClient {
792795
async fn get_raw_transaction_rest(
793796
rest_client: Arc<RestClient>, txid: &Txid,
794797
) -> std::io::Result<Option<Transaction>> {
795-
let txid_hex = bitcoin::consensus::encode::serialize_hex(txid);
798+
let txid_hex = txid.to_string();
796799
let tx_path = format!("tx/{}.json", txid_hex);
797800
match rest_client
798801
.request_resource::<JsonResponse, GetRawTransactionResponse>(&tx_path)
@@ -889,7 +892,7 @@ impl BitcoindClient {
889892
async fn get_mempool_entry_inner(
890893
client: Arc<RpcClient>, txid: Txid,
891894
) -> std::io::Result<Option<MempoolEntry>> {
892-
let txid_hex = bitcoin::consensus::encode::serialize_hex(&txid);
895+
let txid_hex = txid.to_string();
893896
let txid_json = serde_json::json!(txid_hex);
894897

895898
match client.call_method::<GetMempoolEntryResponse>("getmempoolentry", &[txid_json]).await {
@@ -964,11 +967,12 @@ impl BitcoindClient {
964967
/// - mempool transactions, alongside their first-seen unix timestamps.
965968
/// - transactions that have been evicted from the mempool, alongside the last time they were seen absent.
966969
pub(crate) async fn get_updated_mempool_transactions(
967-
&self, best_processed_height: u32, unconfirmed_txids: Vec<Txid>,
970+
&self, best_processed_height: u32, bdk_unconfirmed_txids: Vec<Txid>,
968971
) -> std::io::Result<(Vec<(Transaction, u64)>, Vec<(Txid, u64)>)> {
969972
let mempool_txs =
970973
self.get_mempool_transactions_and_timestamp_at_height(best_processed_height).await?;
971-
let evicted_txids = self.get_evicted_mempool_txids_and_timestamp(unconfirmed_txids).await?;
974+
let evicted_txids =
975+
self.get_evicted_mempool_txids_and_timestamp(bdk_unconfirmed_txids).await?;
972976
Ok((mempool_txs, evicted_txids))
973977
}
974978

@@ -1078,22 +1082,22 @@ impl BitcoindClient {
10781082
// To this end, we first update our local mempool_entries_cache and then return all unconfirmed
10791083
// wallet `Txid`s that don't appear in the mempool still.
10801084
async fn get_evicted_mempool_txids_and_timestamp(
1081-
&self, unconfirmed_txids: Vec<Txid>,
1085+
&self, bdk_unconfirmed_txids: Vec<Txid>,
10821086
) -> std::io::Result<Vec<(Txid, u64)>> {
10831087
match self {
10841088
BitcoindClient::Rpc { latest_mempool_timestamp, mempool_entries_cache, .. } => {
10851089
Self::get_evicted_mempool_txids_and_timestamp_inner(
10861090
latest_mempool_timestamp,
10871091
mempool_entries_cache,
1088-
unconfirmed_txids,
1092+
bdk_unconfirmed_txids,
10891093
)
10901094
.await
10911095
},
10921096
BitcoindClient::Rest { latest_mempool_timestamp, mempool_entries_cache, .. } => {
10931097
Self::get_evicted_mempool_txids_and_timestamp_inner(
10941098
latest_mempool_timestamp,
10951099
mempool_entries_cache,
1096-
unconfirmed_txids,
1100+
bdk_unconfirmed_txids,
10971101
)
10981102
.await
10991103
},
@@ -1103,13 +1107,13 @@ impl BitcoindClient {
11031107
async fn get_evicted_mempool_txids_and_timestamp_inner(
11041108
latest_mempool_timestamp: &AtomicU64,
11051109
mempool_entries_cache: &tokio::sync::Mutex<HashMap<Txid, MempoolEntry>>,
1106-
unconfirmed_txids: Vec<Txid>,
1110+
bdk_unconfirmed_txids: Vec<Txid>,
11071111
) -> std::io::Result<Vec<(Txid, u64)>> {
11081112
let latest_mempool_timestamp = latest_mempool_timestamp.load(Ordering::Relaxed);
11091113
let mempool_entries_cache = mempool_entries_cache.lock().await;
1110-
let evicted_txids = unconfirmed_txids
1114+
let evicted_txids = bdk_unconfirmed_txids
11111115
.into_iter()
1112-
.filter(|txid| mempool_entries_cache.contains_key(txid))
1116+
.filter(|txid| !mempool_entries_cache.contains_key(txid))
11131117
.map(|txid| (txid, latest_mempool_timestamp))
11141118
.collect();
11151119
Ok(evicted_txids)
@@ -1236,7 +1240,7 @@ impl TryInto<GetRawMempoolResponse> for JsonResponse {
12361240

12371241
for hex in res {
12381242
let txid = if let Some(hex_str) = hex.as_str() {
1239-
match bitcoin::consensus::encode::deserialize_hex(hex_str) {
1243+
match hex_str.parse::<Txid>() {
12401244
Ok(txid) => txid,
12411245
Err(_) => {
12421246
return Err(std::io::Error::new(
@@ -1407,3 +1411,164 @@ impl std::fmt::Display for HttpError {
14071411
write!(f, "status_code: {}, contents: {}", self.status_code, contents)
14081412
}
14091413
}
1414+
1415+
#[cfg(test)]
1416+
mod tests {
1417+
use bitcoin::hashes::Hash;
1418+
use bitcoin::{FeeRate, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness};
1419+
use lightning_block_sync::http::JsonResponse;
1420+
use proptest::{arbitrary::any, collection::vec, prop_assert_eq, prop_compose, proptest};
1421+
use serde_json::json;
1422+
1423+
use crate::chain::bitcoind::{
1424+
FeeResponse, GetMempoolEntryResponse, GetRawMempoolResponse, GetRawTransactionResponse,
1425+
MempoolMinFeeResponse,
1426+
};
1427+
1428+
prop_compose! {
1429+
fn arbitrary_witness()(
1430+
witness_elements in vec(vec(any::<u8>(), 0..100), 0..20)
1431+
) -> Witness {
1432+
let mut witness = Witness::new();
1433+
for element in witness_elements {
1434+
witness.push(element);
1435+
}
1436+
witness
1437+
}
1438+
}
1439+
1440+
prop_compose! {
1441+
fn arbitrary_txin()(
1442+
outpoint_hash in any::<[u8; 32]>(),
1443+
outpoint_vout in any::<u32>(),
1444+
script_bytes in vec(any::<u8>(), 0..100),
1445+
witness in arbitrary_witness(),
1446+
sequence in any::<u32>()
1447+
) -> TxIn {
1448+
TxIn {
1449+
previous_output: OutPoint {
1450+
txid: Txid::from_byte_array(outpoint_hash),
1451+
vout: outpoint_vout,
1452+
},
1453+
script_sig: ScriptBuf::from_bytes(script_bytes),
1454+
sequence: bitcoin::Sequence::from_consensus(sequence),
1455+
witness,
1456+
}
1457+
}
1458+
}
1459+
1460+
prop_compose! {
1461+
fn arbitrary_txout()(
1462+
value in 0u64..21_000_000_00_000_000u64,
1463+
script_bytes in vec(any::<u8>(), 0..100)
1464+
) -> TxOut {
1465+
TxOut {
1466+
value: bitcoin::Amount::from_sat(value),
1467+
script_pubkey: ScriptBuf::from_bytes(script_bytes),
1468+
}
1469+
}
1470+
}
1471+
1472+
prop_compose! {
1473+
fn arbitrary_transaction()(
1474+
version in any::<i32>(),
1475+
inputs in vec(arbitrary_txin(), 1..20),
1476+
outputs in vec(arbitrary_txout(), 1..20),
1477+
lock_time in any::<u32>()
1478+
) -> Transaction {
1479+
Transaction {
1480+
version: bitcoin::transaction::Version(version),
1481+
input: inputs,
1482+
output: outputs,
1483+
lock_time: bitcoin::absolute::LockTime::from_consensus(lock_time),
1484+
}
1485+
}
1486+
}
1487+
1488+
proptest! {
1489+
#![proptest_config(proptest::test_runner::Config::with_cases(20))]
1490+
1491+
#[test]
1492+
fn prop_get_raw_mempool_response_roundtrip(txids in vec(any::<[u8;32]>(), 0..10)) {
1493+
let txid_vec: Vec<Txid> = txids.into_iter().map(Txid::from_byte_array).collect();
1494+
let original = GetRawMempoolResponse(txid_vec.clone());
1495+
1496+
let json_vec: Vec<String> = txid_vec.iter().map(|t| t.to_string()).collect();
1497+
let json_val = serde_json::Value::Array(json_vec.iter().map(|s| json!(s)).collect());
1498+
1499+
let resp = JsonResponse(json_val);
1500+
let decoded: GetRawMempoolResponse = resp.try_into().unwrap();
1501+
1502+
prop_assert_eq!(original.0.len(), decoded.0.len());
1503+
1504+
prop_assert_eq!(original.0, decoded.0);
1505+
}
1506+
1507+
#[test]
1508+
fn prop_get_mempool_entry_response_roundtrip(
1509+
time in any::<u64>(),
1510+
height in any::<u32>()
1511+
) {
1512+
let json_val = json!({
1513+
"time": time,
1514+
"height": height
1515+
});
1516+
1517+
let resp = JsonResponse(json_val);
1518+
let decoded: GetMempoolEntryResponse = resp.try_into().unwrap();
1519+
1520+
prop_assert_eq!(decoded.time, time);
1521+
prop_assert_eq!(decoded.height, height);
1522+
}
1523+
1524+
#[test]
1525+
fn prop_get_raw_transaction_response_roundtrip(tx in arbitrary_transaction()) {
1526+
let hex = bitcoin::consensus::encode::serialize_hex(&tx);
1527+
let json_val = serde_json::Value::String(hex.clone());
1528+
1529+
let resp = JsonResponse(json_val);
1530+
let decoded: GetRawTransactionResponse = resp.try_into().unwrap();
1531+
1532+
prop_assert_eq!(decoded.0.compute_txid(), tx.compute_txid());
1533+
prop_assert_eq!(decoded.0.compute_wtxid(), tx.compute_wtxid());
1534+
1535+
prop_assert_eq!(decoded.0, tx);
1536+
}
1537+
1538+
#[test]
1539+
fn prop_fee_response_roundtrip(fee_rate in any::<f64>()) {
1540+
let fee_rate = fee_rate.abs();
1541+
let json_val = json!({
1542+
"feerate": fee_rate,
1543+
"errors": serde_json::Value::Null
1544+
});
1545+
1546+
let resp = JsonResponse(json_val);
1547+
let decoded: FeeResponse = resp.try_into().unwrap();
1548+
1549+
let expected = {
1550+
let fee_rate_sat_per_kwu = (fee_rate * 25_000_000.0).round() as u64;
1551+
FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu)
1552+
};
1553+
prop_assert_eq!(decoded.0, expected);
1554+
}
1555+
1556+
#[test]
1557+
fn prop_mempool_min_fee_response_roundtrip(fee_rate in any::<f64>()) {
1558+
let fee_rate = fee_rate.abs();
1559+
let json_val = json!({
1560+
"mempoolminfee": fee_rate
1561+
});
1562+
1563+
let resp = JsonResponse(json_val);
1564+
let decoded: MempoolMinFeeResponse = resp.try_into().unwrap();
1565+
1566+
let expected = {
1567+
let fee_rate_sat_per_kwu = (fee_rate * 25_000_000.0).round() as u64;
1568+
FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu)
1569+
};
1570+
prop_assert_eq!(decoded.0, expected);
1571+
}
1572+
1573+
}
1574+
}

tests/common/mod.rs

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ use lightning_types::payment::{PaymentHash, PaymentPreimage};
3030
use lightning_persister::fs_store::FilesystemStore;
3131

3232
use bitcoin::hashes::sha256::Hash as Sha256;
33-
use bitcoin::hashes::Hash;
34-
use bitcoin::{Address, Amount, Network, OutPoint, Txid};
33+
use bitcoin::hashes::{hex::FromHex, Hash};
34+
use bitcoin::{
35+
Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness,
36+
};
3537

3638
use electrsd::corepc_node::Client as BitcoindClient;
3739
use electrsd::corepc_node::Node as BitcoinD;
@@ -42,7 +44,7 @@ use rand::distributions::Alphanumeric;
4244
use rand::{thread_rng, Rng};
4345
use serde_json::{json, Value};
4446

45-
use std::collections::HashMap;
47+
use std::collections::{HashMap, HashSet};
4648
use std::env;
4749
use std::path::PathBuf;
4850
use std::sync::{Arc, RwLock};
@@ -487,12 +489,25 @@ where
487489
pub(crate) fn premine_and_distribute_funds<E: ElectrumApi>(
488490
bitcoind: &BitcoindClient, electrs: &E, addrs: Vec<Address>, amount: Amount,
489491
) {
492+
premine_blocks(bitcoind, electrs);
493+
494+
distribute_funds_unconfirmed(bitcoind, electrs, addrs, amount);
495+
generate_blocks_and_wait(bitcoind, electrs, 1);
496+
}
497+
498+
pub(crate) fn premine_blocks<E: ElectrumApi>(bitcoind: &BitcoindClient, electrs: &E) {
490499
let _ = bitcoind.create_wallet("ldk_node_test");
491500
let _ = bitcoind.load_wallet("ldk_node_test");
492501
generate_blocks_and_wait(bitcoind, electrs, 101);
502+
}
493503

494-
let amounts: HashMap<String, f64> =
495-
addrs.iter().map(|addr| (addr.to_string(), amount.to_btc())).collect();
504+
pub(crate) fn distribute_funds_unconfirmed<E: ElectrumApi>(
505+
bitcoind: &BitcoindClient, electrs: &E, addrs: Vec<Address>, amount: Amount,
506+
) -> Txid {
507+
let mut amounts = HashMap::<String, f64>::new();
508+
for addr in &addrs {
509+
amounts.insert(addr.to_string(), amount.to_btc());
510+
}
496511

497512
let empty_account = json!("");
498513
let amounts_json = json!(amounts);
@@ -505,7 +520,70 @@ pub(crate) fn premine_and_distribute_funds<E: ElectrumApi>(
505520
.unwrap();
506521

507522
wait_for_tx(electrs, txid);
508-
generate_blocks_and_wait(bitcoind, electrs, 1);
523+
524+
txid
525+
}
526+
527+
pub(crate) fn prepare_rbf<E: ElectrumApi>(
528+
electrs: &E, txid: Txid, scripts_buf: &HashSet<ScriptBuf>,
529+
) -> (Transaction, usize) {
530+
let tx = electrs.transaction_get(&txid).unwrap();
531+
532+
let fee_output_index = tx
533+
.output
534+
.iter()
535+
.position(|output| !scripts_buf.contains(&output.script_pubkey))
536+
.expect("No output available for fee bumping");
537+
538+
(tx, fee_output_index)
539+
}
540+
541+
pub(crate) fn bump_fee_and_broadcast<E: ElectrumApi>(
542+
bitcoind: &BitcoindClient, electrs: &E, mut tx: Transaction, fee_output_index: usize,
543+
is_insert_block: bool,
544+
) -> Transaction {
545+
let mut bump_fee_amount_sat = tx.vsize() as u64;
546+
let attempts = 5;
547+
548+
for _ in 0..attempts {
549+
let fee_output = &mut tx.output[fee_output_index];
550+
let new_fee_value = fee_output.value.to_sat().saturating_sub(bump_fee_amount_sat);
551+
if new_fee_value < 546 {
552+
panic!("Warning: Fee output approaching dust limit ({} sats)", new_fee_value);
553+
}
554+
fee_output.value = Amount::from_sat(new_fee_value);
555+
556+
for input in &mut tx.input {
557+
input.sequence = Sequence::ENABLE_RBF_NO_LOCKTIME;
558+
input.script_sig = ScriptBuf::new();
559+
input.witness = Witness::new();
560+
}
561+
562+
let signed_result = bitcoind.sign_raw_transaction_with_wallet(&tx).unwrap();
563+
assert!(signed_result.complete, "Failed to sign RBF transaction");
564+
565+
let tx_bytes = Vec::<u8>::from_hex(&signed_result.hex).unwrap();
566+
tx = bitcoin::consensus::encode::deserialize::<Transaction>(&tx_bytes).unwrap();
567+
568+
match bitcoind.send_raw_transaction(&tx) {
569+
Ok(res) => {
570+
if is_insert_block {
571+
generate_blocks_and_wait(bitcoind, electrs, 1);
572+
}
573+
let new_txid: Txid = res.0.parse().unwrap();
574+
wait_for_tx(electrs, new_txid);
575+
return tx;
576+
},
577+
Err(_) => {
578+
bump_fee_amount_sat += bump_fee_amount_sat * 5;
579+
if tx.output[fee_output_index].value.to_sat() < bump_fee_amount_sat {
580+
panic!("Insufficient funds to increase fee");
581+
}
582+
},
583+
}
584+
}
585+
586+
panic!("Failed to bump fee after {} attempts", attempts);
509587
}
510588

511589
pub fn open_channel(

0 commit comments

Comments
 (0)