Skip to content

Commit 90541c2

Browse files
authored
Merge pull request #1527 from wpaulino/update-htlc-relay-policy
Expose API to update a channel's ChannelConfig
2 parents 16115cd + 0f30d76 commit 90541c2

File tree

8 files changed

+433
-30
lines changed

8 files changed

+433
-30
lines changed

fuzz/src/router.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
235235
next_outbound_htlc_limit_msat: capacity.saturating_mul(1000),
236236
inbound_htlc_minimum_msat: None,
237237
inbound_htlc_maximum_msat: None,
238+
config: None,
238239
});
239240
}
240241
Some(&first_hops_vec[..])

lightning/src/ln/channel.rs

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ use util::events::ClosureReason;
3939
use util::ser::{Readable, ReadableArgs, Writeable, Writer, VecWriter};
4040
use util::logger::Logger;
4141
use util::errors::APIError;
42-
use util::config::{UserConfig, LegacyChannelConfig, ChannelHandshakeConfig, ChannelHandshakeLimits};
42+
use util::config::{UserConfig, ChannelConfig, LegacyChannelConfig, ChannelHandshakeConfig, ChannelHandshakeLimits};
4343
use util::scid_utils::scid_from_parts;
4444

4545
use io;
@@ -482,6 +482,16 @@ pub(crate) const CONCURRENT_INBOUND_HTLC_FEE_BUFFER: u32 = 2;
482482
/// transaction (not counting the value of the HTLCs themselves).
483483
pub(crate) const MIN_AFFORDABLE_HTLC_COUNT: usize = 4;
484484

485+
/// When a [`Channel`] has its [`ChannelConfig`] updated, its existing one is stashed for up to this
486+
/// number of ticks to allow forwarding HTLCs by nodes that have yet to receive the new
487+
/// ChannelUpdate prompted by the config update. This value was determined as follows:
488+
///
489+
/// * The expected interval between ticks (1 minute).
490+
/// * The average convergence delay of updates across the network, i.e., ~300 seconds on average
491+
/// for a node to see an update as seen on `<https://arxiv.org/pdf/2205.12737.pdf>`.
492+
/// * `EXPIRE_PREV_CONFIG_TICKS` = convergence_delay / tick_interval
493+
pub(crate) const EXPIRE_PREV_CONFIG_TICKS: usize = 5;
494+
485495
// TODO: We should refactor this to be an Inbound/OutboundChannel until initial setup handshaking
486496
// has been completed, and then turn into a Channel to get compiler-time enforcement of things like
487497
// calling channel_id() before we're set up or things like get_outbound_funding_signed on an
@@ -490,11 +500,13 @@ pub(crate) const MIN_AFFORDABLE_HTLC_COUNT: usize = 4;
490500
// Holder designates channel data owned for the benefice of the user client.
491501
// Counterparty designates channel data owned by the another channel participant entity.
492502
pub(super) struct Channel<Signer: Sign> {
493-
#[cfg(any(test, feature = "_test_utils"))]
494-
pub(crate) config: LegacyChannelConfig,
495-
#[cfg(not(any(test, feature = "_test_utils")))]
496503
config: LegacyChannelConfig,
497504

505+
// Track the previous `ChannelConfig` so that we can continue forwarding HTLCs that were
506+
// constructed using it. The second element in the tuple corresponds to the number of ticks that
507+
// have elapsed since the update occurred.
508+
prev_config: Option<(ChannelConfig, usize)>,
509+
498510
inbound_handshake_limits_override: Option<ChannelHandshakeLimits>,
499511

500512
user_id: u64,
@@ -937,6 +949,8 @@ impl<Signer: Sign> Channel<Signer> {
937949
commit_upfront_shutdown_pubkey: config.channel_handshake_config.commit_upfront_shutdown_pubkey,
938950
},
939951

952+
prev_config: None,
953+
940954
inbound_handshake_limits_override: Some(config.channel_handshake_limits.clone()),
941955

942956
channel_id: keys_provider.get_secure_random_bytes(),
@@ -1264,6 +1278,8 @@ impl<Signer: Sign> Channel<Signer> {
12641278
commit_upfront_shutdown_pubkey: config.channel_handshake_config.commit_upfront_shutdown_pubkey,
12651279
},
12661280

1281+
prev_config: None,
1282+
12671283
inbound_handshake_limits_override: None,
12681284

12691285
channel_id: msg.temporary_channel_id,
@@ -4491,6 +4507,84 @@ impl<Signer: Sign> Channel<Signer> {
44914507
self.config.options.max_dust_htlc_exposure_msat
44924508
}
44934509

4510+
/// Returns the previous [`ChannelConfig`] applied to this channel, if any.
4511+
pub fn prev_config(&self) -> Option<ChannelConfig> {
4512+
self.prev_config.map(|prev_config| prev_config.0)
4513+
}
4514+
4515+
/// Tracks the number of ticks elapsed since the previous [`ChannelConfig`] was updated. Once
4516+
/// [`EXPIRE_PREV_CONFIG_TICKS`] is reached, the previous config is considered expired and will
4517+
/// no longer be considered when forwarding HTLCs.
4518+
pub fn maybe_expire_prev_config(&mut self) {
4519+
if self.prev_config.is_none() {
4520+
return;
4521+
}
4522+
let prev_config = self.prev_config.as_mut().unwrap();
4523+
prev_config.1 += 1;
4524+
if prev_config.1 == EXPIRE_PREV_CONFIG_TICKS {
4525+
self.prev_config = None;
4526+
}
4527+
}
4528+
4529+
/// Returns the current [`ChannelConfig`] applied to the channel.
4530+
pub fn config(&self) -> ChannelConfig {
4531+
self.config.options
4532+
}
4533+
4534+
/// Updates the channel's config. A bool is returned indicating whether the config update
4535+
/// applied resulted in a new ChannelUpdate message.
4536+
pub fn update_config(&mut self, config: &ChannelConfig) -> bool {
4537+
let did_channel_update =
4538+
self.config.options.forwarding_fee_proportional_millionths != config.forwarding_fee_proportional_millionths ||
4539+
self.config.options.forwarding_fee_base_msat != config.forwarding_fee_base_msat ||
4540+
self.config.options.cltv_expiry_delta != config.cltv_expiry_delta;
4541+
if did_channel_update {
4542+
self.prev_config = Some((self.config.options, 0));
4543+
// Update the counter, which backs the ChannelUpdate timestamp, to allow the relay
4544+
// policy change to propagate throughout the network.
4545+
self.update_time_counter += 1;
4546+
}
4547+
self.config.options = *config;
4548+
did_channel_update
4549+
}
4550+
4551+
fn internal_htlc_satisfies_config(
4552+
&self, htlc: &msgs::UpdateAddHTLC, amt_to_forward: u64, outgoing_cltv_value: u32, config: &ChannelConfig,
4553+
) -> Result<(), (&'static str, u16)> {
4554+
let fee = amt_to_forward.checked_mul(config.forwarding_fee_proportional_millionths as u64)
4555+
.and_then(|prop_fee| (prop_fee / 1000000).checked_add(config.forwarding_fee_base_msat as u64));
4556+
if fee.is_none() || htlc.amount_msat < fee.unwrap() ||
4557+
(htlc.amount_msat - fee.unwrap()) < amt_to_forward {
4558+
return Err((
4559+
"Prior hop has deviated from specified fees parameters or origin node has obsolete ones",
4560+
0x1000 | 12, // fee_insufficient
4561+
));
4562+
}
4563+
if (htlc.cltv_expiry as u64) < outgoing_cltv_value as u64 + config.cltv_expiry_delta as u64 {
4564+
return Err((
4565+
"Forwarding node has tampered with the intended HTLC values or origin node has an obsolete cltv_expiry_delta",
4566+
0x1000 | 13, // incorrect_cltv_expiry
4567+
));
4568+
}
4569+
Ok(())
4570+
}
4571+
4572+
/// Determines whether the parameters of an incoming HTLC to be forwarded satisfy the channel's
4573+
/// [`ChannelConfig`]. This first looks at the channel's current [`ChannelConfig`], and if
4574+
/// unsuccessful, falls back to the previous one if one exists.
4575+
pub fn htlc_satisfies_config(
4576+
&self, htlc: &msgs::UpdateAddHTLC, amt_to_forward: u64, outgoing_cltv_value: u32,
4577+
) -> Result<(), (&'static str, u16)> {
4578+
self.internal_htlc_satisfies_config(&htlc, amt_to_forward, outgoing_cltv_value, &self.config())
4579+
.or_else(|err| {
4580+
if let Some(prev_config) = self.prev_config() {
4581+
self.internal_htlc_satisfies_config(htlc, amt_to_forward, outgoing_cltv_value, &prev_config)
4582+
} else {
4583+
Err(err)
4584+
}
4585+
})
4586+
}
4587+
44944588
pub fn get_feerate(&self) -> u32 {
44954589
self.feerate_per_kw
44964590
}
@@ -6339,6 +6433,8 @@ impl<'a, Signer: Sign, K: Deref> ReadableArgs<(&'a K, u32)> for Channel<Signer>
63396433

63406434
config: config.unwrap(),
63416435

6436+
prev_config: None,
6437+
63426438
// Note that we don't care about serializing handshake limits as we only ever serialize
63436439
// channel data after the handshake has completed.
63446440
inbound_handshake_limits_override: None,

lightning/src/ln/channelmanager.rs

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ use ln::onion_utils;
5151
use ln::msgs::{ChannelMessageHandler, DecodeError, LightningError, MAX_VALUE_MSAT, OptionalField};
5252
use ln::wire::Encode;
5353
use chain::keysinterface::{Sign, KeysInterface, KeysManager, InMemorySigner, Recipient};
54-
use util::config::UserConfig;
54+
use util::config::{UserConfig, ChannelConfig};
5555
use util::events::{EventHandler, EventsProvider, MessageSendEvent, MessageSendEventsProvider, ClosureReason};
5656
use util::{byte_utils, events};
5757
use util::scid_utils::fake_scid;
@@ -1101,6 +1101,10 @@ pub struct ChannelDetails {
11011101
pub inbound_htlc_minimum_msat: Option<u64>,
11021102
/// The largest value HTLC (in msat) we currently will accept, for this channel.
11031103
pub inbound_htlc_maximum_msat: Option<u64>,
1104+
/// Set of configurable parameters that affect channel operation.
1105+
///
1106+
/// This field is only `None` for `ChannelDetails` objects serialized prior to LDK 0.0.109.
1107+
pub config: Option<ChannelConfig>,
11041108
}
11051109

11061110
impl ChannelDetails {
@@ -1765,7 +1769,8 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
17651769
is_usable: channel.is_live(),
17661770
is_public: channel.should_announce(),
17671771
inbound_htlc_minimum_msat: Some(channel.get_holder_htlc_minimum_msat()),
1768-
inbound_htlc_maximum_msat: channel.get_holder_htlc_maximum_msat()
1772+
inbound_htlc_maximum_msat: channel.get_holder_htlc_maximum_msat(),
1773+
config: Some(channel.config()),
17691774
});
17701775
}
17711776
}
@@ -2231,7 +2236,7 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
22312236
},
22322237
Some(id) => Some(id.clone()),
22332238
};
2234-
let (chan_update_opt, forwardee_cltv_expiry_delta) = if let Some(forwarding_id) = forwarding_id_opt {
2239+
let chan_update_opt = if let Some(forwarding_id) = forwarding_id_opt {
22352240
let chan = channel_state.as_mut().unwrap().by_id.get_mut(&forwarding_id).unwrap();
22362241
if !chan.should_announce() && !self.default_configuration.accept_forwards_to_priv_channels {
22372242
// Note that the behavior here should be identical to the above block - we
@@ -2258,18 +2263,20 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
22582263
if *amt_to_forward < chan.get_counterparty_htlc_minimum_msat() { // amount_below_minimum
22592264
break Some(("HTLC amount was below the htlc_minimum_msat", 0x1000 | 11, chan_update_opt));
22602265
}
2261-
let fee = amt_to_forward.checked_mul(chan.get_fee_proportional_millionths() as u64)
2262-
.and_then(|prop_fee| { (prop_fee / 1000000)
2263-
.checked_add(chan.get_outbound_forwarding_fee_base_msat() as u64) });
2264-
if fee.is_none() || msg.amount_msat < fee.unwrap() || (msg.amount_msat - fee.unwrap()) < *amt_to_forward { // fee_insufficient
2265-
break Some(("Prior hop has deviated from specified fees parameters or origin node has obsolete ones", 0x1000 | 12, chan_update_opt));
2266+
if let Err((err, code)) = chan.htlc_satisfies_config(&msg, *amt_to_forward, *outgoing_cltv_value) {
2267+
break Some((err, code, chan_update_opt));
2268+
}
2269+
chan_update_opt
2270+
} else {
2271+
if (msg.cltv_expiry as u64) < (*outgoing_cltv_value) as u64 + MIN_CLTV_EXPIRY_DELTA as u64 { // incorrect_cltv_expiry
2272+
break Some((
2273+
"Forwarding node has tampered with the intended HTLC values or origin node has an obsolete cltv_expiry_delta",
2274+
0x1000 | 13, None,
2275+
));
22662276
}
2267-
(chan_update_opt, chan.get_cltv_expiry_delta())
2268-
} else { (None, MIN_CLTV_EXPIRY_DELTA) };
2277+
None
2278+
};
22692279

2270-
if (msg.cltv_expiry as u64) < (*outgoing_cltv_value) as u64 + forwardee_cltv_expiry_delta as u64 { // incorrect_cltv_expiry
2271-
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));
2272-
}
22732280
let cur_height = self.best_block.read().unwrap().height() + 1;
22742281
// Theoretically, channel counterparty shouldn't send us a HTLC expiring now,
22752282
// but we want to be robust wrt to counterparty packet sanitization (see
@@ -2940,6 +2947,73 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
29402947
}
29412948
}
29422949

2950+
/// Atomically updates the [`ChannelConfig`] for the given channels.
2951+
///
2952+
/// Once the updates are applied, each eligible channel (advertised with a known short channel
2953+
/// ID and a change in [`forwarding_fee_proportional_millionths`], [`forwarding_fee_base_msat`],
2954+
/// or [`cltv_expiry_delta`]) has a [`BroadcastChannelUpdate`] event message generated
2955+
/// containing the new [`ChannelUpdate`] message which should be broadcast to the network.
2956+
///
2957+
/// Returns [`ChannelUnavailable`] when a channel is not found or an incorrect
2958+
/// `counterparty_node_id` is provided.
2959+
///
2960+
/// Returns [`APIMisuseError`] when a [`cltv_expiry_delta`] update is to be applied with a value
2961+
/// below [`MIN_CLTV_EXPIRY_DELTA`].
2962+
///
2963+
/// If an error is returned, none of the updates should be considered applied.
2964+
///
2965+
/// [`forwarding_fee_proportional_millionths`]: ChannelConfig::forwarding_fee_proportional_millionths
2966+
/// [`forwarding_fee_base_msat`]: ChannelConfig::forwarding_fee_base_msat
2967+
/// [`cltv_expiry_delta`]: ChannelConfig::cltv_expiry_delta
2968+
/// [`BroadcastChannelUpdate`]: events::MessageSendEvent::BroadcastChannelUpdate
2969+
/// [`ChannelUpdate`]: msgs::ChannelUpdate
2970+
/// [`ChannelUnavailable`]: APIError::ChannelUnavailable
2971+
/// [`APIMisuseError`]: APIError::APIMisuseError
2972+
pub fn update_channel_config(
2973+
&self, counterparty_node_id: &PublicKey, channel_ids: &[[u8; 32]], config: &ChannelConfig,
2974+
) -> Result<(), APIError> {
2975+
if config.cltv_expiry_delta < MIN_CLTV_EXPIRY_DELTA {
2976+
return Err(APIError::APIMisuseError {
2977+
err: format!("The chosen CLTV expiry delta is below the minimum of {}", MIN_CLTV_EXPIRY_DELTA),
2978+
});
2979+
}
2980+
2981+
let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(
2982+
&self.total_consistency_lock, &self.persistence_notifier,
2983+
);
2984+
{
2985+
let mut channel_state_lock = self.channel_state.lock().unwrap();
2986+
let channel_state = &mut *channel_state_lock;
2987+
for channel_id in channel_ids {
2988+
let channel_counterparty_node_id = channel_state.by_id.get(channel_id)
2989+
.ok_or(APIError::ChannelUnavailable {
2990+
err: format!("Channel with ID {} was not found", log_bytes!(*channel_id)),
2991+
})?
2992+
.get_counterparty_node_id();
2993+
if channel_counterparty_node_id != *counterparty_node_id {
2994+
return Err(APIError::APIMisuseError {
2995+
err: "counterparty node id mismatch".to_owned(),
2996+
});
2997+
}
2998+
}
2999+
for channel_id in channel_ids {
3000+
let channel = channel_state.by_id.get_mut(channel_id).unwrap();
3001+
if !channel.update_config(config) {
3002+
continue;
3003+
}
3004+
if let Ok(msg) = self.get_channel_update_for_broadcast(channel) {
3005+
channel_state.pending_msg_events.push(events::MessageSendEvent::BroadcastChannelUpdate { msg });
3006+
} else if let Ok(msg) = self.get_channel_update_for_unicast(channel) {
3007+
channel_state.pending_msg_events.push(events::MessageSendEvent::SendChannelUpdate {
3008+
node_id: channel.get_counterparty_node_id(),
3009+
msg,
3010+
});
3011+
}
3012+
}
3013+
}
3014+
Ok(())
3015+
}
3016+
29433017
/// Processes HTLCs which are pending waiting on random forward delay.
29443018
///
29453019
/// Should only really ever be called in response to a PendingHTLCsForwardable event.
@@ -3465,6 +3539,8 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
34653539
/// * Broadcasting `ChannelUpdate` messages if we've been disconnected from our peer for more
34663540
/// than a minute, informing the network that they should no longer attempt to route over
34673541
/// the channel.
3542+
/// * Expiring a channel's previous `ChannelConfig` if necessary to only allow forwarding HTLCs
3543+
/// with the current `ChannelConfig`.
34683544
///
34693545
/// Note that this may cause reentrancy through `chain::Watch::update_channel` calls or feerate
34703546
/// estimate fetches.
@@ -3523,6 +3599,8 @@ impl<Signer: Sign, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelMana
35233599
_ => {},
35243600
}
35253601

3602+
chan.maybe_expire_prev_config();
3603+
35263604
true
35273605
});
35283606

@@ -6115,6 +6193,7 @@ impl_writeable_tlv_based!(ChannelDetails, {
61156193
(4, counterparty, required),
61166194
(5, outbound_scid_alias, option),
61176195
(6, funding_txo, option),
6196+
(7, config, option),
61186197
(8, short_channel_id, option),
61196198
(10, channel_value_satoshis, required),
61206199
(12, unspendable_punishment_reserve, option),

lightning/src/ln/functional_test_utils.rs

Lines changed: 10 additions & 3 deletions
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);

0 commit comments

Comments
 (0)