Skip to content
This repository was archived by the owner on Jan 6, 2025. It is now read-only.

Commit 4bfaa9a

Browse files
Add MPP support to LSPS2 (#22)
* add support for mpp * fixes from review * take fees proportionally from each htlc * separate and test htlc proportional fee calculation * minor review cleanup * htlc fees using integer division * format
1 parent 95cbc27 commit 4bfaa9a

File tree

3 files changed

+185
-77
lines changed

3 files changed

+185
-77
lines changed

src/jit_channel/channel_manager.rs

+184-70
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ use crate::jit_channel::msgs::{
4545

4646
const SUPPORTED_SPEC_VERSIONS: [u16; 1] = [1];
4747

48+
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
49+
struct InterceptedHTLC {
50+
intercept_id: InterceptId,
51+
expected_outbound_amount_msat: u64,
52+
}
53+
4854
struct ChannelStateError(String);
4955

5056
impl From<ChannelStateError> for LightningError {
@@ -186,50 +192,86 @@ impl InboundJITChannel {
186192

187193
#[derive(PartialEq, Debug)]
188194
enum OutboundJITChannelState {
189-
InvoiceParametersGenerated {
190-
short_channel_id: u64,
191-
cltv_expiry_delta: u32,
195+
AwaitingPayment {
196+
min_fee_msat: u64,
197+
proportional_fee: u32,
198+
htlcs: Vec<InterceptedHTLC>,
192199
payment_size_msat: Option<u64>,
193-
opening_fee_params: OpeningFeeParams,
194200
},
195201
PendingChannelOpen {
196-
intercept_id: InterceptId,
202+
htlcs: Vec<InterceptedHTLC>,
197203
opening_fee_msat: u64,
198204
amt_to_forward_msat: u64,
199205
},
200206
ChannelReady {
201-
intercept_id: InterceptId,
207+
htlcs: Vec<InterceptedHTLC>,
202208
amt_to_forward_msat: u64,
203209
},
204210
}
205211

206212
impl OutboundJITChannelState {
207-
pub fn new(
208-
short_channel_id: u64, cltv_expiry_delta: u32, payment_size_msat: Option<u64>,
209-
opening_fee_params: OpeningFeeParams,
210-
) -> Self {
211-
OutboundJITChannelState::InvoiceParametersGenerated {
212-
short_channel_id,
213-
cltv_expiry_delta,
213+
pub fn new(payment_size_msat: Option<u64>, opening_fee_params: OpeningFeeParams) -> Self {
214+
OutboundJITChannelState::AwaitingPayment {
215+
min_fee_msat: opening_fee_params.min_fee_msat,
216+
proportional_fee: opening_fee_params.proportional,
217+
htlcs: vec![],
214218
payment_size_msat,
215-
opening_fee_params,
216219
}
217220
}
218221

219-
pub fn htlc_intercepted(
220-
&self, expected_outbound_amount_msat: u64, intercept_id: InterceptId,
221-
) -> Result<Self, ChannelStateError> {
222+
pub fn htlc_intercepted(&self, htlc: InterceptedHTLC) -> Result<Self, ChannelStateError> {
222223
match self {
223-
OutboundJITChannelState::InvoiceParametersGenerated { opening_fee_params, .. } => {
224-
compute_opening_fee(
225-
expected_outbound_amount_msat,
226-
opening_fee_params.min_fee_msat,
227-
opening_fee_params.proportional.into(),
228-
).map(|opening_fee_msat| OutboundJITChannelState::PendingChannelOpen {
229-
intercept_id,
230-
opening_fee_msat,
231-
amt_to_forward_msat: expected_outbound_amount_msat - opening_fee_msat,
232-
}).ok_or(ChannelStateError(format!("Could not compute valid opening fee with min_fee_msat = {}, proportional = {}, and expected_outbound_amount_msat = {}", opening_fee_params.min_fee_msat, opening_fee_params.proportional, expected_outbound_amount_msat)))
224+
OutboundJITChannelState::AwaitingPayment {
225+
htlcs,
226+
payment_size_msat,
227+
min_fee_msat,
228+
proportional_fee,
229+
} => {
230+
let mut htlcs = htlcs.clone();
231+
htlcs.push(htlc);
232+
233+
let total_expected_outbound_amount_msat =
234+
htlcs.iter().map(|htlc| htlc.expected_outbound_amount_msat).sum();
235+
236+
let expected_payment_size_msat =
237+
payment_size_msat.unwrap_or(total_expected_outbound_amount_msat);
238+
239+
let opening_fee_msat = compute_opening_fee(
240+
expected_payment_size_msat,
241+
*min_fee_msat,
242+
(*proportional_fee).into(),
243+
).ok_or(ChannelStateError(
244+
format!("Could not compute valid opening fee with min_fee_msat = {}, proportional = {}, and total_expected_outbound_amount_msat = {}",
245+
min_fee_msat,
246+
proportional_fee,
247+
total_expected_outbound_amount_msat
248+
)
249+
))?;
250+
251+
let amt_to_forward_msat =
252+
expected_payment_size_msat.saturating_sub(opening_fee_msat);
253+
254+
if total_expected_outbound_amount_msat >= expected_payment_size_msat
255+
&& amt_to_forward_msat > 0
256+
{
257+
Ok(OutboundJITChannelState::PendingChannelOpen {
258+
htlcs,
259+
opening_fee_msat,
260+
amt_to_forward_msat,
261+
})
262+
} else {
263+
// payment size being specified means MPP is supported
264+
if payment_size_msat.is_some() {
265+
Ok(OutboundJITChannelState::AwaitingPayment {
266+
min_fee_msat: *min_fee_msat,
267+
proportional_fee: *proportional_fee,
268+
htlcs,
269+
payment_size_msat: *payment_size_msat,
270+
})
271+
} else {
272+
Err(ChannelStateError("HTLC is too small to pay opening fee".to_string()))
273+
}
274+
}
233275
}
234276
state => Err(ChannelStateError(format!(
235277
"Invoice params received when JIT Channel was in state: {:?}",
@@ -240,14 +282,12 @@ impl OutboundJITChannelState {
240282

241283
pub fn channel_ready(&self) -> Result<Self, ChannelStateError> {
242284
match self {
243-
OutboundJITChannelState::PendingChannelOpen {
244-
intercept_id,
245-
amt_to_forward_msat,
246-
..
247-
} => Ok(OutboundJITChannelState::ChannelReady {
248-
intercept_id: *intercept_id,
249-
amt_to_forward_msat: *amt_to_forward_msat,
250-
}),
285+
OutboundJITChannelState::PendingChannelOpen { htlcs, amt_to_forward_msat, .. } => {
286+
Ok(OutboundJITChannelState::ChannelReady {
287+
htlcs: htlcs.clone(),
288+
amt_to_forward_msat: *amt_to_forward_msat,
289+
})
290+
}
251291
state => Err(ChannelStateError(format!(
252292
"Channel ready received when JIT Channel was in state: {:?}",
253293
state
@@ -258,34 +298,39 @@ impl OutboundJITChannelState {
258298

259299
struct OutboundJITChannel {
260300
state: OutboundJITChannelState,
301+
scid: u64,
302+
cltv_expiry_delta: u32,
303+
client_trusts_lsp: bool,
261304
}
262305

263306
impl OutboundJITChannel {
264307
pub fn new(
265-
scid: u64, cltv_expiry_delta: u32, payment_size_msat: Option<u64>,
308+
scid: u64, cltv_expiry_delta: u32, client_trusts_lsp: bool, payment_size_msat: Option<u64>,
266309
opening_fee_params: OpeningFeeParams,
267310
) -> Self {
268311
Self {
269-
state: OutboundJITChannelState::new(
270-
scid,
271-
cltv_expiry_delta,
272-
payment_size_msat,
273-
opening_fee_params,
274-
),
312+
scid,
313+
cltv_expiry_delta,
314+
client_trusts_lsp,
315+
state: OutboundJITChannelState::new(payment_size_msat, opening_fee_params),
275316
}
276317
}
277318

278319
pub fn htlc_intercepted(
279-
&mut self, expected_outbound_amount_msat: u64, intercept_id: InterceptId,
280-
) -> Result<(u64, u64), LightningError> {
281-
self.state = self.state.htlc_intercepted(expected_outbound_amount_msat, intercept_id)?;
320+
&mut self, htlc: InterceptedHTLC,
321+
) -> Result<Option<(u64, u64)>, LightningError> {
322+
self.state = self.state.htlc_intercepted(htlc)?;
282323

283324
match &self.state {
325+
OutboundJITChannelState::AwaitingPayment { htlcs, payment_size_msat, .. } => {
326+
// TODO: log that we received an htlc but are still awaiting payment
327+
Ok(None)
328+
}
284329
OutboundJITChannelState::PendingChannelOpen {
285330
opening_fee_msat,
286331
amt_to_forward_msat,
287332
..
288-
} => Ok((*opening_fee_msat, *amt_to_forward_msat)),
333+
} => Ok(Some((*opening_fee_msat, *amt_to_forward_msat))),
289334
impossible_state => Err(LightningError {
290335
err: format!(
291336
"Impossible state transition during htlc_intercepted to {:?}",
@@ -296,12 +341,12 @@ impl OutboundJITChannel {
296341
}
297342
}
298343

299-
pub fn channel_ready(&mut self) -> Result<(InterceptId, u64), LightningError> {
344+
pub fn channel_ready(&mut self) -> Result<(Vec<InterceptedHTLC>, u64), LightningError> {
300345
self.state = self.state.channel_ready()?;
301346

302347
match &self.state {
303-
OutboundJITChannelState::ChannelReady { intercept_id, amt_to_forward_msat } => {
304-
Ok((*intercept_id, *amt_to_forward_msat))
348+
OutboundJITChannelState::ChannelReady { htlcs, amt_to_forward_msat } => {
349+
Ok((htlcs.clone(), *amt_to_forward_msat))
305350
}
306351
impossible_state => Err(LightningError {
307352
err: format!(
@@ -585,6 +630,7 @@ where
585630
let outbound_jit_channel = OutboundJITChannel::new(
586631
scid,
587632
cltv_expiry_delta,
633+
client_trusts_lsp,
588634
buy_request.payment_size_msat,
589635
buy_request.opening_fee_params,
590636
);
@@ -615,8 +661,7 @@ where
615661
}
616662

617663
pub(crate) fn htlc_intercepted(
618-
&self, scid: u64, intercept_id: InterceptId, inbound_amount_msat: u64,
619-
expected_outbound_amount_msat: u64,
664+
&self, scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64,
620665
) -> Result<(), APIError> {
621666
let peer_by_scid = self.peer_by_scid.read().unwrap();
622667
if let Some(counterparty_node_id) = peer_by_scid.get(&scid) {
@@ -625,25 +670,17 @@ where
625670
Some(inner_state_lock) => {
626671
let mut peer_state = inner_state_lock.lock().unwrap();
627672
if let Some(jit_channel) = peer_state.outbound_channels_by_scid.get_mut(&scid) {
628-
// TODO: Need to support MPP payments. If payment_amount_msat is known, needs to queue intercepted HTLCs in a map by payment_hash
629-
// LiquidityManager will need to be regularly polled so it can continually check if the payment amount has been received
630-
// and can release the payment or if the channel valid_until has expired and should be failed.
631-
// Can perform check each time HTLC is received and on interval? I guess interval only needs to check expiration as
632-
// we can only reach threshold when htlc is intercepted.
633-
634-
match jit_channel
635-
.htlc_intercepted(expected_outbound_amount_msat, intercept_id)
636-
{
637-
Ok((opening_fee_msat, amt_to_forward_msat)) => {
673+
let htlc = InterceptedHTLC { intercept_id, expected_outbound_amount_msat };
674+
match jit_channel.htlc_intercepted(htlc) {
675+
Ok(Some((opening_fee_msat, amt_to_forward_msat))) => {
638676
self.enqueue_event(Event::LSPS2(LSPS2Event::OpenChannel {
639677
their_network_key: counterparty_node_id.clone(),
640-
inbound_amount_msat,
641-
expected_outbound_amount_msat,
642678
amt_to_forward_msat,
643679
opening_fee_msat,
644680
user_channel_id: scid as u128,
645681
}));
646682
}
683+
Ok(None) => {}
647684
Err(e) => {
648685
self.channel_manager.fail_intercepted_htlc(intercept_id)?;
649686
peer_state.outbound_channels_by_scid.remove(&scid);
@@ -675,13 +712,22 @@ where
675712
let mut peer_state = inner_state_lock.lock().unwrap();
676713
if let Some(jit_channel) = peer_state.outbound_channels_by_scid.get_mut(&scid) {
677714
match jit_channel.channel_ready() {
678-
Ok((intercept_id, amt_to_forward_msat)) => {
679-
self.channel_manager.forward_intercepted_htlc(
680-
intercept_id,
681-
channel_id,
682-
*counterparty_node_id,
683-
amt_to_forward_msat,
684-
)?;
715+
Ok((htlcs, total_amt_to_forward_msat)) => {
716+
let amounts_to_forward_msat = calculate_amount_to_forward_per_htlc(
717+
&htlcs,
718+
total_amt_to_forward_msat,
719+
);
720+
721+
for (intercept_id, amount_to_forward_msat) in
722+
amounts_to_forward_msat
723+
{
724+
self.channel_manager.forward_intercepted_htlc(
725+
intercept_id,
726+
channel_id,
727+
*counterparty_node_id,
728+
amount_to_forward_msat,
729+
)?;
730+
}
685731
}
686732
Err(e) => {
687733
return Err(APIError::APIMisuseError {
@@ -1237,3 +1283,71 @@ where
12371283
}
12381284
}
12391285
}
1286+
1287+
fn calculate_amount_to_forward_per_htlc(
1288+
htlcs: &[InterceptedHTLC], total_amt_to_forward_msat: u64,
1289+
) -> Vec<(InterceptId, u64)> {
1290+
let total_received_msat: u64 =
1291+
htlcs.iter().map(|htlc| htlc.expected_outbound_amount_msat).sum();
1292+
1293+
let mut fee_remaining_msat = total_received_msat - total_amt_to_forward_msat;
1294+
let total_fee_msat = fee_remaining_msat;
1295+
1296+
let mut per_htlc_forwards = vec![];
1297+
1298+
for (index, htlc) in htlcs.iter().enumerate() {
1299+
let proportional_fee_amt_msat =
1300+
total_fee_msat * htlc.expected_outbound_amount_msat / total_received_msat;
1301+
1302+
let mut actual_fee_amt_msat = std::cmp::min(fee_remaining_msat, proportional_fee_amt_msat);
1303+
fee_remaining_msat -= actual_fee_amt_msat;
1304+
1305+
if index == htlcs.len() - 1 {
1306+
actual_fee_amt_msat += fee_remaining_msat;
1307+
}
1308+
1309+
let amount_to_forward_msat = htlc.expected_outbound_amount_msat - actual_fee_amt_msat;
1310+
1311+
per_htlc_forwards.push((htlc.intercept_id, amount_to_forward_msat))
1312+
}
1313+
1314+
per_htlc_forwards
1315+
}
1316+
1317+
#[cfg(test)]
1318+
mod tests {
1319+
1320+
use super::*;
1321+
1322+
#[test]
1323+
fn test_calculate_amount_to_forward() {
1324+
// TODO: Use proptest to generate random allocations
1325+
let htlcs = vec![
1326+
InterceptedHTLC {
1327+
intercept_id: InterceptId([0; 32]),
1328+
expected_outbound_amount_msat: 1000,
1329+
},
1330+
InterceptedHTLC {
1331+
intercept_id: InterceptId([1; 32]),
1332+
expected_outbound_amount_msat: 2000,
1333+
},
1334+
InterceptedHTLC {
1335+
intercept_id: InterceptId([2; 32]),
1336+
expected_outbound_amount_msat: 3000,
1337+
},
1338+
];
1339+
1340+
let total_amt_to_forward_msat = 5000;
1341+
1342+
let result = calculate_amount_to_forward_per_htlc(&htlcs, total_amt_to_forward_msat);
1343+
1344+
assert_eq!(result[0].0, htlcs[0].intercept_id);
1345+
assert_eq!(result[0].1, 834);
1346+
1347+
assert_eq!(result[1].0, htlcs[1].intercept_id);
1348+
assert_eq!(result[1].1, 1667);
1349+
1350+
assert_eq!(result[2].0, htlcs[2].intercept_id);
1351+
assert_eq!(result[2].1, 2499);
1352+
}
1353+
}

src/jit_channel/event.rs

-4
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,6 @@ pub enum LSPS2Event {
110110
OpenChannel {
111111
/// The node to open channel with.
112112
their_network_key: PublicKey,
113-
/// The intercepted HTLC amount in msats.
114-
inbound_amount_msat: u64,
115-
/// The amount the client expects to receive before fees are taken out.
116-
expected_outbound_amount_msat: u64,
117113
/// The amount to forward after fees.
118114
amt_to_forward_msat: u64,
119115
/// The fee earned for opening the channel.

src/transport/message_handler.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -447,14 +447,12 @@ where {
447447
/// [`Event::HTLCIntercepted`]: lightning::events::Event::HTLCIntercepted
448448
/// [`LSPS2Event::OpenChannel`]: crate::jit_channel::LSPS2Event::OpenChannel
449449
pub fn htlc_intercepted(
450-
&self, scid: u64, intercept_id: InterceptId, inbound_amount_msat: u64,
451-
expected_outbound_amount_msat: u64,
450+
&self, scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64,
452451
) -> Result<(), APIError> {
453452
if let Some(lsps2_message_handler) = &self.lsps2_message_handler {
454453
lsps2_message_handler.htlc_intercepted(
455454
scid,
456455
intercept_id,
457-
inbound_amount_msat,
458456
expected_outbound_amount_msat,
459457
)?;
460458
}

0 commit comments

Comments
 (0)