Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose withheld amount for underpaying HTLCs in PaymentForwarded #2858

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions lightning/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@ pub enum Event {
/// The outgoing channel between the next node and us. This is only `None` for events
/// generated or serialized by versions prior to 0.0.107.
next_channel_id: Option<ChannelId>,
/// The fee, in milli-satoshis, which was earned as a result of the payment.
/// The total fee, in milli-satoshis, which was earned as a result of the payment.
///
/// Note that if we force-closed the channel over which we forwarded an HTLC while the HTLC
/// was pending, the amount the next hop claimed will have been rounded down to the nearest
Expand All @@ -794,15 +794,29 @@ pub enum Event {
/// If the channel which sent us the payment has been force-closed, we will claim the funds
/// via an on-chain transaction. In that case we do not yet know the on-chain transaction
/// fees which we will spend and will instead set this to `None`. It is possible duplicate
/// `PaymentForwarded` events are generated for the same payment iff `fee_earned_msat` is
/// `PaymentForwarded` events are generated for the same payment iff `total_fee_earned_msat` is
/// `None`.
fee_earned_msat: Option<u64>,
total_fee_earned_msat: Option<u64>,
/// The share of the total fee, in milli-satoshis, which was withheld in addition to the
/// forwarding fee.
///
/// This will only be `Some` if we forwarded an intercepted HTLC with less than the
tnull marked this conversation as resolved.
Show resolved Hide resolved
/// expected amount. This means our counterparty accepted to receive less than the invoice
/// amount, e.g., by claiming the payment featuring a corresponding
/// [`PaymentClaimable::counterparty_skimmed_fee_msat`].
///
/// Will also always be `None` for events serialized with LDK prior to version 0.0.122.
///
/// The caveat described above the `total_fee_earned_msat` field applies here as well.
///
/// [`PaymentClaimable::counterparty_skimmed_fee_msat`]: Self::PaymentClaimable::counterparty_skimmed_fee_msat
skimmed_fee_msat: Option<u64>,
tnull marked this conversation as resolved.
Show resolved Hide resolved
/// If this is `true`, the forwarded HTLC was claimed by our counterparty via an on-chain
/// transaction.
claim_from_onchain_tx: bool,
/// The final amount forwarded, in milli-satoshis, after the fee is deducted.
///
/// The caveat described above the `fee_earned_msat` field applies here as well.
/// The caveat described above the `total_fee_earned_msat` field applies here as well.
outbound_amount_forwarded_msat: Option<u64>,
},
/// Used to indicate that a channel with the given `channel_id` is being opened and pending
Expand Down Expand Up @@ -1083,16 +1097,17 @@ impl Writeable for Event {
});
}
&Event::PaymentForwarded {
fee_earned_msat, prev_channel_id, claim_from_onchain_tx,
next_channel_id, outbound_amount_forwarded_msat
total_fee_earned_msat, prev_channel_id, claim_from_onchain_tx,
next_channel_id, outbound_amount_forwarded_msat, skimmed_fee_msat,
} => {
7u8.write(writer)?;
write_tlv_fields!(writer, {
(0, fee_earned_msat, option),
(0, total_fee_earned_msat, option),
(1, prev_channel_id, option),
(2, claim_from_onchain_tx, required),
(3, next_channel_id, option),
(5, outbound_amount_forwarded_msat, option),
(7, skimmed_fee_msat, option),
});
},
&Event::ChannelClosed { ref channel_id, ref user_channel_id, ref reason,
Expand Down Expand Up @@ -1384,21 +1399,23 @@ impl MaybeReadable for Event {
},
7u8 => {
let f = || {
let mut fee_earned_msat = None;
let mut total_fee_earned_msat = None;
let mut prev_channel_id = None;
let mut claim_from_onchain_tx = false;
let mut next_channel_id = None;
let mut outbound_amount_forwarded_msat = None;
let mut skimmed_fee_msat = None;
read_tlv_fields!(reader, {
(0, fee_earned_msat, option),
(0, total_fee_earned_msat, option),
(1, prev_channel_id, option),
(2, claim_from_onchain_tx, required),
(3, next_channel_id, option),
(5, outbound_amount_forwarded_msat, option),
(7, skimmed_fee_msat, option),
});
Ok(Some(Event::PaymentForwarded {
fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id,
outbound_amount_forwarded_msat
total_fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id,
outbound_amount_forwarded_msat, skimmed_fee_msat,
}))
};
f()
Expand Down
3 changes: 2 additions & 1 deletion lightning/src/ln/chanmon_update_fail_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3404,7 +3404,8 @@ fn do_test_reload_mon_update_completion_actions(close_during_reload: bool) {
let bc_update_id = nodes[1].chain_monitor.latest_monitor_update_id.lock().unwrap().get(&chan_id_bc).unwrap().2;
let mut events = nodes[1].node.get_and_clear_pending_events();
assert_eq!(events.len(), if close_during_reload { 2 } else { 1 });
expect_payment_forwarded(events.pop().unwrap(), &nodes[1], &nodes[0], &nodes[2], Some(1000), close_during_reload, false);
expect_payment_forwarded(events.pop().unwrap(), &nodes[1], &nodes[0], &nodes[2], Some(1000),
None, close_during_reload, false);
if close_during_reload {
match events[0] {
Event::ChannelClosed { .. } => {},
Expand Down
4 changes: 2 additions & 2 deletions lightning/src/ln/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3327,15 +3327,15 @@ impl<SP: Deref> Channel<SP> where
Err(ChannelError::Close("Remote tried to fulfill/fail an HTLC we couldn't find".to_owned()))
}

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

self.mark_outbound_htlc_removed(msg.htlc_id, Some(msg.payment_preimage), None).map(|htlc| (htlc.source.clone(), htlc.amount_msat))
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))
}

pub fn update_fail_htlc(&mut self, msg: &msgs::UpdateFailHTLC, fail_reason: HTLCFailReason) -> Result<(), ChannelError> {
Expand Down
28 changes: 18 additions & 10 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5690,9 +5690,9 @@ where
}

fn claim_funds_internal(&self, source: HTLCSource, payment_preimage: PaymentPreimage,
forwarded_htlc_value_msat: Option<u64>, from_onchain: bool, startup_replay: bool,
next_channel_counterparty_node_id: Option<PublicKey>, next_channel_outpoint: OutPoint,
next_channel_id: ChannelId,
forwarded_htlc_value_msat: Option<u64>, skimmed_fee_msat: Option<u64>, from_onchain: bool,
startup_replay: bool, next_channel_counterparty_node_id: Option<PublicKey>,
next_channel_outpoint: OutPoint, next_channel_id: ChannelId,
) {
match source {
HTLCSource::OutboundRoute { session_priv, payment_id, path, .. } => {
Expand Down Expand Up @@ -5788,18 +5788,21 @@ where
})
} else { None }
} else {
let fee_earned_msat = if let Some(forwarded_htlc_value) = forwarded_htlc_value_msat {
let total_fee_earned_msat = if let Some(forwarded_htlc_value) = forwarded_htlc_value_msat {
if let Some(claimed_htlc_value) = htlc_claim_value_msat {
Some(claimed_htlc_value - forwarded_htlc_value)
} else { None }
} else { None };
debug_assert!(skimmed_fee_msat <= total_fee_earned_msat,
"skimmed_fee_msat must always be included in total_fee_earned_msat");
Some(MonitorUpdateCompletionAction::EmitEventAndFreeOtherChannel {
event: events::Event::PaymentForwarded {
fee_earned_msat,
total_fee_earned_msat,
claim_from_onchain_tx: from_onchain,
prev_channel_id: Some(prev_channel_id),
next_channel_id: Some(next_channel_id),
outbound_amount_forwarded_msat: forwarded_htlc_value_msat,
skimmed_fee_msat,
},
downstream_counterparty_and_funding_outpoint: chan_to_release,
})
Expand Down Expand Up @@ -6739,7 +6742,7 @@ where

fn internal_update_fulfill_htlc(&self, counterparty_node_id: &PublicKey, msg: &msgs::UpdateFulfillHTLC) -> Result<(), MsgHandleErrInternal> {
let funding_txo;
let (htlc_source, forwarded_htlc_value) = {
let (htlc_source, forwarded_htlc_value, skimmed_fee_msat) = {
let per_peer_state = self.per_peer_state.read().unwrap();
let peer_state_mutex = per_peer_state.get(counterparty_node_id)
.ok_or_else(|| {
Expand Down Expand Up @@ -6777,8 +6780,11 @@ where
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))
}
};
self.claim_funds_internal(htlc_source, msg.payment_preimage.clone(), Some(forwarded_htlc_value),
false, false, Some(*counterparty_node_id), funding_txo, msg.channel_id);
self.claim_funds_internal(htlc_source, msg.payment_preimage.clone(),
Some(forwarded_htlc_value), skimmed_fee_msat, false, false, Some(*counterparty_node_id),
funding_txo, msg.channel_id
);

Ok(())
}

Expand Down Expand Up @@ -7276,7 +7282,9 @@ where
let logger = WithContext::from(&self.logger, counterparty_node_id, Some(channel_id));
if let Some(preimage) = htlc_update.payment_preimage {
log_trace!(logger, "Claiming HTLC with preimage {} from our monitor", preimage);
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);
self.claim_funds_internal(htlc_update.source, preimage,
htlc_update.htlc_value_satoshis.map(|v| v * 1000), None, true,
false, counterparty_node_id, funding_outpoint, channel_id);
} else {
log_trace!(logger, "Failing HTLC with hash {} from our monitor", &htlc_update.payment_hash);
let receiver = HTLCDestination::NextHopChannel { node_id: counterparty_node_id, channel_id };
Expand Down Expand Up @@ -11168,7 +11176,7 @@ where
// We use `downstream_closed` in place of `from_onchain` here just as a guess - we
// don't remember in the `ChannelMonitor` where we got a preimage from, but if the
// channel is closed we just assume that it probably came from an on-chain claim.
channel_manager.claim_funds_internal(source, preimage, Some(downstream_value),
channel_manager.claim_funds_internal(source, preimage, Some(downstream_value), None,
downstream_closed, true, downstream_node_id, downstream_funding, downstream_channel_id);
}

Expand Down
86 changes: 66 additions & 20 deletions lightning/src/ln/functional_test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2203,14 +2203,19 @@ macro_rules! expect_payment_path_successful {

pub fn expect_payment_forwarded<CM: AChannelManager, H: NodeHolder<CM=CM>>(
event: Event, node: &H, prev_node: &H, next_node: &H, expected_fee: Option<u64>,
upstream_force_closed: bool, downstream_force_closed: bool
expected_extra_fees_msat: Option<u64>, upstream_force_closed: bool,
downstream_force_closed: bool
) {
match event {
Event::PaymentForwarded {
fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id,
outbound_amount_forwarded_msat: _
total_fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id,
outbound_amount_forwarded_msat: _, skimmed_fee_msat
} => {
assert_eq!(fee_earned_msat, expected_fee);
assert_eq!(total_fee_earned_msat, expected_fee);

// Check that the (knowingly) withheld amount is always less or equal to the expected
// overpaid amount.
assert!(skimmed_fee_msat == expected_extra_fees_msat);
if !upstream_force_closed {
// Is the event prev_channel_id in one of the channels between the two nodes?
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()));
Expand All @@ -2226,13 +2231,15 @@ pub fn expect_payment_forwarded<CM: AChannelManager, H: NodeHolder<CM=CM>>(
}
}

#[macro_export]
macro_rules! expect_payment_forwarded {
($node: expr, $prev_node: expr, $next_node: expr, $expected_fee: expr, $upstream_force_closed: expr, $downstream_force_closed: expr) => {
let mut events = $node.node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
$crate::ln::functional_test_utils::expect_payment_forwarded(
events.pop().unwrap(), &$node, &$prev_node, &$next_node, $expected_fee,
$upstream_force_closed, $downstream_force_closed);
events.pop().unwrap(), &$node, &$prev_node, &$next_node, $expected_fee, None,
$upstream_force_closed, $downstream_force_closed
);
}
}

Expand Down Expand Up @@ -2552,24 +2559,54 @@ pub fn do_claim_payment_along_route<'a, 'b, 'c>(
origin_node: &Node<'a, 'b, 'c>, expected_paths: &[&[&Node<'a, 'b, 'c>]], skip_last: bool,
our_payment_preimage: PaymentPreimage
) -> u64 {
let extra_fees = vec![0; expected_paths.len()];
do_claim_payment_along_route_with_extra_penultimate_hop_fees(origin_node, expected_paths,
&extra_fees[..], skip_last, our_payment_preimage)
}

pub fn do_claim_payment_along_route_with_extra_penultimate_hop_fees<'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 {
assert_eq!(expected_paths.len(), expected_extra_fees.len());
for path in expected_paths.iter() {
assert_eq!(path.last().unwrap().node.get_our_node_id(), expected_paths[0].last().unwrap().node.get_our_node_id());
}
expected_paths[0].last().unwrap().node.claim_funds(our_payment_preimage);
pass_claimed_payment_along_route(origin_node, expected_paths, expected_extra_fees, skip_last, our_payment_preimage)
pass_claimed_payment_along_route(
ClaimAlongRouteArgs::new(origin_node, expected_paths, our_payment_preimage)
.skip_last(skip_last)
)
}

pub struct ClaimAlongRouteArgs<'a, 'b, 'c, 'd> {
pub origin_node: &'a Node<'b, 'c, 'd>,
pub expected_paths: &'a [&'a [&'a Node<'b, 'c, 'd>]],
pub expected_extra_fees: Vec<u32>,
pub expected_min_htlc_overpay: Vec<u32>,
pub skip_last: bool,
pub payment_preimage: PaymentPreimage,
}

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 {
impl<'a, 'b, 'c, 'd> ClaimAlongRouteArgs<'a, 'b, 'c, 'd> {
pub fn new(
origin_node: &'a Node<'b, 'c, 'd>, expected_paths: &'a [&'a [&'a Node<'b, 'c, 'd>]],
payment_preimage: PaymentPreimage,
) -> Self {
Self {
origin_node, expected_paths, expected_extra_fees: vec![0; expected_paths.len()],
expected_min_htlc_overpay: vec![0; expected_paths.len()], skip_last: false, payment_preimage,
}
}
pub fn skip_last(mut self, skip_last: bool) -> Self {
self.skip_last = skip_last;
self
}
pub fn with_expected_extra_fees(mut self, extra_fees: Vec<u32>) -> Self {
self.expected_extra_fees = extra_fees;
self
}
pub fn with_expected_min_htlc_overpay(mut self, extra_fees: Vec<u32>) -> Self {
self.expected_min_htlc_overpay = extra_fees;
self
}
}

pub fn pass_claimed_payment_along_route<'a, 'b, 'c, 'd>(args: ClaimAlongRouteArgs) -> u64 {
let ClaimAlongRouteArgs {
origin_node, expected_paths, expected_extra_fees, expected_min_htlc_overpay, skip_last,
payment_preimage: our_payment_preimage
} = args;
let claim_event = expected_paths[0].last().unwrap().node.get_and_clear_pending_events();
assert_eq!(claim_event.len(), 1);
match claim_event[0] {
Expand Down Expand Up @@ -2666,8 +2703,17 @@ pub fn pass_claimed_payment_along_route<'a, 'b, 'c>(origin_node: &Node<'a, 'b, '
channel.context().config().forwarding_fee_base_msat
}
};
if $idx == 1 { fee += expected_extra_fees[i]; }
expect_payment_forwarded!(*$node, $next_node, $prev_node, Some(fee as u64), false, false);

let mut expected_extra_fee = None;
if $idx == 1 {
fee += expected_extra_fees[i];
fee += expected_min_htlc_overpay[i];
expected_extra_fee = if expected_extra_fees[i] > 0 { Some(expected_extra_fees[i] as u64) } else { None };
}
let mut events = $node.node.get_and_clear_pending_events();
assert_eq!(events.len(), 1);
expect_payment_forwarded(events.pop().unwrap(), *$node, $next_node, $prev_node,
Some(fee as u64), expected_extra_fee, false, false);
expected_total_fee_msat += fee as u64;
check_added_monitors!($node, 1);
let new_next_msgs = if $new_msgs {
Expand Down
18 changes: 12 additions & 6 deletions lightning/src/ln/functional_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2889,8 +2889,10 @@ fn test_htlc_on_chain_success() {
}
let chan_id = Some(chan_1.2);
match forwarded_events[1] {
Event::PaymentForwarded { fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id, outbound_amount_forwarded_msat } => {
assert_eq!(fee_earned_msat, Some(1000));
Event::PaymentForwarded { total_fee_earned_msat, prev_channel_id, claim_from_onchain_tx,
next_channel_id, outbound_amount_forwarded_msat, ..
} => {
assert_eq!(total_fee_earned_msat, Some(1000));
assert_eq!(prev_channel_id, chan_id);
assert_eq!(claim_from_onchain_tx, true);
assert_eq!(next_channel_id, Some(chan_2.2));
Expand All @@ -2899,8 +2901,10 @@ fn test_htlc_on_chain_success() {
_ => panic!()
}
match forwarded_events[2] {
Event::PaymentForwarded { fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id, outbound_amount_forwarded_msat } => {
assert_eq!(fee_earned_msat, Some(1000));
Event::PaymentForwarded { total_fee_earned_msat, prev_channel_id, claim_from_onchain_tx,
next_channel_id, outbound_amount_forwarded_msat, ..
} => {
assert_eq!(total_fee_earned_msat, Some(1000));
assert_eq!(prev_channel_id, chan_id);
assert_eq!(claim_from_onchain_tx, true);
assert_eq!(next_channel_id, Some(chan_2.2));
Expand Down Expand Up @@ -4912,8 +4916,10 @@ fn test_onchain_to_onchain_claim() {
_ => panic!("Unexpected event"),
}
match events[1] {
Event::PaymentForwarded { fee_earned_msat, prev_channel_id, claim_from_onchain_tx, next_channel_id, outbound_amount_forwarded_msat } => {
assert_eq!(fee_earned_msat, Some(1000));
Event::PaymentForwarded { total_fee_earned_msat, prev_channel_id, claim_from_onchain_tx,
next_channel_id, outbound_amount_forwarded_msat, ..
} => {
assert_eq!(total_fee_earned_msat, Some(1000));
assert_eq!(prev_channel_id, Some(chan_1.2));
assert_eq!(claim_from_onchain_tx, true);
assert_eq!(next_channel_id, Some(chan_2.2));
Expand Down
Loading
Loading