Skip to content

Commit 048827d

Browse files
committed
feat: track in-flight amount for pending payments
Tracks actual in-flight amounts for pending payments to enable accurate balance calculations in wallets - Added payment_id to Balance::MaybeTimeoutClaimableHTLC so monitors can report which payment each HTLC belongs to - Added is_retryable to RecentPaymentDetails::Pending to distinguish active retries from stuck HTLCs - Added reconcile_inflight_payments() using ChannelMonitor as authoritative source for amounts to prevent double-counting - Added aggregate_outbound_htlcs_by_payment() to ChannelMonitor for aggregating HTLCs by payment This solves the race condition when fetching from ChannelMonitor and ChannelManager separately during retries
1 parent e9ce486 commit 048827d

File tree

8 files changed

+798
-32
lines changed

8 files changed

+798
-32
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// This file is Copyright its original authors, visible in version control
2+
// history.
3+
//
4+
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
5+
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
7+
// You may not use this file except in accordance with one or both of these
8+
// licenses.
9+
10+
use super::channelmonitor::Balance;
11+
use crate::ln::channelmanager::PaymentId;
12+
use crate::types::payment::PaymentHash;
13+
14+
#[test]
15+
fn test_aggregate_outbound_htlcs_by_payment() {
16+
// Empty balances
17+
let balances = vec![];
18+
let aggregated = Balance::aggregate_outbound_htlcs_by_payment(&balances);
19+
assert!(aggregated.is_empty());
20+
21+
// Single payment
22+
let payment_id_1 = PaymentId([1; 32]);
23+
let balances = vec![Balance::MaybeTimeoutClaimableHTLC {
24+
amount_satoshis: 10_000,
25+
claimable_height: 100,
26+
payment_hash: PaymentHash([0; 32]),
27+
payment_id: Some(payment_id_1),
28+
outbound_payment: true,
29+
}];
30+
let aggregated = Balance::aggregate_outbound_htlcs_by_payment(&balances);
31+
assert_eq!(aggregated.len(), 1);
32+
assert_eq!(*aggregated.get(&payment_id_1).unwrap(), 10_000);
33+
34+
// Test multiple HTLCs for same payment
35+
let balances = vec![
36+
Balance::MaybeTimeoutClaimableHTLC {
37+
amount_satoshis: 10_000,
38+
claimable_height: 100,
39+
payment_hash: PaymentHash([0; 32]),
40+
payment_id: Some(payment_id_1),
41+
outbound_payment: true,
42+
},
43+
Balance::MaybeTimeoutClaimableHTLC {
44+
amount_satoshis: 15_000,
45+
claimable_height: 100,
46+
payment_hash: PaymentHash([0; 32]),
47+
payment_id: Some(payment_id_1),
48+
outbound_payment: true,
49+
},
50+
Balance::MaybeTimeoutClaimableHTLC {
51+
amount_satoshis: 5_000,
52+
claimable_height: 100,
53+
payment_hash: PaymentHash([0; 32]),
54+
payment_id: Some(payment_id_1),
55+
outbound_payment: true,
56+
},
57+
];
58+
let aggregated = Balance::aggregate_outbound_htlcs_by_payment(&balances);
59+
assert_eq!(aggregated.len(), 1);
60+
assert_eq!(*aggregated.get(&payment_id_1).unwrap(), 30_000);
61+
62+
// Test multiple distinct payments
63+
let payment_id_2 = PaymentId([2; 32]);
64+
let balances = vec![
65+
Balance::MaybeTimeoutClaimableHTLC {
66+
amount_satoshis: 10_000,
67+
claimable_height: 100,
68+
payment_hash: PaymentHash([0; 32]),
69+
payment_id: Some(payment_id_1),
70+
outbound_payment: true,
71+
},
72+
Balance::MaybeTimeoutClaimableHTLC {
73+
amount_satoshis: 20_000,
74+
claimable_height: 100,
75+
payment_hash: PaymentHash([1; 32]),
76+
payment_id: Some(payment_id_2),
77+
outbound_payment: true,
78+
},
79+
];
80+
let aggregated = Balance::aggregate_outbound_htlcs_by_payment(&balances);
81+
assert_eq!(aggregated.len(), 2);
82+
assert_eq!(*aggregated.get(&payment_id_1).unwrap(), 10_000);
83+
assert_eq!(*aggregated.get(&payment_id_2).unwrap(), 20_000);
84+
85+
// Should ignore forwarded HTLCs
86+
let balances = vec![
87+
Balance::MaybeTimeoutClaimableHTLC {
88+
amount_satoshis: 10_000,
89+
claimable_height: 100,
90+
payment_hash: PaymentHash([0; 32]),
91+
payment_id: Some(payment_id_1),
92+
outbound_payment: true,
93+
},
94+
Balance::MaybeTimeoutClaimableHTLC {
95+
amount_satoshis: 50_000,
96+
claimable_height: 100,
97+
payment_hash: PaymentHash([1; 32]),
98+
payment_id: None,
99+
outbound_payment: false,
100+
},
101+
];
102+
let aggregated = Balance::aggregate_outbound_htlcs_by_payment(&balances);
103+
assert_eq!(aggregated.len(), 1);
104+
assert_eq!(*aggregated.get(&payment_id_1).unwrap(), 10_000);
105+
106+
// Should ignore outbound HTLCs without payment_id
107+
let balances = vec![
108+
Balance::MaybeTimeoutClaimableHTLC {
109+
amount_satoshis: 10_000,
110+
claimable_height: 100,
111+
payment_hash: PaymentHash([0; 32]),
112+
payment_id: Some(payment_id_1),
113+
outbound_payment: true,
114+
},
115+
Balance::MaybeTimeoutClaimableHTLC {
116+
amount_satoshis: 99_000,
117+
claimable_height: 100,
118+
payment_hash: PaymentHash([2; 32]),
119+
payment_id: None,
120+
outbound_payment: true,
121+
},
122+
];
123+
let aggregated = Balance::aggregate_outbound_htlcs_by_payment(&balances);
124+
assert_eq!(aggregated.len(), 1);
125+
assert_eq!(*aggregated.get(&payment_id_1).unwrap(), 10_000);
126+
127+
// Should ignore other balance types
128+
let balances = vec![
129+
Balance::MaybeTimeoutClaimableHTLC {
130+
amount_satoshis: 10_000,
131+
claimable_height: 100,
132+
payment_hash: PaymentHash([0; 32]),
133+
payment_id: Some(payment_id_1),
134+
outbound_payment: true,
135+
},
136+
Balance::MaybePreimageClaimableHTLC {
137+
amount_satoshis: 50_000,
138+
expiry_height: 100,
139+
payment_hash: PaymentHash([1; 32]),
140+
},
141+
Balance::ClaimableAwaitingConfirmations {
142+
amount_satoshis: 100_000,
143+
confirmation_height: 100,
144+
source: crate::chain::channelmonitor::BalanceSource::Htlc,
145+
},
146+
];
147+
let aggregated = Balance::aggregate_outbound_htlcs_by_payment(&balances);
148+
assert_eq!(aggregated.len(), 1);
149+
assert_eq!(*aggregated.get(&payment_id_1).unwrap(), 10_000);
150+
}

lightning/src/chain/channelmonitor.rs

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ use crate::ln::channel_keys::{
5555
DelayedPaymentBasepoint, DelayedPaymentKey, HtlcBasepoint, HtlcKey, RevocationBasepoint,
5656
RevocationKey,
5757
};
58-
use crate::ln::channelmanager::{HTLCSource, PaymentClaimDetails, SentHTLCId};
58+
use crate::ln::channelmanager::{HTLCSource, PaymentClaimDetails, PaymentId, SentHTLCId};
5959
use crate::ln::msgs::DecodeError;
6060
use crate::ln::types::ChannelId;
6161
use crate::sign::{
@@ -926,6 +926,9 @@ pub enum Balance {
926926
claimable_height: u32,
927927
/// The payment hash whose preimage our counterparty needs to claim this HTLC.
928928
payment_hash: PaymentHash,
929+
/// The payment ID for outbound HTLCs, enabling grouping of MPPs.
930+
/// `None` for forwarded HTLCs or if the channel monitor was created before this feature.
931+
payment_id: Option<PaymentId>,
929932
/// Whether this HTLC represents a payment which was sent outbound from us. Otherwise it
930933
/// represents an HTLC which was forwarded (and should, thus, have a corresponding inbound
931934
/// edge on another channel).
@@ -999,6 +1002,31 @@ impl Balance {
9991002
Balance::MaybePreimageClaimableHTLC { .. } => 0,
10001003
}
10011004
}
1005+
1006+
/// Aggregates outbound payment HTLCs from a list of balances by [`PaymentId`].
1007+
///
1008+
/// Returns a map of [`PaymentId`] to total satoshis locked across all channels. For balance
1009+
/// calculations, prefer [`reconcile_inflight_payments`] which provides an atomic view and
1010+
/// returns msats.
1011+
///
1012+
/// [`reconcile_inflight_payments`]: crate::ln::channelmanager::reconcile_inflight_payments
1013+
pub fn aggregate_outbound_htlcs_by_payment(balances: &[Balance]) -> HashMap<PaymentId, u64> {
1014+
let mut payment_amounts = HashMap::default();
1015+
1016+
for balance in balances {
1017+
if let Balance::MaybeTimeoutClaimableHTLC {
1018+
amount_satoshis,
1019+
payment_id: Some(payment_id),
1020+
outbound_payment: true,
1021+
..
1022+
} = balance
1023+
{
1024+
*payment_amounts.entry(*payment_id).or_insert(0) += amount_satoshis;
1025+
}
1026+
}
1027+
1028+
payment_amounts
1029+
}
10021030
}
10031031

10041032
/// An HTLC which has been irrevocably resolved on-chain, and has reached ANTI_REORG_DELAY.
@@ -2865,15 +2893,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> {
28652893
source: BalanceSource::Htlc,
28662894
});
28672895
} else {
2868-
let outbound_payment = match source {
2896+
let (outbound_payment, payment_id) = match source {
28692897
None => panic!("Outbound HTLCs should have a source"),
2870-
Some(&HTLCSource::PreviousHopData(_)) => false,
2871-
Some(&HTLCSource::OutboundRoute { .. }) => true,
2898+
Some(&HTLCSource::PreviousHopData(_)) => (false, None),
2899+
Some(&HTLCSource::OutboundRoute { payment_id, .. }) => (true, Some(payment_id)),
28722900
};
28732901
return Some(Balance::MaybeTimeoutClaimableHTLC {
28742902
amount_satoshis: htlc.amount_msat / 1000,
28752903
claimable_height: htlc.cltv_expiry,
28762904
payment_hash: htlc.payment_hash,
2905+
payment_id,
28772906
outbound_payment,
28782907
});
28792908
}
@@ -3077,10 +3106,10 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
30773106
htlc.amount_msat
30783107
} else { htlc.amount_msat % 1000 };
30793108
if htlc.offered {
3080-
let outbound_payment = match source {
3109+
let (outbound_payment, payment_id) = match source {
30813110
None => panic!("Outbound HTLCs should have a source"),
3082-
Some(HTLCSource::PreviousHopData(_)) => false,
3083-
Some(HTLCSource::OutboundRoute { .. }) => true,
3111+
Some(HTLCSource::PreviousHopData(_)) => (false, None),
3112+
Some(HTLCSource::OutboundRoute { payment_id, .. }) => (true, Some(*payment_id)),
30843113
};
30853114
if outbound_payment {
30863115
outbound_payment_htlc_rounded_msat += rounded_value_msat;
@@ -3092,6 +3121,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> {
30923121
amount_satoshis: htlc.amount_msat / 1000,
30933122
claimable_height: htlc.cltv_expiry,
30943123
payment_hash: htlc.payment_hash,
3124+
payment_id,
30953125
outbound_payment,
30963126
});
30973127
}

lightning/src/chain/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ pub(crate) mod onchaintx;
3535
pub(crate) mod package;
3636
pub mod transaction;
3737

38+
#[cfg(test)]
39+
mod balance_aggregation_tests;
40+
3841
/// The best known block as identified by its hash and height.
3942
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
4043
pub struct BestBlock {

0 commit comments

Comments
 (0)