Skip to content

Commit 3751398

Browse files
Merge pull request #2858 from tnull/2024-01-expose-skimmed-fee-msat
Expose withheld amount for underpaying HTLCs in `PaymentForwarded`
2 parents cc43f9c + 9644588 commit 3751398

7 files changed

+138
-57
lines changed

lightning/src/events/mod.rs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -783,7 +783,7 @@ pub enum Event {
783783
/// The outgoing channel between the next node and us. This is only `None` for events
784784
/// generated or serialized by versions prior to 0.0.107.
785785
next_channel_id: Option<ChannelId>,
786-
/// The fee, in milli-satoshis, which was earned as a result of the payment.
786+
/// The total fee, in milli-satoshis, which was earned as a result of the payment.
787787
///
788788
/// Note that if we force-closed the channel over which we forwarded an HTLC while the HTLC
789789
/// was pending, the amount the next hop claimed will have been rounded down to the nearest
@@ -794,15 +794,29 @@ pub enum Event {
794794
/// If the channel which sent us the payment has been force-closed, we will claim the funds
795795
/// via an on-chain transaction. In that case we do not yet know the on-chain transaction
796796
/// fees which we will spend and will instead set this to `None`. It is possible duplicate
797-
/// `PaymentForwarded` events are generated for the same payment iff `fee_earned_msat` is
797+
/// `PaymentForwarded` events are generated for the same payment iff `total_fee_earned_msat` is
798798
/// `None`.
799-
fee_earned_msat: Option<u64>,
799+
total_fee_earned_msat: Option<u64>,
800+
/// The share of the total fee, in milli-satoshis, which was withheld in addition to the
801+
/// forwarding fee.
802+
///
803+
/// This will only be `Some` if we forwarded an intercepted HTLC with less than the
804+
/// expected amount. This means our counterparty accepted to receive less than the invoice
805+
/// amount, e.g., by claiming the payment featuring a corresponding
806+
/// [`PaymentClaimable::counterparty_skimmed_fee_msat`].
807+
///
808+
/// Will also always be `None` for events serialized with LDK prior to version 0.0.122.
809+
///
810+
/// The caveat described above the `total_fee_earned_msat` field applies here as well.
811+
///
812+
/// [`PaymentClaimable::counterparty_skimmed_fee_msat`]: Self::PaymentClaimable::counterparty_skimmed_fee_msat
813+
skimmed_fee_msat: Option<u64>,
800814
/// If this is `true`, the forwarded HTLC was claimed by our counterparty via an on-chain
801815
/// transaction.
802816
claim_from_onchain_tx: bool,
803817
/// The final amount forwarded, in milli-satoshis, after the fee is deducted.
804818
///
805-
/// The caveat described above the `fee_earned_msat` field applies here as well.
819+
/// The caveat described above the `total_fee_earned_msat` field applies here as well.
806820
outbound_amount_forwarded_msat: Option<u64>,
807821
},
808822
/// Used to indicate that a channel with the given `channel_id` is being opened and pending
@@ -1083,16 +1097,17 @@ impl Writeable for Event {
10831097
});
10841098
}
10851099
&Event::PaymentForwarded {
1086-
fee_earned_msat, prev_channel_id, claim_from_onchain_tx,
1087-
next_channel_id, outbound_amount_forwarded_msat
1100+
total_fee_earned_msat, prev_channel_id, claim_from_onchain_tx,
1101+
next_channel_id, outbound_amount_forwarded_msat, skimmed_fee_msat,
10881102
} => {
10891103
7u8.write(writer)?;
10901104
write_tlv_fields!(writer, {
1091-
(0, fee_earned_msat, option),
1105+
(0, total_fee_earned_msat, option),
10921106
(1, prev_channel_id, option),
10931107
(2, claim_from_onchain_tx, required),
10941108
(3, next_channel_id, option),
10951109
(5, outbound_amount_forwarded_msat, option),
1110+
(7, skimmed_fee_msat, option),
10961111
});
10971112
},
10981113
&Event::ChannelClosed { ref channel_id, ref user_channel_id, ref reason,
@@ -1384,21 +1399,23 @@ impl MaybeReadable for Event {
13841399
},
13851400
7u8 => {
13861401
let f = || {
1387-
let mut fee_earned_msat = None;
1402+
let mut total_fee_earned_msat = None;
13881403
let mut prev_channel_id = None;
13891404
let mut claim_from_onchain_tx = false;
13901405
let mut next_channel_id = None;
13911406
let mut outbound_amount_forwarded_msat = None;
1407+
let mut skimmed_fee_msat = None;
13921408
read_tlv_fields!(reader, {
1393-
(0, fee_earned_msat, option),
1409+
(0, total_fee_earned_msat, option),
13941410
(1, prev_channel_id, option),
13951411
(2, claim_from_onchain_tx, required),
13961412
(3, next_channel_id, option),
13971413
(5, outbound_amount_forwarded_msat, option),
1414+
(7, skimmed_fee_msat, option),
13981415
});
13991416
Ok(Some(Event::PaymentForwarded {
1400-
fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id,
1401-
outbound_amount_forwarded_msat
1417+
total_fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id,
1418+
outbound_amount_forwarded_msat, skimmed_fee_msat,
14021419
}))
14031420
};
14041421
f()

lightning/src/ln/chanmon_update_fail_tests.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3404,7 +3404,8 @@ fn do_test_reload_mon_update_completion_actions(close_during_reload: bool) {
34043404
let bc_update_id = nodes[1].chain_monitor.latest_monitor_update_id.lock().unwrap().get(&chan_id_bc).unwrap().2;
34053405
let mut events = nodes[1].node.get_and_clear_pending_events();
34063406
assert_eq!(events.len(), if close_during_reload { 2 } else { 1 });
3407-
expect_payment_forwarded(events.pop().unwrap(), &nodes[1], &nodes[0], &nodes[2], Some(1000), close_during_reload, false);
3407+
expect_payment_forwarded(events.pop().unwrap(), &nodes[1], &nodes[0], &nodes[2], Some(1000),
3408+
None, close_during_reload, false);
34083409
if close_during_reload {
34093410
match events[0] {
34103411
Event::ChannelClosed { .. } => {},

lightning/src/ln/channel.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3327,15 +3327,15 @@ impl<SP: Deref> Channel<SP> where
33273327
Err(ChannelError::Close("Remote tried to fulfill/fail an HTLC we couldn't find".to_owned()))
33283328
}
33293329

3330-
pub fn update_fulfill_htlc(&mut self, msg: &msgs::UpdateFulfillHTLC) -> Result<(HTLCSource, u64), ChannelError> {
3330+
pub fn update_fulfill_htlc(&mut self, msg: &msgs::UpdateFulfillHTLC) -> Result<(HTLCSource, u64, Option<u64>), ChannelError> {
33313331
if !matches!(self.context.channel_state, ChannelState::ChannelReady(_)) {
33323332
return Err(ChannelError::Close("Got fulfill HTLC message when channel was not in an operational state".to_owned()));
33333333
}
33343334
if self.context.channel_state.is_peer_disconnected() {
33353335
return Err(ChannelError::Close("Peer sent update_fulfill_htlc when we needed a channel_reestablish".to_owned()));
33363336
}
33373337

3338-
self.mark_outbound_htlc_removed(msg.htlc_id, Some(msg.payment_preimage), None).map(|htlc| (htlc.source.clone(), htlc.amount_msat))
3338+
self.mark_outbound_htlc_removed(msg.htlc_id, Some(msg.payment_preimage), None).map(|htlc| (htlc.source.clone(), htlc.amount_msat, htlc.skimmed_fee_msat))
33393339
}
33403340

33413341
pub fn update_fail_htlc(&mut self, msg: &msgs::UpdateFailHTLC, fail_reason: HTLCFailReason) -> Result<(), ChannelError> {

lightning/src/ln/channelmanager.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5687,9 +5687,9 @@ where
56875687
}
56885688

56895689
fn claim_funds_internal(&self, source: HTLCSource, payment_preimage: PaymentPreimage,
5690-
forwarded_htlc_value_msat: Option<u64>, from_onchain: bool, startup_replay: bool,
5691-
next_channel_counterparty_node_id: Option<PublicKey>, next_channel_outpoint: OutPoint,
5692-
next_channel_id: ChannelId,
5690+
forwarded_htlc_value_msat: Option<u64>, skimmed_fee_msat: Option<u64>, from_onchain: bool,
5691+
startup_replay: bool, next_channel_counterparty_node_id: Option<PublicKey>,
5692+
next_channel_outpoint: OutPoint, next_channel_id: ChannelId,
56935693
) {
56945694
match source {
56955695
HTLCSource::OutboundRoute { session_priv, payment_id, path, .. } => {
@@ -5785,18 +5785,21 @@ where
57855785
})
57865786
} else { None }
57875787
} else {
5788-
let fee_earned_msat = if let Some(forwarded_htlc_value) = forwarded_htlc_value_msat {
5788+
let total_fee_earned_msat = if let Some(forwarded_htlc_value) = forwarded_htlc_value_msat {
57895789
if let Some(claimed_htlc_value) = htlc_claim_value_msat {
57905790
Some(claimed_htlc_value - forwarded_htlc_value)
57915791
} else { None }
57925792
} else { None };
5793+
debug_assert!(skimmed_fee_msat <= total_fee_earned_msat,
5794+
"skimmed_fee_msat must always be included in total_fee_earned_msat");
57935795
Some(MonitorUpdateCompletionAction::EmitEventAndFreeOtherChannel {
57945796
event: events::Event::PaymentForwarded {
5795-
fee_earned_msat,
5797+
total_fee_earned_msat,
57965798
claim_from_onchain_tx: from_onchain,
57975799
prev_channel_id: Some(prev_channel_id),
57985800
next_channel_id: Some(next_channel_id),
57995801
outbound_amount_forwarded_msat: forwarded_htlc_value_msat,
5802+
skimmed_fee_msat,
58005803
},
58015804
downstream_counterparty_and_funding_outpoint: chan_to_release,
58025805
})
@@ -6736,7 +6739,7 @@ where
67366739

67376740
fn internal_update_fulfill_htlc(&self, counterparty_node_id: &PublicKey, msg: &msgs::UpdateFulfillHTLC) -> Result<(), MsgHandleErrInternal> {
67386741
let funding_txo;
6739-
let (htlc_source, forwarded_htlc_value) = {
6742+
let (htlc_source, forwarded_htlc_value, skimmed_fee_msat) = {
67406743
let per_peer_state = self.per_peer_state.read().unwrap();
67416744
let peer_state_mutex = per_peer_state.get(counterparty_node_id)
67426745
.ok_or_else(|| {
@@ -6774,8 +6777,11 @@ where
67746777
hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id))
67756778
}
67766779
};
6777-
self.claim_funds_internal(htlc_source, msg.payment_preimage.clone(), Some(forwarded_htlc_value),
6778-
false, false, Some(*counterparty_node_id), funding_txo, msg.channel_id);
6780+
self.claim_funds_internal(htlc_source, msg.payment_preimage.clone(),
6781+
Some(forwarded_htlc_value), skimmed_fee_msat, false, false, Some(*counterparty_node_id),
6782+
funding_txo, msg.channel_id
6783+
);
6784+
67796785
Ok(())
67806786
}
67816787

@@ -7273,7 +7279,9 @@ where
72737279
let logger = WithContext::from(&self.logger, counterparty_node_id, Some(channel_id));
72747280
if let Some(preimage) = htlc_update.payment_preimage {
72757281
log_trace!(logger, "Claiming HTLC with preimage {} from our monitor", preimage);
7276-
self.claim_funds_internal(htlc_update.source, preimage, htlc_update.htlc_value_satoshis.map(|v| v * 1000), true, false, counterparty_node_id, funding_outpoint, channel_id);
7282+
self.claim_funds_internal(htlc_update.source, preimage,
7283+
htlc_update.htlc_value_satoshis.map(|v| v * 1000), None, true,
7284+
false, counterparty_node_id, funding_outpoint, channel_id);
72777285
} else {
72787286
log_trace!(logger, "Failing HTLC with hash {} from our monitor", &htlc_update.payment_hash);
72797287
let receiver = HTLCDestination::NextHopChannel { node_id: counterparty_node_id, channel_id };
@@ -11164,7 +11172,7 @@ where
1116411172
// We use `downstream_closed` in place of `from_onchain` here just as a guess - we
1116511173
// don't remember in the `ChannelMonitor` where we got a preimage from, but if the
1116611174
// channel is closed we just assume that it probably came from an on-chain claim.
11167-
channel_manager.claim_funds_internal(source, preimage, Some(downstream_value),
11175+
channel_manager.claim_funds_internal(source, preimage, Some(downstream_value), None,
1116811176
downstream_closed, true, downstream_node_id, downstream_funding, downstream_channel_id);
1116911177
}
1117011178

lightning/src/ln/functional_test_utils.rs

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2203,14 +2203,19 @@ macro_rules! expect_payment_path_successful {
22032203

22042204
pub fn expect_payment_forwarded<CM: AChannelManager, H: NodeHolder<CM=CM>>(
22052205
event: Event, node: &H, prev_node: &H, next_node: &H, expected_fee: Option<u64>,
2206-
upstream_force_closed: bool, downstream_force_closed: bool
2206+
expected_extra_fees_msat: Option<u64>, upstream_force_closed: bool,
2207+
downstream_force_closed: bool
22072208
) {
22082209
match event {
22092210
Event::PaymentForwarded {
2210-
fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id,
2211-
outbound_amount_forwarded_msat: _
2211+
total_fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id,
2212+
outbound_amount_forwarded_msat: _, skimmed_fee_msat
22122213
} => {
2213-
assert_eq!(fee_earned_msat, expected_fee);
2214+
assert_eq!(total_fee_earned_msat, expected_fee);
2215+
2216+
// Check that the (knowingly) withheld amount is always less or equal to the expected
2217+
// overpaid amount.
2218+
assert!(skimmed_fee_msat == expected_extra_fees_msat);
22142219
if !upstream_force_closed {
22152220
// Is the event prev_channel_id in one of the channels between the two nodes?
22162221
assert!(node.node().list_channels().iter().any(|x| x.counterparty.node_id == prev_node.node().get_our_node_id() && x.channel_id == prev_channel_id.unwrap()));
@@ -2226,13 +2231,15 @@ pub fn expect_payment_forwarded<CM: AChannelManager, H: NodeHolder<CM=CM>>(
22262231
}
22272232
}
22282233

2234+
#[macro_export]
22292235
macro_rules! expect_payment_forwarded {
22302236
($node: expr, $prev_node: expr, $next_node: expr, $expected_fee: expr, $upstream_force_closed: expr, $downstream_force_closed: expr) => {
22312237
let mut events = $node.node.get_and_clear_pending_events();
22322238
assert_eq!(events.len(), 1);
22332239
$crate::ln::functional_test_utils::expect_payment_forwarded(
2234-
events.pop().unwrap(), &$node, &$prev_node, &$next_node, $expected_fee,
2235-
$upstream_force_closed, $downstream_force_closed);
2240+
events.pop().unwrap(), &$node, &$prev_node, &$next_node, $expected_fee, None,
2241+
$upstream_force_closed, $downstream_force_closed
2242+
);
22362243
}
22372244
}
22382245

@@ -2552,24 +2559,54 @@ pub fn do_claim_payment_along_route<'a, 'b, 'c>(
25522559
origin_node: &Node<'a, 'b, 'c>, expected_paths: &[&[&Node<'a, 'b, 'c>]], skip_last: bool,
25532560
our_payment_preimage: PaymentPreimage
25542561
) -> u64 {
2555-
let extra_fees = vec![0; expected_paths.len()];
2556-
do_claim_payment_along_route_with_extra_penultimate_hop_fees(origin_node, expected_paths,
2557-
&extra_fees[..], skip_last, our_payment_preimage)
2558-
}
2559-
2560-
pub fn do_claim_payment_along_route_with_extra_penultimate_hop_fees<'a, 'b, 'c>(
2561-
origin_node: &Node<'a, 'b, 'c>, expected_paths: &[&[&Node<'a, 'b, 'c>]], expected_extra_fees:
2562-
&[u32], skip_last: bool, our_payment_preimage: PaymentPreimage
2563-
) -> u64 {
2564-
assert_eq!(expected_paths.len(), expected_extra_fees.len());
25652562
for path in expected_paths.iter() {
25662563
assert_eq!(path.last().unwrap().node.get_our_node_id(), expected_paths[0].last().unwrap().node.get_our_node_id());
25672564
}
25682565
expected_paths[0].last().unwrap().node.claim_funds(our_payment_preimage);
2569-
pass_claimed_payment_along_route(origin_node, expected_paths, expected_extra_fees, skip_last, our_payment_preimage)
2566+
pass_claimed_payment_along_route(
2567+
ClaimAlongRouteArgs::new(origin_node, expected_paths, our_payment_preimage)
2568+
.skip_last(skip_last)
2569+
)
2570+
}
2571+
2572+
pub struct ClaimAlongRouteArgs<'a, 'b, 'c, 'd> {
2573+
pub origin_node: &'a Node<'b, 'c, 'd>,
2574+
pub expected_paths: &'a [&'a [&'a Node<'b, 'c, 'd>]],
2575+
pub expected_extra_fees: Vec<u32>,
2576+
pub expected_min_htlc_overpay: Vec<u32>,
2577+
pub skip_last: bool,
2578+
pub payment_preimage: PaymentPreimage,
25702579
}
25712580

2572-
pub fn pass_claimed_payment_along_route<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>, expected_paths: &[&[&Node<'a, 'b, 'c>]], expected_extra_fees: &[u32], skip_last: bool, our_payment_preimage: PaymentPreimage) -> u64 {
2581+
impl<'a, 'b, 'c, 'd> ClaimAlongRouteArgs<'a, 'b, 'c, 'd> {
2582+
pub fn new(
2583+
origin_node: &'a Node<'b, 'c, 'd>, expected_paths: &'a [&'a [&'a Node<'b, 'c, 'd>]],
2584+
payment_preimage: PaymentPreimage,
2585+
) -> Self {
2586+
Self {
2587+
origin_node, expected_paths, expected_extra_fees: vec![0; expected_paths.len()],
2588+
expected_min_htlc_overpay: vec![0; expected_paths.len()], skip_last: false, payment_preimage,
2589+
}
2590+
}
2591+
pub fn skip_last(mut self, skip_last: bool) -> Self {
2592+
self.skip_last = skip_last;
2593+
self
2594+
}
2595+
pub fn with_expected_extra_fees(mut self, extra_fees: Vec<u32>) -> Self {
2596+
self.expected_extra_fees = extra_fees;
2597+
self
2598+
}
2599+
pub fn with_expected_min_htlc_overpay(mut self, extra_fees: Vec<u32>) -> Self {
2600+
self.expected_min_htlc_overpay = extra_fees;
2601+
self
2602+
}
2603+
}
2604+
2605+
pub fn pass_claimed_payment_along_route<'a, 'b, 'c, 'd>(args: ClaimAlongRouteArgs) -> u64 {
2606+
let ClaimAlongRouteArgs {
2607+
origin_node, expected_paths, expected_extra_fees, expected_min_htlc_overpay, skip_last,
2608+
payment_preimage: our_payment_preimage
2609+
} = args;
25732610
let claim_event = expected_paths[0].last().unwrap().node.get_and_clear_pending_events();
25742611
assert_eq!(claim_event.len(), 1);
25752612
match claim_event[0] {
@@ -2666,8 +2703,17 @@ pub fn pass_claimed_payment_along_route<'a, 'b, 'c>(origin_node: &Node<'a, 'b, '
26662703
channel.context().config().forwarding_fee_base_msat
26672704
}
26682705
};
2669-
if $idx == 1 { fee += expected_extra_fees[i]; }
2670-
expect_payment_forwarded!(*$node, $next_node, $prev_node, Some(fee as u64), false, false);
2706+
2707+
let mut expected_extra_fee = None;
2708+
if $idx == 1 {
2709+
fee += expected_extra_fees[i];
2710+
fee += expected_min_htlc_overpay[i];
2711+
expected_extra_fee = if expected_extra_fees[i] > 0 { Some(expected_extra_fees[i] as u64) } else { None };
2712+
}
2713+
let mut events = $node.node.get_and_clear_pending_events();
2714+
assert_eq!(events.len(), 1);
2715+
expect_payment_forwarded(events.pop().unwrap(), *$node, $next_node, $prev_node,
2716+
Some(fee as u64), expected_extra_fee, false, false);
26712717
expected_total_fee_msat += fee as u64;
26722718
check_added_monitors!($node, 1);
26732719
let new_next_msgs = if $new_msgs {

lightning/src/ln/functional_tests.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2889,8 +2889,10 @@ fn test_htlc_on_chain_success() {
28892889
}
28902890
let chan_id = Some(chan_1.2);
28912891
match forwarded_events[1] {
2892-
Event::PaymentForwarded { fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id, outbound_amount_forwarded_msat } => {
2893-
assert_eq!(fee_earned_msat, Some(1000));
2892+
Event::PaymentForwarded { total_fee_earned_msat, prev_channel_id, claim_from_onchain_tx,
2893+
next_channel_id, outbound_amount_forwarded_msat, ..
2894+
} => {
2895+
assert_eq!(total_fee_earned_msat, Some(1000));
28942896
assert_eq!(prev_channel_id, chan_id);
28952897
assert_eq!(claim_from_onchain_tx, true);
28962898
assert_eq!(next_channel_id, Some(chan_2.2));
@@ -2899,8 +2901,10 @@ fn test_htlc_on_chain_success() {
28992901
_ => panic!()
29002902
}
29012903
match forwarded_events[2] {
2902-
Event::PaymentForwarded { fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id, outbound_amount_forwarded_msat } => {
2903-
assert_eq!(fee_earned_msat, Some(1000));
2904+
Event::PaymentForwarded { total_fee_earned_msat, prev_channel_id, claim_from_onchain_tx,
2905+
next_channel_id, outbound_amount_forwarded_msat, ..
2906+
} => {
2907+
assert_eq!(total_fee_earned_msat, Some(1000));
29042908
assert_eq!(prev_channel_id, chan_id);
29052909
assert_eq!(claim_from_onchain_tx, true);
29062910
assert_eq!(next_channel_id, Some(chan_2.2));
@@ -4912,8 +4916,10 @@ fn test_onchain_to_onchain_claim() {
49124916
_ => panic!("Unexpected event"),
49134917
}
49144918
match events[1] {
4915-
Event::PaymentForwarded { fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id, outbound_amount_forwarded_msat } => {
4916-
assert_eq!(fee_earned_msat, Some(1000));
4919+
Event::PaymentForwarded { total_fee_earned_msat, prev_channel_id, claim_from_onchain_tx,
4920+
next_channel_id, outbound_amount_forwarded_msat, ..
4921+
} => {
4922+
assert_eq!(total_fee_earned_msat, Some(1000));
49174923
assert_eq!(prev_channel_id, Some(chan_1.2));
49184924
assert_eq!(claim_from_onchain_tx, true);
49194925
assert_eq!(next_channel_id, Some(chan_2.2));

0 commit comments

Comments
 (0)