Skip to content

Commit f509489

Browse files
committed
Track reorged on-chain payments as pending
Move affected on-chain payments back to pending when BDK reports that their transaction is unconfirmed again. This keeps payment history aligned with wallet events after a reorg. It does not update payment records directly from disconnected-block notifications. Co-Authored-By: HAL 9000
1 parent b384487 commit f509489

3 files changed

Lines changed: 129 additions & 6 deletions

File tree

src/payment/pending_payment_store.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use lightning::ln::channelmanager::PaymentId;
1111

1212
use crate::data_store::{StorableObject, StorableObjectUpdate};
1313
use crate::payment::store::PaymentDetailsUpdate;
14-
use crate::payment::PaymentDetails;
14+
use crate::payment::{PaymentDetails, PaymentKind};
1515

1616
/// Represents a pending payment
1717
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -68,6 +68,12 @@ impl StorableObject for PendingPaymentDetails {
6868
}
6969
}
7070

71+
if let PaymentKind::Onchain { txid, .. } = &self.details.kind {
72+
let conflicts_len = self.conflicting_txids.len();
73+
self.conflicting_txids.retain(|conflicting_txid| conflicting_txid != txid);
74+
updated |= self.conflicting_txids.len() != conflicts_len;
75+
}
76+
7177
updated
7278
}
7379

@@ -92,3 +98,50 @@ impl From<&PendingPaymentDetails> for PendingPaymentDetailsUpdate {
9298
Self { id: value.id(), payment_update: Some(value.details.to_update()), conflicting_txids }
9399
}
94100
}
101+
102+
#[cfg(test)]
103+
mod tests {
104+
use bitcoin::hashes::Hash;
105+
106+
use super::*;
107+
use crate::payment::{ConfirmationStatus, PaymentDirection, PaymentKind, PaymentStatus};
108+
109+
fn test_txid(byte: u8) -> Txid {
110+
Txid::from_byte_array([byte; 32])
111+
}
112+
113+
fn pending_onchain_payment(payment_id: PaymentId, txid: Txid) -> PaymentDetails {
114+
PaymentDetails::new(
115+
payment_id,
116+
PaymentKind::Onchain { txid, status: ConfirmationStatus::Unconfirmed },
117+
Some(1_000),
118+
Some(100),
119+
PaymentDirection::Outbound,
120+
PaymentStatus::Pending,
121+
)
122+
}
123+
124+
#[test]
125+
fn pending_onchain_conflicts_exclude_current_txid_after_txid_rotation() {
126+
let original_txid = test_txid(1);
127+
let replacement_txid = test_txid(2);
128+
let payment_id = PaymentId(original_txid.to_byte_array());
129+
130+
let mut pending_payment = PendingPaymentDetails::new(
131+
pending_onchain_payment(payment_id, replacement_txid),
132+
vec![original_txid],
133+
);
134+
let update = PendingPaymentDetails::new(
135+
pending_onchain_payment(payment_id, original_txid),
136+
Vec::new(),
137+
)
138+
.to_update();
139+
140+
assert!(pending_payment.update(update));
141+
assert_eq!(
142+
pending_payment.conflicting_txids,
143+
Vec::<Txid>::new(),
144+
"current txid must not remain in its own conflict list"
145+
);
146+
}
147+
}

src/wallet/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ impl Wallet {
345345
}
346346
}
347347
},
348-
WalletEvent::TxUnconfirmed { txid, tx, old_block_time: None } => {
348+
WalletEvent::TxUnconfirmed { txid, tx, .. } => {
349349
let payment_id = self
350350
.find_payment_by_txid(txid)
351351
.unwrap_or_else(|| PaymentId(txid.to_byte_array()));

tests/integration_tests_rust.rs

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ use common::{
2121
expect_channel_pending_event, expect_channel_ready_event, expect_channel_ready_events,
2222
expect_event, expect_payment_claimable_event, expect_payment_received_event,
2323
expect_payment_successful_event, expect_splice_negotiated_event, generate_blocks_and_wait,
24-
generate_listening_addresses, open_channel, open_channel_push_amt, open_channel_with_all,
25-
premine_and_distribute_funds, premine_blocks, prepare_rbf, random_chain_source, random_config,
26-
setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, splice_in_with_all,
27-
wait_for_tx, TestChainSource, TestConfig, TestStoreType, TestSyncStore,
24+
generate_listening_addresses, invalidate_blocks, open_channel, open_channel_push_amt,
25+
open_channel_with_all, premine_and_distribute_funds, premine_blocks, prepare_rbf,
26+
random_chain_source, random_config, setup_bitcoind_and_electrsd, setup_builder, setup_node,
27+
setup_two_nodes, splice_in_with_all, wait_for_block, wait_for_tx, TestChainSource, TestConfig,
28+
TestStoreType, TestSyncStore,
2829
};
2930
use electrsd::corepc_node::Node as BitcoinD;
3031
use electrsd::ElectrsD;
@@ -42,6 +43,7 @@ use lightning::routing::router::RouteParametersConfig;
4243
use lightning_invoice::{Bolt11InvoiceDescription, Description};
4344
use lightning_types::payment::{PaymentHash, PaymentPreimage};
4445
use log::LevelFilter;
46+
use serde_json::json;
4547

4648
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
4749
async fn channel_full_cycle() {
@@ -672,6 +674,74 @@ async fn onchain_send_receive() {
672674
assert_eq!(node_b_payments.len(), 5);
673675
}
674676

677+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
678+
async fn reorged_onchain_payment_returns_to_unconfirmed() {
679+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
680+
let chain_source = random_chain_source(&bitcoind, &electrsd);
681+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
682+
683+
let addr_a = node_a.onchain_payment().new_address().unwrap();
684+
let addr_b = node_b.onchain_payment().new_address().unwrap();
685+
let premine_amount_sat = 500_000;
686+
premine_and_distribute_funds(
687+
&bitcoind.client,
688+
&electrsd.client,
689+
vec![addr_b],
690+
Amount::from_sat(premine_amount_sat),
691+
)
692+
.await;
693+
694+
node_a.sync_wallets().unwrap();
695+
node_b.sync_wallets().unwrap();
696+
697+
let amount_to_send_sats = 100_000;
698+
let txid =
699+
node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap();
700+
wait_for_tx(&electrsd.client, txid).await;
701+
702+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await;
703+
node_a.sync_wallets().unwrap();
704+
node_b.sync_wallets().unwrap();
705+
706+
let payment_id = PaymentId(txid.to_byte_array());
707+
for node in [&node_a, &node_b] {
708+
let payment = node.payment(&payment_id).unwrap();
709+
assert_eq!(payment.status, PaymentStatus::Pending);
710+
match payment.kind {
711+
PaymentKind::Onchain { status, .. } => {
712+
assert!(matches!(status, ConfirmationStatus::Confirmed { .. }));
713+
},
714+
_ => panic!("Unexpected payment kind"),
715+
}
716+
}
717+
718+
let original_height =
719+
bitcoind.client.get_blockchain_info().expect("failed to get blockchain info").blocks;
720+
invalidate_blocks(&bitcoind.client, 1);
721+
let replacement_address = bitcoind.client.new_address().expect("failed to get new address");
722+
for _ in 0..2 {
723+
let _res: serde_json::Value = bitcoind
724+
.client
725+
.call("generateblock", &[json!(replacement_address.to_string()), json!([])])
726+
.expect("failed to generate empty block");
727+
}
728+
wait_for_block(&electrsd.client, original_height as usize + 1).await;
729+
730+
node_a.sync_wallets().unwrap();
731+
node_b.sync_wallets().unwrap();
732+
733+
for node in [&node_a, &node_b] {
734+
let payment = node.payment(&payment_id).unwrap();
735+
assert_eq!(payment.status, PaymentStatus::Pending);
736+
match payment.kind {
737+
PaymentKind::Onchain { status, .. } => {
738+
assert!(matches!(status, ConfirmationStatus::Unconfirmed));
739+
},
740+
_ => panic!("Unexpected payment kind"),
741+
}
742+
}
743+
}
744+
675745
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
676746
async fn onchain_send_all_retains_reserve() {
677747
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

0 commit comments

Comments
 (0)