Skip to content

Commit e14f25c

Browse files
committed
Allow forwarding HTLCs that were constructed for previous config
This is mostly motivated by the fact that payments may happen while the latest `ChannelUpdate` indicating our new `ChannelConfig` is still propagating throughout the network. By temporarily allowing the previous config, we can help reduce payment failures across the network.
1 parent e2f216b commit e14f25c

File tree

4 files changed

+86
-20
lines changed

4 files changed

+86
-20
lines changed

lightning/src/ln/channel.rs

+37
Original file line numberDiff line numberDiff line change
@@ -4551,6 +4551,43 @@ impl<Signer: Sign> Channel<Signer> {
45514551
did_channel_update
45524552
}
45534553

4554+
fn internal_htlc_satisfies_config(
4555+
&self, htlc: &msgs::UpdateAddHTLC, amt_to_forward: u64, outgoing_cltv_value: u32, config: &ChannelConfig,
4556+
) -> Result<(), (&'static str, u16)> {
4557+
let fee = amt_to_forward.checked_mul(config.forwarding_fee_proportional_millionths as u64)
4558+
.and_then(|prop_fee| (prop_fee / 1000000).checked_add(config.forwarding_fee_base_msat as u64));
4559+
if fee.is_none() || htlc.amount_msat < fee.unwrap() ||
4560+
(htlc.amount_msat - fee.unwrap()) < amt_to_forward {
4561+
return Err((
4562+
"Prior hop has deviated from specified fees parameters or origin node has obsolete ones",
4563+
0x1000 | 12, // fee_insufficient
4564+
));
4565+
}
4566+
if (htlc.cltv_expiry as u64) < outgoing_cltv_value as u64 + config.cltv_expiry_delta as u64 {
4567+
return Err((
4568+
"Forwarding node has tampered with the intended HTLC values or origin node has an obsolete cltv_expiry_delta",
4569+
0x1000 | 13, // incorrect_cltv_expiry
4570+
));
4571+
}
4572+
Ok(())
4573+
}
4574+
4575+
/// Determines whether the parameters of an incoming HTLC to be forwarded satisfy the channel's
4576+
/// [`ChannelConfig`]. This first looks at the channel's current [`ChannelConfig`], and if
4577+
/// unsuccessful, falls back to the previous one if one exists.
4578+
pub fn htlc_satisfies_config(
4579+
&self, htlc: &msgs::UpdateAddHTLC, amt_to_forward: u64, outgoing_cltv_value: u32,
4580+
) -> Result<(), (&'static str, u16)> {
4581+
self.internal_htlc_satisfies_config(&htlc, amt_to_forward, outgoing_cltv_value, &self.config())
4582+
.or_else(|err| {
4583+
if let Some(prev_config) = self.prev_config() {
4584+
self.internal_htlc_satisfies_config(htlc, amt_to_forward, outgoing_cltv_value, &prev_config)
4585+
} else {
4586+
Err(err)
4587+
}
4588+
})
4589+
}
4590+
45544591
pub fn get_feerate(&self) -> u32 {
45554592
self.feerate_per_kw
45564593
}

lightning/src/ln/channelmanager.rs

+13-11
Original file line numberDiff line numberDiff line change
@@ -2230,7 +2230,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
22302230
},
22312231
Some(id) => Some(id.clone()),
22322232
};
2233-
let (chan_update_opt, forwardee_cltv_expiry_delta) = if let Some(forwarding_id) = forwarding_id_opt {
2233+
let chan_update_opt = if let Some(forwarding_id) = forwarding_id_opt {
22342234
let chan = channel_state.as_mut().unwrap().by_id.get_mut(&forwarding_id).unwrap();
22352235
if !chan.should_announce() && !self.default_configuration.accept_forwards_to_priv_channels {
22362236
// Note that the behavior here should be identical to the above block - we
@@ -2257,18 +2257,20 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
22572257
if *amt_to_forward < chan.get_counterparty_htlc_minimum_msat() { // amount_below_minimum
22582258
break Some(("HTLC amount was below the htlc_minimum_msat", 0x1000 | 11, chan_update_opt));
22592259
}
2260-
let fee = amt_to_forward.checked_mul(chan.get_fee_proportional_millionths() as u64)
2261-
.and_then(|prop_fee| { (prop_fee / 1000000)
2262-
.checked_add(chan.get_outbound_forwarding_fee_base_msat() as u64) });
2263-
if fee.is_none() || msg.amount_msat < fee.unwrap() || (msg.amount_msat - fee.unwrap()) < *amt_to_forward { // fee_insufficient
2264-
break Some(("Prior hop has deviated from specified fees parameters or origin node has obsolete ones", 0x1000 | 12, chan_update_opt));
2260+
if let Err((err, code)) = chan.htlc_satisfies_config(&msg, *amt_to_forward, *outgoing_cltv_value) {
2261+
break Some((err, code, chan_update_opt));
22652262
}
2266-
(chan_update_opt, chan.get_cltv_expiry_delta())
2267-
} else { (None, MIN_CLTV_EXPIRY_DELTA) };
2263+
chan_update_opt
2264+
} else {
2265+
if (msg.cltv_expiry as u64) < (*outgoing_cltv_value) as u64 + MIN_CLTV_EXPIRY_DELTA as u64 { // incorrect_cltv_expiry
2266+
break Some((
2267+
"Forwarding node has tampered with the intended HTLC values or origin node has an obsolete cltv_expiry_delta",
2268+
0x1000 | 13, None,
2269+
));
2270+
}
2271+
None
2272+
};
22682273

2269-
if (msg.cltv_expiry as u64) < (*outgoing_cltv_value) as u64 + forwardee_cltv_expiry_delta as u64 { // incorrect_cltv_expiry
2270-
break Some(("Forwarding node has tampered with the intended HTLC values or origin node has an obsolete cltv_expiry_delta", 0x1000 | 13, chan_update_opt));
2271-
}
22722274
let cur_height = self.best_block.read().unwrap().height() + 1;
22732275
// Theoretically, channel counterparty shouldn't send us a HTLC expiring now,
22742276
// but we want to be robust wrt to counterparty packet sanitization (see

lightning/src/ln/functional_test_utils.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -1689,9 +1689,16 @@ pub fn do_claim_payment_along_route<'a, 'b, 'c>(origin_node: &Node<'a, 'b, 'c>,
16891689
($node: expr, $prev_node: expr, $next_node: expr, $new_msgs: expr) => {
16901690
{
16911691
$node.node.handle_update_fulfill_htlc(&$prev_node.node.get_our_node_id(), &next_msgs.as_ref().unwrap().0);
1692-
let fee = $node.node.channel_state.lock().unwrap()
1693-
.by_id.get(&next_msgs.as_ref().unwrap().0.channel_id).unwrap()
1694-
.config.options.forwarding_fee_base_msat;
1692+
let fee = {
1693+
let channel_state = $node.node.channel_state.lock().unwrap();
1694+
let channel = channel_state
1695+
.by_id.get(&next_msgs.as_ref().unwrap().0.channel_id).unwrap();
1696+
if let Some(prev_config) = channel.prev_config() {
1697+
prev_config.forwarding_fee_base_msat
1698+
} else {
1699+
channel.config().forwarding_fee_base_msat
1700+
}
1701+
};
16951702
expect_payment_forwarded!($node, $next_node, $prev_node, Some(fee as u64), false, false);
16961703
expected_total_fee_msat += fee as u64;
16971704
check_added_monitors!($node, 1);

lightning/src/ln/onion_route_tests.rs

+26-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use chain::channelmonitor::{ChannelMonitor, CLTV_CLAIM_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS};
1515
use chain::keysinterface::{KeysInterface, Recipient};
1616
use ln::{PaymentHash, PaymentSecret};
17+
use ln::channel::EXPIRE_PREV_CONFIG_TICKS;
1718
use ln::channelmanager::{ChannelManager, ChannelManagerReadArgs, HTLCForwardInfo, CLTV_FAR_FAR_AWAY, MIN_CLTV_EXPIRY_DELTA, PendingHTLCInfo, PendingHTLCRouting};
1819
use ln::onion_utils;
1920
use routing::gossip::{NetworkUpdate, RoutingFees, NodeId};
@@ -648,9 +649,16 @@ fn do_test_onion_failure_stale_channel_update(announced_channel: bool) {
648649
payment_hash, payment_secret);
649650
claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
650651

652+
// Closure to force expiry of a channel's previous config.
653+
let expire_prev_config = || {
654+
for _ in 0..EXPIRE_PREV_CONFIG_TICKS {
655+
nodes[1].node.timer_tick_occurred();
656+
}
657+
};
658+
651659
// Closure to update and retrieve the latest ChannelUpdate.
652660
let update_and_get_channel_update = |config: &ChannelConfig, expect_new_update: bool,
653-
prev_update: Option<&msgs::ChannelUpdate>| -> Option<msgs::ChannelUpdate> {
661+
prev_update: Option<&msgs::ChannelUpdate>, should_expire_prev_config: bool| -> Option<msgs::ChannelUpdate> {
654662
nodes[1].node.update_channel_config(
655663
channel_to_update_counterparty, &[channel_to_update.0], config,
656664
).unwrap();
@@ -674,6 +682,9 @@ fn do_test_onion_failure_stale_channel_update(announced_channel: bool) {
674682
if prev_update.is_some() {
675683
assert!(new_update.contents.timestamp > prev_update.unwrap().contents.timestamp)
676684
}
685+
if should_expire_prev_config {
686+
expire_prev_config();
687+
}
677688
Some(new_update)
678689
};
679690

@@ -704,28 +715,37 @@ fn do_test_onion_failure_stale_channel_update(announced_channel: bool) {
704715
.find(|channel| channel.channel_id == channel_to_update.0).unwrap()
705716
.config.unwrap();
706717
config.forwarding_fee_base_msat = u32::max_value();
707-
let msg = update_and_get_channel_update(&config, true, None).unwrap();
718+
let msg = update_and_get_channel_update(&config, true, None, false).unwrap();
719+
720+
// The old policy should still be in effect until a new block is connected.
721+
send_along_route_with_secret(&nodes[0], route.clone(), &[&[&nodes[1], &nodes[2]]], PAYMENT_AMT,
722+
payment_hash, payment_secret);
723+
claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage);
724+
725+
// Connect a block, which should expire the previous config, leading to a failure when
726+
// forwarding the HTLC.
727+
expire_prev_config();
708728
expect_onion_failure("fee_insufficient", UPDATE|12, &msg);
709729

710730
// Redundant updates should not trigger a new ChannelUpdate.
711-
assert!(update_and_get_channel_update(&config, false, None).is_none());
731+
assert!(update_and_get_channel_update(&config, false, None, false).is_none());
712732

713733
// Similarly, updates that do not have an affect on ChannelUpdate should not trigger a new one.
714734
config.force_close_avoidance_max_fee_satoshis *= 2;
715-
assert!(update_and_get_channel_update(&config, false, None).is_none());
735+
assert!(update_and_get_channel_update(&config, false, None, false).is_none());
716736

717737
// Reset the base fee to the default and increase the proportional fee which should trigger a
718738
// new ChannelUpdate.
719739
config.forwarding_fee_base_msat = default_config.forwarding_fee_base_msat;
720740
config.cltv_expiry_delta = u16::max_value();
721-
let msg = update_and_get_channel_update(&config, true, Some(&msg)).unwrap();
741+
let msg = update_and_get_channel_update(&config, true, Some(&msg), true).unwrap();
722742
expect_onion_failure("incorrect_cltv_expiry", UPDATE|13, &msg);
723743

724744
// Reset the proportional fee and increase the CLTV expiry delta which should trigger a new
725745
// ChannelUpdate.
726746
config.cltv_expiry_delta = default_config.cltv_expiry_delta;
727747
config.forwarding_fee_proportional_millionths = u32::max_value();
728-
let msg = update_and_get_channel_update(&config, true, Some(&msg)).unwrap();
748+
let msg = update_and_get_channel_update(&config, true, Some(&msg), true).unwrap();
729749
expect_onion_failure("fee_insufficient", UPDATE|12, &msg);
730750

731751
// To test persistence of the updated config, we'll re-initialize the ChannelManager.

0 commit comments

Comments
 (0)