Skip to content

Commit 3eb2b3b

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 3eb2b3b

2 files changed

Lines changed: 168 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: 125 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,94 @@ 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+
async fn settle_force_close_balance<E: ElectrumApi>(
440+
node: &TestNode, counterparty_node_id: PublicKey, peer_node: &TestNode,
441+
bitcoind: &BitcoindClient, electrsd: &E,
442+
) {
443+
let balances = node.list_balances();
444+
if balances.lightning_balances.len() == 1 {
445+
match balances.lightning_balances[0] {
446+
LightningBalance::ClaimableAwaitingConfirmations {
447+
counterparty_node_id: actual_counterparty_node_id,
448+
confirmation_height,
449+
..
450+
} => {
451+
assert_eq!(actual_counterparty_node_id, counterparty_node_id);
452+
let cur_height = node.status().current_best_block.height;
453+
let blocks_to_go = confirmation_height - cur_height;
454+
generate_blocks_and_wait(bitcoind, electrsd, blocks_to_go as usize).await;
455+
node.sync_wallets().unwrap();
456+
peer_node.sync_wallets().unwrap();
457+
},
458+
_ => panic!("Unexpected balance state!"),
459+
}
460+
} else {
461+
assert!(balances.lightning_balances.is_empty(), "Unexpected balance state: {:?}", balances);
462+
assert_eq!(balances.pending_balances_from_channel_closures.len(), 1);
463+
}
464+
465+
for _ in 0..6 {
466+
if node.list_balances().lightning_balances.is_empty() {
467+
break;
468+
}
469+
generate_blocks_and_wait(bitcoind, electrsd, 1).await;
470+
node.sync_wallets().unwrap();
471+
peer_node.sync_wallets().unwrap();
472+
}
473+
474+
let balances = node.list_balances();
475+
assert!(balances.lightning_balances.is_empty(), "Unexpected balance state: {:?}", balances);
476+
assert_eq!(balances.pending_balances_from_channel_closures.len(), 1);
477+
match balances.pending_balances_from_channel_closures[0] {
478+
PendingSweepBalance::BroadcastAwaitingConfirmation { .. } => {
479+
generate_blocks_and_wait(bitcoind, electrsd, 1).await;
480+
node.sync_wallets().unwrap();
481+
peer_node.sync_wallets().unwrap();
482+
483+
assert!(node.list_balances().lightning_balances.is_empty());
484+
assert_eq!(node.list_balances().pending_balances_from_channel_closures.len(), 1);
485+
match node.list_balances().pending_balances_from_channel_closures[0] {
486+
PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {},
487+
_ => panic!("Unexpected balance state!"),
488+
}
489+
},
490+
PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {},
491+
_ => panic!("Unexpected balance state!"),
492+
}
493+
494+
generate_blocks_and_wait(bitcoind, electrsd, 5).await;
495+
node.sync_wallets().unwrap();
496+
peer_node.sync_wallets().unwrap();
497+
}
498+
410499
#[derive(Clone)]
411500
pub(crate) enum TestChainSource<'a> {
412501
Esplora(&'a ElectrsD),
@@ -1474,84 +1563,11 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
14741563
node_b.sync_wallets().unwrap();
14751564

14761565
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-
1566+
settle_force_close_balance(&node_b, node_a.node_id(), &node_a, &bitcoind, electrsd).await;
14951567
assert!(node_b.list_balances().lightning_balances.is_empty());
14961568
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();
15041569

1505-
assert!(node_b.list_balances().lightning_balances.is_empty());
1506-
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();
1545-
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();
1570+
settle_force_close_balance(&node_a, node_b.node_id(), &node_b, &bitcoind, electrsd).await;
15551571
} else {
15561572
assert_eq!(node_a.list_balances().lightning_balances.len(), 1);
15571573
assert!(node_a.list_balances().pending_balances_from_channel_closures.is_empty());
@@ -1597,6 +1613,25 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
15971613
assert!(node_b.list_balances().pending_balances_from_channel_closures.is_empty());
15981614
}
15991615

1616+
if force_close {
1617+
assert_any_node_has_onchain_tx_type(
1618+
&[("node_a", &node_a), ("node_b", &node_b)],
1619+
"UnilateralClose",
1620+
|tx_type| matches!(tx_type, TransactionType::UnilateralClose { .. }),
1621+
);
1622+
assert_any_node_has_onchain_tx_type(
1623+
&[("node_a", &node_a), ("node_b", &node_b)],
1624+
"Sweep",
1625+
|tx_type| matches!(tx_type, TransactionType::Sweep { .. }),
1626+
);
1627+
} else {
1628+
assert_any_node_has_onchain_tx_type(
1629+
&[("node_a", &node_a), ("node_b", &node_b)],
1630+
"CooperativeClose",
1631+
|tx_type| matches!(tx_type, TransactionType::CooperativeClose { .. }),
1632+
);
1633+
}
1634+
16001635
let sum_of_all_payments_sat = (push_msat
16011636
+ invoice_amount_1_msat
16021637
+ overpaid_amount_msat
@@ -1619,20 +1654,20 @@ pub(crate) async fn do_channel_full_cycle<E: ElectrumApi>(
16191654
assert_eq!(node_b.list_balances().total_anchor_channels_reserve_sats, 0);
16201655

16211656
// 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-
);
1657+
let node_a_inbound_onchain_count = node_a
1658+
.list_payments_with_filter(|p| {
1659+
p.direction == PaymentDirection::Inbound
1660+
&& matches!(p.kind, PaymentKind::Onchain { .. })
1661+
})
1662+
.len();
1663+
let node_b_inbound_onchain_count = node_b
1664+
.list_payments_with_filter(|p| {
1665+
p.direction == PaymentDirection::Inbound
1666+
&& matches!(p.kind, PaymentKind::Onchain { .. })
1667+
})
1668+
.len();
1669+
assert!(node_a_inbound_onchain_count >= 3);
1670+
assert!(node_b_inbound_onchain_count >= 2);
16361671

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

0 commit comments

Comments
 (0)