Skip to content

Commit cb799ae

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 cb799ae

11 files changed

Lines changed: 651 additions & 62 deletions

File tree

benches/payments.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ use bitcoin::Amount;
99
use common::{
1010
expect_channel_ready_event, generate_blocks_and_wait, premine_and_distribute_funds,
1111
random_chain_source, setup_bitcoind_and_electrsd, setup_two_nodes_with_store,
12+
ExpectOnchainPaymentEvent, OnchainPaymentEvent,
1213
};
1314
use criterion::{criterion_group, criterion_main, Criterion};
15+
use ldk_node::lightning::chain::channelmonitor::ANTI_REORG_DELAY;
1416
use ldk_node::{Event, Node};
1517
use lightning_types::payment::{PaymentHash, PaymentPreimage};
1618
use rand::RngCore;
@@ -142,7 +144,7 @@ fn payment_benchmark(c: &mut Criterion) {
142144
runtime.block_on(async move {
143145
let address_a = node_a_cloned.onchain_payment().new_address().unwrap();
144146
let premine_sat = 25_000_000;
145-
premine_and_distribute_funds(
147+
let premine_txid = premine_and_distribute_funds(
146148
&bitcoind.client,
147149
&electrsd.client,
148150
vec![address_a],
@@ -151,6 +153,18 @@ fn payment_benchmark(c: &mut Criterion) {
151153
.await;
152154
node_a_cloned.sync_wallets().unwrap();
153155
node_b_cloned.sync_wallets().unwrap();
156+
generate_blocks_and_wait(
157+
&bitcoind.client,
158+
&electrsd.client,
159+
(ANTI_REORG_DELAY - 1) as usize,
160+
)
161+
.await;
162+
node_a_cloned.sync_wallets().unwrap();
163+
node_b_cloned.sync_wallets().unwrap();
164+
assert_eq!(
165+
node_a_cloned.expect_onchain_payment_event(OnchainPaymentEvent::Received).await,
166+
premine_txid,
167+
);
154168
open_channel_push_amt(
155169
&node_a_cloned,
156170
&node_b_cloned,

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) {

0 commit comments

Comments
 (0)