Skip to content

Commit e7d55fb

Browse files
committed
Emit settled on-chain payment events
Notify users when otherwise unclassified on-chain payments reach the anti-reorg confirmation depth. Use the wallet's effective stored payment so classified funding, splice, close, and sweep transactions stay quiet. Co-Authored-By: HAL 9000
1 parent 0a6010e commit e7d55fb

10 files changed

Lines changed: 636 additions & 61 deletions

File tree

bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,28 @@ class LibraryTest {
226226
node1.syncWallets()
227227
node2.syncWallets()
228228

229+
val onchainPaymentReceivedEvent1 = node1.waitNextEvent()
230+
println("Got event: $onchainPaymentReceivedEvent1")
231+
when (onchainPaymentReceivedEvent1) {
232+
is Event.OnchainPaymentReceived -> {
233+
assertEquals(txid1, onchainPaymentReceivedEvent1.txid)
234+
assertEquals(100000000uL, onchainPaymentReceivedEvent1.amountMsat)
235+
}
236+
else -> error("Expected initial on-chain payment event")
237+
}
238+
node1.eventHandled()
239+
240+
val onchainPaymentReceivedEvent2 = node2.waitNextEvent()
241+
println("Got event: $onchainPaymentReceivedEvent2")
242+
when (onchainPaymentReceivedEvent2) {
243+
is Event.OnchainPaymentReceived -> {
244+
assertEquals(txid2, onchainPaymentReceivedEvent2.txid)
245+
assertEquals(100000000uL, onchainPaymentReceivedEvent2.amountMsat)
246+
}
247+
else -> error("Expected initial on-chain payment event")
248+
}
249+
node2.eventHandled()
250+
229251
val spendableBalance1 = node1.listBalances().spendableOnchainBalanceSats
230252
val spendableBalance2 = node2.listBalances().spendableOnchainBalanceSats
231253
val totalBalance1 = node1.listBalances().totalOnchainBalanceSats

bindings/python/src/ldk_node/test_ldk_node.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ def fund_nodes(node_1, node_2, esplora_endpoint, amount_sats=100000):
158158
node_1.sync_wallets()
159159
node_2.sync_wallets()
160160

161+
received_event_1 = expect_event(node_1, Event.ONCHAIN_PAYMENT_RECEIVED)
162+
assert received_event_1.txid == txid_1
163+
assert received_event_1.amount_msat == amount_sats * 1000
164+
165+
received_event_2 = expect_event(node_2, Event.ONCHAIN_PAYMENT_RECEIVED)
166+
assert received_event_2.txid == txid_2
167+
assert received_event_2.amount_msat == amount_sats * 1000
168+
161169
def open_channel_and_wait_ready(node_1, node_2, node_id_2, listening_address_2, esplora_endpoint, channel_amount_sats=50000):
162170
node_1.open_channel(node_id_2, listening_address_2, channel_amount_sats, None, None)
163171

src/event.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::sync::{Arc, Mutex};
1313

1414
use bitcoin::blockdata::locktime::absolute::LockTime;
1515
use bitcoin::secp256k1::PublicKey;
16-
use bitcoin::{Amount, OutPoint};
16+
use bitcoin::{Amount, BlockHash, OutPoint, Txid};
1717
use lightning::blinded_path::message::NextMessageHop;
1818
use lightning::events::bump_transaction::BumpTransactionEvent;
1919
#[cfg(not(feature = "uniffi"))]
@@ -271,6 +271,46 @@ pub enum Event {
271271
/// This will be `None` for events serialized by LDK Node v0.2.1 and prior.
272272
reason: Option<ClosureReason>,
273273
},
274+
/// A sent on-chain payment was successful.
275+
///
276+
/// This is only emitted for wallet transactions which were not classified as channel
277+
/// funding, splices, closes, sweeps, or other LDK-driven chain activity.
278+
///
279+
/// It's guaranteed to have reached at least [`ANTI_REORG_DELAY`] confirmations.
280+
///
281+
/// [`ANTI_REORG_DELAY`]: lightning::chain::channelmonitor::ANTI_REORG_DELAY
282+
OnchainPaymentSuccessful {
283+
/// A local identifier used to track the payment.
284+
payment_id: PaymentId,
285+
/// The transaction identifier.
286+
txid: Txid,
287+
/// The value, in thousandths of a satoshi, that was sent.
288+
amount_msat: u64,
289+
/// The hash of the block in which the transaction was confirmed.
290+
block_hash: BlockHash,
291+
/// The height at which the block was confirmed.
292+
block_height: u32,
293+
},
294+
/// An on-chain payment has been received.
295+
///
296+
/// This is only emitted for wallet transactions which were not classified as channel
297+
/// funding, splices, closes, sweeps, or other LDK-driven chain activity.
298+
///
299+
/// It's guaranteed to have reached at least [`ANTI_REORG_DELAY`] confirmations.
300+
///
301+
/// [`ANTI_REORG_DELAY`]: lightning::chain::channelmonitor::ANTI_REORG_DELAY
302+
OnchainPaymentReceived {
303+
/// A local identifier used to track the payment.
304+
payment_id: PaymentId,
305+
/// The transaction identifier.
306+
txid: Txid,
307+
/// The value, in thousandths of a satoshi, that has been received.
308+
amount_msat: u64,
309+
/// The hash of the block in which the transaction was confirmed.
310+
block_hash: BlockHash,
311+
/// The height at which the block was confirmed.
312+
block_height: u32,
313+
},
274314
/// A channel splice has been negotiated and the funding transaction is pending
275315
/// confirmation on-chain.
276316
SpliceNegotiated {
@@ -374,6 +414,20 @@ impl_writeable_tlv_based_enum!(Event,
374414
(5, user_channel_id, required),
375415
// TLV 7 (abandoned_funding_txo) may be set for LDK Node v0.7.
376416
},
417+
(10, OnchainPaymentSuccessful) => {
418+
(0, payment_id, required),
419+
(2, txid, required),
420+
(4, amount_msat, required),
421+
(6, block_hash, required),
422+
(8, block_height, required),
423+
},
424+
(11, OnchainPaymentReceived) => {
425+
(0, payment_id, required),
426+
(2, txid, required),
427+
(4, amount_msat, required),
428+
(6, block_hash, required),
429+
(8, block_height, required),
430+
},
377431
);
378432

379433
pub struct EventQueue<L: Deref>

src/wallet/mod.rs

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ use lightning_invoice::RawBolt11Invoice;
5454
use persist::KVStoreWalletPersister;
5555

5656
use crate::config::Config;
57-
use crate::event::EventQueue;
57+
use crate::event::{Event, EventQueue};
5858
use crate::fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator};
5959
use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger};
6060
use crate::payment::store::ConfirmationStatus;
@@ -278,16 +278,22 @@ impl Wallet {
278278
confirmation_status,
279279
);
280280

281-
self.runtime.block_on(self.payment_store.insert_or_update(payment.clone()))?;
281+
self.runtime.block_on(async {
282+
let (updated, stored_payment) =
283+
self.payment_store.insert_or_update_and_get(payment).await?;
282284

283-
if payment_status == PaymentStatus::Pending {
284-
let pending_payment =
285-
self.create_pending_payment_from_tx(payment, Vec::new());
285+
if updated && payment_status == PaymentStatus::Succeeded {
286+
self.emit_onchain_payment_event(&stored_payment).await?;
287+
}
286288

287-
self.runtime.block_on(
288-
self.pending_payment_store.insert_or_update(pending_payment),
289-
)?;
290-
}
289+
if payment_status == PaymentStatus::Pending {
290+
let pending_payment =
291+
self.create_pending_payment_from_tx(stored_payment, Vec::new());
292+
self.pending_payment_store.insert_or_update(pending_payment).await?;
293+
}
294+
295+
Ok::<(), Error>(())
296+
})?;
291297
},
292298
WalletEvent::ChainTipChanged { new_tip, .. } => {
293299
let pending_payments: Vec<PendingPaymentDetails> =
@@ -304,27 +310,34 @@ impl Wallet {
304310
let mut unconfirmed_outbound_txids: Vec<Txid> = Vec::new();
305311

306312
for mut payment in pending_payments {
307-
match payment.details.kind {
313+
match &payment.details.kind {
308314
PaymentKind::Onchain {
309315
status: ConfirmationStatus::Confirmed { height, .. },
310316
..
311317
} => {
312318
let payment_id = payment.details.id;
313-
if new_tip.height >= height + ANTI_REORG_DELAY - 1 {
319+
if new_tip.height >= *height + ANTI_REORG_DELAY - 1 {
314320
payment.details.status = PaymentStatus::Succeeded;
315-
self.runtime.block_on(
316-
self.payment_store.insert_or_update(payment.details),
317-
)?;
318-
self.runtime
319-
.block_on(self.pending_payment_store.remove(&payment_id))?;
321+
self.runtime.block_on(async {
322+
let (updated, stored_payment) = self
323+
.payment_store
324+
.insert_or_update_and_get(payment.details)
325+
.await?;
326+
if updated {
327+
self.emit_onchain_payment_event(&stored_payment)
328+
.await?;
329+
}
330+
self.pending_payment_store.remove(&payment_id).await?;
331+
Ok::<(), Error>(())
332+
})?;
320333
}
321334
},
322335
PaymentKind::Onchain {
323336
txid,
324337
status: ConfirmationStatus::Unconfirmed,
325338
..
326339
} if payment.details.direction == PaymentDirection::Outbound => {
327-
unconfirmed_outbound_txids.push(txid);
340+
unconfirmed_outbound_txids.push(*txid);
328341
},
329342
_ => {},
330343
}
@@ -1447,6 +1460,52 @@ impl Wallet {
14471460
PendingPaymentDetails::new(payment, conflicting_txids, Vec::new())
14481461
}
14491462

1463+
async fn emit_onchain_payment_event(&self, payment: &PaymentDetails) -> Result<(), Error> {
1464+
if payment.status != PaymentStatus::Succeeded {
1465+
return Ok(());
1466+
}
1467+
1468+
let (txid, block_hash, block_height) = match &payment.kind {
1469+
PaymentKind::Onchain {
1470+
txid,
1471+
status: ConfirmationStatus::Confirmed { block_hash, height, .. },
1472+
tx_type: None,
1473+
} => (*txid, *block_hash, *height),
1474+
_ => return Ok(()),
1475+
};
1476+
1477+
let Some(amount_msat) = payment.amount_msat else {
1478+
log_error!(
1479+
self.logger,
1480+
"Skipping on-chain payment event for {} due to missing amount",
1481+
payment.id
1482+
);
1483+
return Ok(());
1484+
};
1485+
1486+
let event = match payment.direction {
1487+
PaymentDirection::Outbound => Event::OnchainPaymentSuccessful {
1488+
payment_id: payment.id,
1489+
txid,
1490+
amount_msat,
1491+
block_hash,
1492+
block_height,
1493+
},
1494+
PaymentDirection::Inbound => Event::OnchainPaymentReceived {
1495+
payment_id: payment.id,
1496+
txid,
1497+
amount_msat,
1498+
block_hash,
1499+
block_height,
1500+
},
1501+
};
1502+
1503+
self.event_queue.add_event(event).await.map_err(|e| {
1504+
log_error!(self.logger, "Failed to push on-chain payment event: {}", e);
1505+
Error::PersistenceFailed
1506+
})
1507+
}
1508+
14501509
fn find_payment_by_txid(&self, target_txid: Txid) -> Option<PaymentId> {
14511510
let direct_payment_id = PaymentId(target_txid.to_byte_array());
14521511
if self.pending_payment_store.contains_key(&direct_payment_id) {

tests/common/mod.rs

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ use ldk_node::config::{
4343
};
4444
use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy};
4545
use ldk_node::io::sqlite_store::SqliteStore;
46-
use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus, TransactionType};
46+
use ldk_node::lightning::chain::channelmonitor::ANTI_REORG_DELAY;
47+
use ldk_node::payment::{
48+
ConfirmationStatus, PaymentDirection, PaymentKind, PaymentStatus, TransactionType,
49+
};
4750
use ldk_node::probing::ProbingConfig;
4851
use ldk_node::{
4952
Builder, ChannelShutdownState, CustomTlvRecord, Event, LightningBalance, Node, NodeError,
@@ -409,6 +412,65 @@ pub(crate) type TestNode = Arc<Node>;
409412
#[cfg(not(feature = "uniffi"))]
410413
pub(crate) type TestNode = Node;
411414

415+
#[derive(Clone, Copy, Debug)]
416+
pub(crate) enum OnchainPaymentEvent {
417+
Successful,
418+
Received,
419+
}
420+
421+
pub(crate) trait ExpectOnchainPaymentEvent {
422+
async fn expect_onchain_payment_event(&self, expected_event: OnchainPaymentEvent) -> Txid;
423+
}
424+
425+
impl ExpectOnchainPaymentEvent for Node {
426+
async fn expect_onchain_payment_event(&self, expected_event: OnchainPaymentEvent) -> Txid {
427+
let event = tokio::time::timeout(
428+
Duration::from_secs(INTEROP_TIMEOUT_SECS),
429+
self.next_event_async(),
430+
)
431+
.await
432+
.unwrap_or_else(|_| {
433+
panic!("{} timed out waiting for {:?} event after 60s", self.node_id(), expected_event,)
434+
});
435+
let txid = match (&expected_event, &event) {
436+
(OnchainPaymentEvent::Successful, Event::OnchainPaymentSuccessful { txid, .. })
437+
| (OnchainPaymentEvent::Received, Event::OnchainPaymentReceived { txid, .. }) => *txid,
438+
_ => panic!("Expected {:?} event, got {:?}", expected_event, event),
439+
};
440+
println!("{} got event {:?}", self.node_id(), event);
441+
assert_onchain_payment_event_matches_payment(self, &event);
442+
self.event_handled().unwrap();
443+
txid
444+
}
445+
}
446+
447+
pub(crate) fn assert_onchain_payment_event_matches_payment(node: &Node, event: &Event) {
448+
let (payment_id, txid, amount_msat, expected_direction) = match event {
449+
Event::OnchainPaymentSuccessful { payment_id, txid, amount_msat, .. } => {
450+
(*payment_id, *txid, *amount_msat, PaymentDirection::Outbound)
451+
},
452+
Event::OnchainPaymentReceived { payment_id, txid, amount_msat, .. } => {
453+
(*payment_id, *txid, *amount_msat, PaymentDirection::Inbound)
454+
},
455+
_ => panic!("Expected on-chain payment event, got {:?}", event),
456+
};
457+
458+
let payment = node.payment(&payment_id).unwrap();
459+
assert_eq!(payment.status, PaymentStatus::Succeeded);
460+
assert_eq!(payment.direction, expected_direction);
461+
assert_eq!(payment.amount_msat, Some(amount_msat));
462+
match payment.kind {
463+
PaymentKind::Onchain {
464+
txid: payment_txid,
465+
status: ConfirmationStatus::Confirmed { .. },
466+
tx_type: None,
467+
} => {
468+
assert_eq!(payment_txid, txid);
469+
},
470+
ref other => panic!("Expected unclassified confirmed on-chain payment, got {:?}", other),
471+
}
472+
}
473+
412474
fn has_onchain_tx_type<F: Fn(&TransactionType) -> bool>(node: &TestNode, predicate: F) -> bool {
413475
node.list_payments().into_iter().any(|payment| {
414476
matches!(
@@ -880,11 +942,12 @@ where
880942

881943
pub(crate) async fn premine_and_distribute_funds<E: ElectrumApi>(
882944
bitcoind: &BitcoindClient, electrs: &E, addrs: Vec<Address>, amount: Amount,
883-
) {
945+
) -> Txid {
884946
premine_blocks(bitcoind, electrs).await;
885947

886-
distribute_funds_unconfirmed(bitcoind, electrs, addrs, amount).await;
948+
let txid = distribute_funds_unconfirmed(bitcoind, electrs, addrs, amount).await;
887949
generate_blocks_and_wait(bitcoind, electrs, 1).await;
950+
txid
888951
}
889952

890953
pub(crate) async fn premine_blocks<E: ElectrumApi>(bitcoind: &BitcoindClient, electrs: &E) {
@@ -1086,7 +1149,7 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
10861149

10871150
let premine_amount_sat = if expect_anchor_channel { 2_125_000 } else { 2_100_000 };
10881151

1089-
premine_and_distribute_funds(
1152+
let premine_txid = premine_and_distribute_funds(
10901153
&bitcoind,
10911154
electrsd,
10921155
vec![addr_a, addr_b],
@@ -1132,6 +1195,18 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
11321195
assert_eq!(node_a.next_event(), None);
11331196
assert_eq!(node_b.next_event(), None);
11341197

1198+
generate_blocks_and_wait(&bitcoind, electrsd, (ANTI_REORG_DELAY - 1) as usize).await;
1199+
node_a.sync_wallets().unwrap();
1200+
node_b.sync_wallets().unwrap();
1201+
assert_eq!(
1202+
node_a.expect_onchain_payment_event(OnchainPaymentEvent::Received).await,
1203+
premine_txid,
1204+
);
1205+
assert_eq!(
1206+
node_b.expect_onchain_payment_event(OnchainPaymentEvent::Received).await,
1207+
premine_txid,
1208+
);
1209+
11351210
println!("\nA -- open_channel -> B");
11361211
let funding_amount_sat = 2_080_000;
11371212
let push_msat = (funding_amount_sat / 2) * 1000; // balance the channel
@@ -1546,12 +1621,18 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
15461621
assert!(splice_out_sat > 500_000);
15471622
node_b.splice_out(&user_channel_id_b, node_a.node_id(), &addr_a, splice_out_sat).unwrap();
15481623

1549-
expect_splice_negotiated_event!(node_a, node_b.node_id());
1624+
let splice_out_txo = expect_splice_negotiated_event!(node_a, node_b.node_id());
15501625
expect_splice_negotiated_event!(node_b, node_a.node_id());
15511626

15521627
generate_blocks_and_wait(&bitcoind, electrsd, 6).await;
15531628
node_a.sync_wallets().unwrap();
15541629
node_b.sync_wallets().unwrap();
1630+
if !allow_0conf {
1631+
assert_eq!(
1632+
node_a.expect_onchain_payment_event(OnchainPaymentEvent::Received).await,
1633+
splice_out_txo.txid,
1634+
);
1635+
}
15551636

15561637
expect_channel_ready_event!(node_a, node_b.node_id());
15571638
expect_channel_ready_event!(node_b, node_a.node_id());

0 commit comments

Comments
 (0)