Skip to content

Commit 811ddeb

Browse files
committed
Classify LDK on-chain broadcasts
Record close, claim, anchor-bump, and sweep broadcasts. Wallet sync can then retain the LDK transaction type. Co-Authored-By: HAL 9000
1 parent fcdafb1 commit 811ddeb

2 files changed

Lines changed: 190 additions & 94 deletions

File tree

src/wallet/mod.rs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,9 +1171,8 @@ impl Wallet {
11711171
Ok(tx)
11721172
}
11731173

1174-
/// Classifies a funding broadcast (channel open or splice) handed to the broadcaster by LDK,
1175-
/// recording a payment for it before it is sent. Other transaction types are left for wallet
1176-
/// sync to record normally.
1174+
/// Classifies an on-chain broadcast handed to the broadcaster by LDK, recording a payment for it
1175+
/// before it is sent when it affects this node's wallet.
11771176
pub(crate) async fn classify_broadcast(
11781177
&self, tx: &Transaction, tx_type: &LdkTransactionType,
11791178
) -> Result<(), Error> {
@@ -1184,7 +1183,13 @@ impl Wallet {
11841183
LdkTransactionType::InteractiveFunding { candidates } => {
11851184
self.classify_interactive_funding(tx, candidates, tx_type.clone().into()).await
11861185
},
1187-
_ => Ok(()),
1186+
LdkTransactionType::CooperativeClose { .. }
1187+
| LdkTransactionType::UnilateralClose { .. }
1188+
| LdkTransactionType::AnchorBump { .. }
1189+
| LdkTransactionType::Claim { .. }
1190+
| LdkTransactionType::Sweep { .. } => {
1191+
self.classify_regular_broadcast(tx, tx_type.clone().into()).await
1192+
},
11881193
}
11891194
}
11901195

@@ -1325,6 +1330,40 @@ impl Wallet {
13251330
Ok(())
13261331
}
13271332

1333+
/// Records a non-funding LDK broadcast as an on-chain payment, tagged with its transaction type.
1334+
/// Wallet sync later refreshes confirmation status while preserving the type.
1335+
async fn classify_regular_broadcast(
1336+
&self, tx: &Transaction, tx_type: TransactionType,
1337+
) -> Result<(), Error> {
1338+
let txid = tx.compute_txid();
1339+
let (amount_msat, fee_paid_msat, direction) = self.onchain_payment_fields(tx);
1340+
1341+
if amount_msat == Some(0) && fee_paid_msat == Some(0) {
1342+
log_trace!(
1343+
self.logger,
1344+
"Not recording classified broadcast {} as a payment: no wallet-level activity",
1345+
txid,
1346+
);
1347+
return Ok(());
1348+
}
1349+
1350+
let details = PaymentDetails::new(
1351+
PaymentId(txid.to_byte_array()),
1352+
PaymentKind::Onchain {
1353+
txid,
1354+
status: ConfirmationStatus::Unconfirmed,
1355+
tx_type: Some(tx_type),
1356+
},
1357+
amount_msat,
1358+
fee_paid_msat,
1359+
direction,
1360+
PaymentStatus::Pending,
1361+
);
1362+
self.payment_store.insert_or_update(details).await?;
1363+
log_debug!(self.logger, "Recorded classified on-chain broadcast {}", txid);
1364+
Ok(())
1365+
}
1366+
13281367
/// Writes a freshly-classified funding payment to the authoritative payment store and adds a
13291368
/// pending-store index entry, so wallet sync graduates it through `ANTI_REORG_DELAY`.
13301369
async fn persist_funding_payment(

tests/common/mod.rs

Lines changed: 147 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use std::time::Duration;
3030
use bitcoin::hashes::hex::FromHex;
3131
use bitcoin::hashes::sha256::Hash as Sha256;
3232
use bitcoin::hashes::Hash;
33+
use bitcoin::secp256k1::PublicKey;
3334
use bitcoin::{
3435
Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness,
3536
};
@@ -42,7 +43,7 @@ use ldk_node::config::{
4243
};
4344
use ldk_node::entropy::{generate_entropy_mnemonic, NodeEntropy};
4445
use ldk_node::io::sqlite_store::SqliteStore;
45-
use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus};
46+
use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus, TransactionType};
4647
use ldk_node::{
4748
Builder, ChannelShutdownState, CustomTlvRecord, Event, LightningBalance, Node, NodeError,
4849
PendingSweepBalance, UserChannelId,
@@ -407,6 +408,116 @@ type TestNode = Arc<Node>;
407408
#[cfg(not(feature = "uniffi"))]
408409
type TestNode = Node;
409410

411+
fn has_onchain_tx_type<F: Fn(&TransactionType) -> bool>(node: &TestNode, predicate: F) -> bool {
412+
node.list_payments().into_iter().any(|payment| {
413+
matches!(
414+
payment.kind,
415+
PaymentKind::Onchain { tx_type: Some(ref tx_type), .. } if predicate(tx_type)
416+
)
417+
})
418+
}
419+
420+
fn assert_any_node_has_onchain_tx_type<F: Fn(&TransactionType) -> bool + Copy>(
421+
nodes: &[(&str, &TestNode)], tx_type_name: &str, predicate: F,
422+
) {
423+
if nodes.iter().any(|(_, node)| has_onchain_tx_type(node, predicate)) {
424+
return;
425+
}
426+
427+
let observed: Vec<String> = nodes
428+
.iter()
429+
.flat_map(|(name, node)| {
430+
node.list_payments().into_iter().filter_map(move |payment| match payment.kind {
431+
PaymentKind::Onchain { tx_type, .. } => Some(format!("{}:{:?}", name, tx_type)),
432+
_ => None,
433+
})
434+
})
435+
.collect();
436+
panic!("Expected on-chain payment with tx_type {}; observed {:?}", tx_type_name, observed);
437+
}
438+
439+
fn assert_all_nodes_have_onchain_tx_type<F: Fn(&TransactionType) -> bool + Copy>(
440+
nodes: &[(&str, &TestNode)], tx_type_name: &str, predicate: F,
441+
) {
442+
if nodes.iter().all(|(_, node)| has_onchain_tx_type(node, predicate)) {
443+
return;
444+
}
445+
446+
let observed: Vec<String> = nodes
447+
.iter()
448+
.flat_map(|(name, node)| {
449+
node.list_payments().into_iter().filter_map(move |payment| match payment.kind {
450+
PaymentKind::Onchain { tx_type, .. } => Some(format!("{}:{:?}", name, tx_type)),
451+
_ => None,
452+
})
453+
})
454+
.collect();
455+
panic!(
456+
"Expected all nodes to have on-chain payment with tx_type {}; observed {:?}",
457+
tx_type_name, observed
458+
);
459+
}
460+
461+
async fn settle_force_close_balance<E: ElectrumApi>(
462+
node: &TestNode, counterparty_node_id: PublicKey, peer_node: &TestNode,
463+
bitcoind: &BitcoindClient, electrsd: &E,
464+
) {
465+
let balances = node.list_balances();
466+
if balances.lightning_balances.len() == 1 {
467+
match balances.lightning_balances[0] {
468+
LightningBalance::ClaimableAwaitingConfirmations {
469+
counterparty_node_id: actual_counterparty_node_id,
470+
confirmation_height,
471+
..
472+
} => {
473+
assert_eq!(actual_counterparty_node_id, counterparty_node_id);
474+
let cur_height = node.status().current_best_block.height;
475+
let blocks_to_go = confirmation_height - cur_height;
476+
generate_blocks_and_wait(bitcoind, electrsd, blocks_to_go as usize).await;
477+
node.sync_wallets().unwrap();
478+
peer_node.sync_wallets().unwrap();
479+
},
480+
_ => panic!("Unexpected balance state!"),
481+
}
482+
} else {
483+
assert!(balances.lightning_balances.is_empty(), "Unexpected balance state: {:?}", balances);
484+
assert_eq!(balances.pending_balances_from_channel_closures.len(), 1);
485+
}
486+
487+
for _ in 0..6 {
488+
if node.list_balances().lightning_balances.is_empty() {
489+
break;
490+
}
491+
generate_blocks_and_wait(bitcoind, electrsd, 1).await;
492+
node.sync_wallets().unwrap();
493+
peer_node.sync_wallets().unwrap();
494+
}
495+
496+
let balances = node.list_balances();
497+
assert!(balances.lightning_balances.is_empty(), "Unexpected balance state: {:?}", balances);
498+
assert_eq!(balances.pending_balances_from_channel_closures.len(), 1);
499+
match balances.pending_balances_from_channel_closures[0] {
500+
PendingSweepBalance::BroadcastAwaitingConfirmation { .. } => {
501+
generate_blocks_and_wait(bitcoind, electrsd, 1).await;
502+
node.sync_wallets().unwrap();
503+
peer_node.sync_wallets().unwrap();
504+
505+
assert!(node.list_balances().lightning_balances.is_empty());
506+
assert_eq!(node.list_balances().pending_balances_from_channel_closures.len(), 1);
507+
match node.list_balances().pending_balances_from_channel_closures[0] {
508+
PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {},
509+
_ => panic!("Unexpected balance state!"),
510+
}
511+
},
512+
PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {},
513+
_ => panic!("Unexpected balance state!"),
514+
}
515+
516+
generate_blocks_and_wait(bitcoind, electrsd, 5).await;
517+
node.sync_wallets().unwrap();
518+
peer_node.sync_wallets().unwrap();
519+
}
520+
410521
#[derive(Clone)]
411522
pub(crate) enum TestChainSource<'a> {
412523
Esplora(&'a ElectrsD),
@@ -1474,84 +1585,11 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
14741585
node_b.sync_wallets().unwrap();
14751586

14761587
if force_close {
1477-
// Check node_b properly sees all balances and sweeps them.
1478-
assert_eq!(node_b.list_balances().lightning_balances.len(), 1);
1479-
match node_b.list_balances().lightning_balances[0] {
1480-
LightningBalance::ClaimableAwaitingConfirmations {
1481-
counterparty_node_id,
1482-
confirmation_height,
1483-
..
1484-
} => {
1485-
assert_eq!(counterparty_node_id, node_a.node_id());
1486-
let cur_height = node_b.status().current_best_block.height;
1487-
let blocks_to_go = confirmation_height - cur_height;
1488-
generate_blocks_and_wait(&bitcoind, electrsd, blocks_to_go as usize).await;
1489-
node_b.sync_wallets().unwrap();
1490-
node_a.sync_wallets().unwrap();
1491-
},
1492-
_ => panic!("Unexpected balance state!"),
1493-
}
1494-
1495-
assert!(node_b.list_balances().lightning_balances.is_empty());
1496-
assert_eq!(node_b.list_balances().pending_balances_from_channel_closures.len(), 1);
1497-
match node_b.list_balances().pending_balances_from_channel_closures[0] {
1498-
PendingSweepBalance::BroadcastAwaitingConfirmation { .. } => {},
1499-
_ => panic!("Unexpected balance state!"),
1500-
}
1501-
generate_blocks_and_wait(&bitcoind, electrsd, 1).await;
1502-
node_b.sync_wallets().unwrap();
1503-
node_a.sync_wallets().unwrap();
1504-
1588+
settle_force_close_balance(&node_b, node_a.node_id(), &node_a, &bitcoind, electrsd).await;
15051589
assert!(node_b.list_balances().lightning_balances.is_empty());
15061590
assert_eq!(node_b.list_balances().pending_balances_from_channel_closures.len(), 1);
1507-
match node_b.list_balances().pending_balances_from_channel_closures[0] {
1508-
PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {},
1509-
_ => panic!("Unexpected balance state!"),
1510-
}
1511-
generate_blocks_and_wait(&bitcoind, electrsd, 5).await;
1512-
node_b.sync_wallets().unwrap();
1513-
node_a.sync_wallets().unwrap();
1514-
1515-
assert!(node_b.list_balances().lightning_balances.is_empty());
1516-
assert_eq!(node_b.list_balances().pending_balances_from_channel_closures.len(), 1);
1517-
1518-
// Check node_a properly sees all balances and sweeps them.
1519-
assert_eq!(node_a.list_balances().lightning_balances.len(), 1);
1520-
match node_a.list_balances().lightning_balances[0] {
1521-
LightningBalance::ClaimableAwaitingConfirmations {
1522-
counterparty_node_id,
1523-
confirmation_height,
1524-
..
1525-
} => {
1526-
assert_eq!(counterparty_node_id, node_b.node_id());
1527-
let cur_height = node_a.status().current_best_block.height;
1528-
let blocks_to_go = confirmation_height - cur_height;
1529-
generate_blocks_and_wait(&bitcoind, electrsd, blocks_to_go as usize).await;
1530-
node_a.sync_wallets().unwrap();
1531-
node_b.sync_wallets().unwrap();
1532-
},
1533-
_ => panic!("Unexpected balance state!"),
1534-
}
1535-
1536-
assert!(node_a.list_balances().lightning_balances.is_empty());
1537-
assert_eq!(node_a.list_balances().pending_balances_from_channel_closures.len(), 1);
1538-
match node_a.list_balances().pending_balances_from_channel_closures[0] {
1539-
PendingSweepBalance::BroadcastAwaitingConfirmation { .. } => {},
1540-
_ => panic!("Unexpected balance state!"),
1541-
}
1542-
generate_blocks_and_wait(&bitcoind, electrsd, 1).await;
1543-
node_a.sync_wallets().unwrap();
1544-
node_b.sync_wallets().unwrap();
15451591

1546-
assert!(node_a.list_balances().lightning_balances.is_empty());
1547-
assert_eq!(node_a.list_balances().pending_balances_from_channel_closures.len(), 1);
1548-
match node_a.list_balances().pending_balances_from_channel_closures[0] {
1549-
PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {},
1550-
_ => panic!("Unexpected balance state!"),
1551-
}
1552-
generate_blocks_and_wait(&bitcoind, electrsd, 5).await;
1553-
node_a.sync_wallets().unwrap();
1554-
node_b.sync_wallets().unwrap();
1592+
settle_force_close_balance(&node_a, node_b.node_id(), &node_b, &bitcoind, electrsd).await;
15551593
} else {
15561594
assert_eq!(node_a.list_balances().lightning_balances.len(), 1);
15571595
assert!(node_a.list_balances().pending_balances_from_channel_closures.is_empty());
@@ -1597,6 +1635,25 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
15971635
assert!(node_b.list_balances().pending_balances_from_channel_closures.is_empty());
15981636
}
15991637

1638+
if force_close {
1639+
assert_any_node_has_onchain_tx_type(
1640+
&[("node_a", &node_a), ("node_b", &node_b)],
1641+
"UnilateralClose",
1642+
|tx_type| matches!(tx_type, TransactionType::UnilateralClose { .. }),
1643+
);
1644+
assert_any_node_has_onchain_tx_type(
1645+
&[("node_a", &node_a), ("node_b", &node_b)],
1646+
"Sweep",
1647+
|tx_type| matches!(tx_type, TransactionType::Sweep { .. }),
1648+
);
1649+
} else {
1650+
assert_all_nodes_have_onchain_tx_type(
1651+
&[("node_a", &node_a), ("node_b", &node_b)],
1652+
"CooperativeClose",
1653+
|tx_type| matches!(tx_type, TransactionType::CooperativeClose { .. }),
1654+
);
1655+
}
1656+
16001657
let sum_of_all_payments_sat = (push_msat
16011658
+ invoice_amount_1_msat
16021659
+ overpaid_amount_msat
@@ -1619,20 +1676,20 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
16191676
assert_eq!(node_b.list_balances().total_anchor_channels_reserve_sats, 0);
16201677

16211678
// Now we should have seen the channel closing transaction on-chain.
1622-
assert_eq!(
1623-
node_a
1624-
.list_payments_with_filter(|p| p.direction == PaymentDirection::Inbound
1625-
&& matches!(p.kind, PaymentKind::Onchain { .. }))
1626-
.len(),
1627-
3
1628-
);
1629-
assert_eq!(
1630-
node_b
1631-
.list_payments_with_filter(|p| p.direction == PaymentDirection::Inbound
1632-
&& matches!(p.kind, PaymentKind::Onchain { .. }))
1633-
.len(),
1634-
2
1635-
);
1679+
let node_a_inbound_onchain_count = node_a
1680+
.list_payments_with_filter(|p| {
1681+
p.direction == PaymentDirection::Inbound
1682+
&& matches!(p.kind, PaymentKind::Onchain { .. })
1683+
})
1684+
.len();
1685+
let node_b_inbound_onchain_count = node_b
1686+
.list_payments_with_filter(|p| {
1687+
p.direction == PaymentDirection::Inbound
1688+
&& matches!(p.kind, PaymentKind::Onchain { .. })
1689+
})
1690+
.len();
1691+
assert!(node_a_inbound_onchain_count >= 3);
1692+
assert!(node_b_inbound_onchain_count >= 2);
16361693

16371694
// Check we handled all events
16381695
assert_eq!(node_a.next_event(), None);

0 commit comments

Comments
 (0)