Skip to content

Commit d4bfc2f

Browse files
committed
Add max dust exposure multiplier config knob
With fee rates rising dramatically in mid-April 2023, thresholds for what is considered dust have risen, often exceeding our previous dust exposure threshold of 5k sats. This causes all payments and HTLC forwards between 5k sats and new dust thresholds to fail. This commit changes our max dust exposure config knob from a fixed upper limit to a `MaxDustHTLCExposure` enum with an additional variant to allow setting our max dust exposure to a multiplier on the current high priority feerate. To remain backwards compatible we'll always write the fixed limit if it's set, or its default value in its currently reserved TLV. We also now write an odd optional TLV for the new enum, so that previous versions can safely ignore it upon downgrading, while allowing us to make use of the new type when it's written.
1 parent 0f2c4c0 commit d4bfc2f

File tree

6 files changed

+135
-38
lines changed

6 files changed

+135
-38
lines changed

lightning/src/ln/channel.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use crate::routing::gossip::NodeId;
4141
use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer, VecWriter};
4242
use crate::util::logger::Logger;
4343
use crate::util::errors::APIError;
44-
use crate::util::config::{UserConfig, ChannelConfig, LegacyChannelConfig, ChannelHandshakeConfig, ChannelHandshakeLimits};
44+
use crate::util::config::{UserConfig, ChannelConfig, LegacyChannelConfig, ChannelHandshakeConfig, ChannelHandshakeLimits, MaxDustHTLCExposure};
4545
use crate::util::scid_utils::scid_from_parts;
4646

4747
use crate::io;
@@ -1060,7 +1060,10 @@ impl<Signer: ChannelSigner> ChannelContext<Signer> {
10601060
}
10611061

10621062
pub fn get_max_dust_htlc_exposure_msat(&self) -> u64 {
1063-
self.config.options.max_dust_htlc_exposure_msat
1063+
match self.config.options.max_dust_htlc_exposure_msat {
1064+
MaxDustHTLCExposure::FixedLimitMsat(limit) => limit,
1065+
MaxDustHTLCExposure::FeeRateMultiplier(_) => 5_000_000,
1066+
}
10641067
}
10651068

10661069
/// Returns the previous [`ChannelConfig`] applied to this channel, if any.

lightning/src/ln/functional_test_utils.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use crate::util::scid_utils;
2727
use crate::util::test_utils;
2828
use crate::util::test_utils::{panicking, TestChainMonitor, TestScorer, TestKeysInterface};
2929
use crate::util::errors::APIError;
30-
use crate::util::config::UserConfig;
30+
use crate::util::config::{UserConfig, MaxDustHTLCExposure};
3131
use crate::util::ser::{ReadableArgs, Writeable};
3232

3333
use bitcoin::blockdata::block::{Block, BlockHeader};
@@ -2573,7 +2573,7 @@ pub fn test_default_channel_config() -> UserConfig {
25732573
default_config.channel_handshake_config.our_htlc_minimum_msat = 1000;
25742574
// When most of our tests were written, we didn't have the notion of a `max_dust_htlc_exposure_msat`,
25752575
// It now defaults to 5_000_000 msat; to avoid interfering with tests we bump it to 50_000_000 msat.
2576-
default_config.channel_config.max_dust_htlc_exposure_msat = 50_000_000;
2576+
default_config.channel_config.max_dust_htlc_exposure_msat = MaxDustHTLCExposure::FixedLimitMsat(50_000_000);
25772577
default_config
25782578
}
25792579

lightning/src/ln/functional_tests.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use crate::util::test_utils;
3535
use crate::util::errors::APIError;
3636
use crate::util::ser::{Writeable, ReadableArgs};
3737
use crate::util::string::UntrustedString;
38-
use crate::util::config::UserConfig;
38+
use crate::util::config::{UserConfig, MaxDustHTLCExposure};
3939

4040
use bitcoin::hash_types::BlockHash;
4141
use bitcoin::blockdata::script::{Builder, Script};
@@ -9530,7 +9530,7 @@ fn do_test_max_dust_htlc_exposure(dust_outbound_balance: bool, exposure_breach_e
95309530

95319531
let chanmon_cfgs = create_chanmon_cfgs(2);
95329532
let mut config = test_default_channel_config();
9533-
config.channel_config.max_dust_htlc_exposure_msat = 5_000_000; // default setting value
9533+
config.channel_config.max_dust_htlc_exposure_msat = MaxDustHTLCExposure::FixedLimitMsat(5_000_000); // default setting value
95349534
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
95359535
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config), None]);
95369536
let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs);
@@ -9574,20 +9574,21 @@ fn do_test_max_dust_htlc_exposure(dust_outbound_balance: bool, exposure_breach_e
95749574
let (mut route, payment_hash, _, payment_secret) =
95759575
get_route_and_payment_hash!(nodes[0], nodes[1], 1000);
95769576

9577-
let dust_buffer_feerate = {
9577+
let (dust_buffer_feerate, max_dust_htlc_exposure_msat) = {
95789578
let per_peer_state = nodes[0].node.per_peer_state.read().unwrap();
95799579
let chan_lock = per_peer_state.get(&nodes[1].node.get_our_node_id()).unwrap().lock().unwrap();
95809580
let chan = chan_lock.channel_by_id.get(&channel_id).unwrap();
9581-
chan.context.get_dust_buffer_feerate(None) as u64
9581+
(chan.context.get_dust_buffer_feerate(None) as u64,
9582+
chan.context.get_max_dust_htlc_exposure_msat())
95829583
};
95839584
let dust_outbound_htlc_on_holder_tx_msat: u64 = (dust_buffer_feerate * htlc_timeout_tx_weight(&channel_type_features) / 1000 + open_channel.dust_limit_satoshis - 1) * 1000;
9584-
let dust_outbound_htlc_on_holder_tx: u64 = config.channel_config.max_dust_htlc_exposure_msat / dust_outbound_htlc_on_holder_tx_msat;
9585+
let dust_outbound_htlc_on_holder_tx: u64 = max_dust_htlc_exposure_msat / dust_outbound_htlc_on_holder_tx_msat;
95859586

95869587
let dust_inbound_htlc_on_holder_tx_msat: u64 = (dust_buffer_feerate * htlc_success_tx_weight(&channel_type_features) / 1000 + open_channel.dust_limit_satoshis - 1) * 1000;
9587-
let dust_inbound_htlc_on_holder_tx: u64 = config.channel_config.max_dust_htlc_exposure_msat / dust_inbound_htlc_on_holder_tx_msat;
9588+
let dust_inbound_htlc_on_holder_tx: u64 = max_dust_htlc_exposure_msat / dust_inbound_htlc_on_holder_tx_msat;
95889589

95899590
let dust_htlc_on_counterparty_tx: u64 = 4;
9590-
let dust_htlc_on_counterparty_tx_msat: u64 = config.channel_config.max_dust_htlc_exposure_msat / dust_htlc_on_counterparty_tx;
9591+
let dust_htlc_on_counterparty_tx_msat: u64 = max_dust_htlc_exposure_msat / dust_htlc_on_counterparty_tx;
95919592

95929593
if on_holder_tx {
95939594
if dust_outbound_balance {
@@ -9652,13 +9653,13 @@ fn do_test_max_dust_htlc_exposure(dust_outbound_balance: bool, exposure_breach_e
96529653
// Outbound dust balance: 6399 sats
96539654
let dust_inbound_overflow = dust_inbound_htlc_on_holder_tx_msat * (dust_inbound_htlc_on_holder_tx + 1);
96549655
let dust_outbound_overflow = dust_outbound_htlc_on_holder_tx_msat * dust_outbound_htlc_on_holder_tx + dust_inbound_htlc_on_holder_tx_msat;
9655-
nodes[0].logger.assert_log("lightning::ln::channel".to_string(), format!("Cannot accept value that would put our exposure to dust HTLCs at {} over the limit {} on holder commitment tx", if dust_outbound_balance { dust_outbound_overflow } else { dust_inbound_overflow }, config.channel_config.max_dust_htlc_exposure_msat), 1);
9656+
nodes[0].logger.assert_log("lightning::ln::channel".to_string(), format!("Cannot accept value that would put our exposure to dust HTLCs at {} over the limit {} on holder commitment tx", if dust_outbound_balance { dust_outbound_overflow } else { dust_inbound_overflow }, max_dust_htlc_exposure_msat), 1);
96569657
} else {
96579658
// Outbound dust balance: 5200 sats
96589659
nodes[0].logger.assert_log("lightning::ln::channel".to_string(),
96599660
format!("Cannot accept value that would put our exposure to dust HTLCs at {} over the limit {} on counterparty commitment tx",
96609661
dust_htlc_on_counterparty_tx_msat * (dust_htlc_on_counterparty_tx - 1) + dust_htlc_on_counterparty_tx_msat + 1,
9661-
config.channel_config.max_dust_htlc_exposure_msat), 1);
9662+
max_dust_htlc_exposure_msat), 1);
96629663
}
96639664
} else if exposure_breach_event == ExposureEvent::AtUpdateFeeOutbound {
96649665
route.paths[0].hops.last_mut().unwrap().fee_msat = 2_500_000;

lightning/src/ln/onion_route_tests.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::ln::msgs::{ChannelMessageHandler, ChannelUpdate};
2626
use crate::ln::wire::Encode;
2727
use crate::util::ser::{Writeable, Writer};
2828
use crate::util::test_utils;
29-
use crate::util::config::{UserConfig, ChannelConfig};
29+
use crate::util::config::{UserConfig, ChannelConfig, MaxDustHTLCExposure};
3030
use crate::util::errors::APIError;
3131

3232
use bitcoin::hash_types::BlockHash;
@@ -1374,7 +1374,8 @@ fn test_phantom_dust_exposure_failure() {
13741374
// Set the max dust exposure to the dust limit.
13751375
let max_dust_exposure = 546;
13761376
let mut receiver_config = UserConfig::default();
1377-
receiver_config.channel_config.max_dust_htlc_exposure_msat = max_dust_exposure;
1377+
receiver_config.channel_config.max_dust_htlc_exposure_msat =
1378+
MaxDustHTLCExposure::FixedLimitMsat(max_dust_exposure);
13781379
receiver_config.channel_handshake_config.announced_channel = true;
13791380

13801381
let chanmon_cfgs = create_chanmon_cfgs(2);

lightning/src/util/config.rs

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,50 @@ impl Default for ChannelHandshakeLimits {
315315
}
316316
}
317317

318+
/// Options for how to set the max dust HTLC exposure allowed on a channel. See
319+
/// [`ChannelConfig::max_dust_htlc_exposure_msat`] for details.
320+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
321+
pub enum MaxDustHTLCExposure {
322+
/// This sets a fixed limit on the total dust exposure in millisatoshis. Setting this too low
323+
/// may prevent the sending or receipt of low-value HTLCs on high-traffic nodes, however this
324+
/// limit is very important to prevent stealing of large amounts of dust HTLCs by miners
325+
/// through [fee griefing
326+
/// attacks](https://lists.linuxfoundation.org/pipermail/lightning-dev/2020-May/002714.html).
327+
///
328+
/// Note that if the feerate increases significantly, without a manually increase
329+
/// to this maximum the channel may be unable to send/receive HTLCs between the maximum dust
330+
/// exposure and the new minimum value for HTLCs to be economically viable to claim.
331+
FixedLimitMsat(u64),
332+
/// This sets a multiplier on the estimated high priority feerate (sats/KW, as obtained from
333+
/// [`FeeEstimator`]) to determine the maximum allowed dust exposure. If this variant is used
334+
/// then the maximum dust exposure in millisatoshis is calculated as:
335+
/// `high_priority_feerate_per_kw * value`. For example, with our default value
336+
/// `FeeRateMultiplier(5000)`:
337+
///
338+
/// - For the minimum fee rate of 1 sat/vByte (250 sat/KW, although the minimum
339+
/// defaults to 253 sats/KW for rounding, see [`FeeEstimator`]), the max dust exposure would
340+
/// be 253 * 5000 = 1,265,000 msats.
341+
/// - For a fee rate of 30 sat/vByte (7500 sat/KW), the max dust exposure would be
342+
/// 7500 * 5000 = 37,500,000 msats.
343+
///
344+
/// This allows the maximum dust exposure to automatically scale with fee rate changes.
345+
///
346+
/// Note, if you're using a third-party fee estimator, this may leave you more exposed to a
347+
/// fee griefing attack, where your fee estimator may purposely overestimate the fee rate,
348+
/// causing you to accept more dust HTLCs than you would otherwise. Similarly, you may
349+
/// accept more dust than you would like if you're using a fee estimator that
350+
/// is evaluated at each block tick, such as Bitcoin Core, as the fee rate may change
351+
/// significantly between blocks.
352+
///
353+
/// [`FeeEstimator`]: crate::chain::chaininterface::FeeEstimator
354+
FeeRateMultiplier(u64),
355+
}
356+
357+
impl_writeable_tlv_based_enum!(MaxDustHTLCExposure, ;
358+
(1, FixedLimitMsat),
359+
(3, FeeRateMultiplier),
360+
);
361+
318362
/// Options which apply on a per-channel basis and may change at runtime or based on negotiation
319363
/// with our counterparty.
320364
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -374,13 +418,11 @@ pub struct ChannelConfig {
374418
/// (specifically the zero fee HTLC transaction variant), this threshold no longer takes into
375419
/// account the HTLC transaction fee as it is zero.
376420
///
377-
/// This limit is applied for sent, forwarded, and received HTLCs and limits the total
378-
/// exposure across all three types per-channel. Setting this too low may prevent the
379-
/// sending or receipt of low-value HTLCs on high-traffic nodes, and this limit is very
380-
/// important to prevent stealing of dust HTLCs by miners.
421+
/// The selected limit is applied for sent, forwarded, and received HTLCs and limits the total
422+
/// exposure across all three types per-channel.
381423
///
382-
/// Default value: 5_000_000 msat.
383-
pub max_dust_htlc_exposure_msat: u64,
424+
/// Default value: [`MaxDustHTLCExposure::FeeRateMultiplier`] with a multiplier of 5000.
425+
pub max_dust_htlc_exposure_msat: MaxDustHTLCExposure,
384426
/// The additional fee we're willing to pay to avoid waiting for the counterparty's
385427
/// `to_self_delay` to reclaim funds.
386428
///
@@ -466,32 +508,73 @@ impl Default for ChannelConfig {
466508
forwarding_fee_proportional_millionths: 0,
467509
forwarding_fee_base_msat: 1000,
468510
cltv_expiry_delta: 6 * 12, // 6 blocks/hour * 12 hours
469-
max_dust_htlc_exposure_msat: 5_000_000,
511+
max_dust_htlc_exposure_msat: MaxDustHTLCExposure::FeeRateMultiplier(5000),
470512
force_close_avoidance_max_fee_satoshis: 1000,
471513
accept_underpaying_htlcs: false,
472514
}
473515
}
474516
}
475517

476-
impl_writeable_tlv_based!(ChannelConfig, {
477-
(0, forwarding_fee_proportional_millionths, required),
478-
(1, accept_underpaying_htlcs, (default_value, false)),
479-
(2, forwarding_fee_base_msat, required),
480-
(4, cltv_expiry_delta, required),
481-
(6, max_dust_htlc_exposure_msat, required),
482-
// ChannelConfig serialized this field with a required type of 8 prior to the introduction of
483-
// LegacyChannelConfig. To make sure that serialization is not compatible with this one, we use
484-
// the next required type of 10, which if seen by the old serialization will always fail.
485-
(10, force_close_avoidance_max_fee_satoshis, required),
486-
});
518+
impl crate::util::ser::Writeable for ChannelConfig {
519+
fn write<W: crate::util::ser::Writer>(&self, writer: &mut W) -> Result<(), crate::io::Error> {
520+
let max_dust_htlc_exposure_msat_fixed_limit = match self.max_dust_htlc_exposure_msat {
521+
MaxDustHTLCExposure::FixedLimitMsat(limit) => limit,
522+
MaxDustHTLCExposure::FeeRateMultiplier(_) => 5_000_000,
523+
};
524+
write_tlv_fields!(writer, {
525+
(0, self.forwarding_fee_proportional_millionths, required),
526+
(1, self.accept_underpaying_htlcs, (default_value, false)),
527+
(2, self.forwarding_fee_base_msat, required),
528+
(3, Some(self.max_dust_htlc_exposure_msat), option),
529+
(4, self.cltv_expiry_delta, required),
530+
(6, max_dust_htlc_exposure_msat_fixed_limit, required),
531+
// ChannelConfig serialized this field with a required type of 8 prior to the introduction of
532+
// LegacyChannelConfig. To make sure that serialization is not compatible with this one, we use
533+
// the next required type of 10, which if seen by the old serialization will always fail.
534+
(10, self.force_close_avoidance_max_fee_satoshis, required),
535+
});
536+
Ok(())
537+
}
538+
}
539+
540+
impl crate::util::ser::Readable for ChannelConfig {
541+
fn read<R: crate::io::Read>(reader: &mut R) -> Result<Self, crate::ln::msgs::DecodeError> {
542+
let mut forwarding_fee_proportional_millionths = 0;
543+
let mut accept_underpaying_htlcs = false;
544+
let mut forwarding_fee_base_msat = 1000;
545+
let mut cltv_expiry_delta = 6 * 12;
546+
let mut max_dust_htlc_exposure_msat = 5_000_000;
547+
let mut max_dust_htlc_exposure_enum = None;
548+
let mut force_close_avoidance_max_fee_satoshis = 1000;
549+
read_tlv_fields!(reader, {
550+
(0, forwarding_fee_proportional_millionths, required),
551+
(1, accept_underpaying_htlcs, (default_value, false)),
552+
(2, forwarding_fee_base_msat, required),
553+
(3, max_dust_htlc_exposure_enum, option),
554+
(4, cltv_expiry_delta, required),
555+
(6, max_dust_htlc_exposure_msat, required),
556+
(10, force_close_avoidance_max_fee_satoshis, required),
557+
});
558+
let max_dust_htlc_exposure_msat = max_dust_htlc_exposure_enum
559+
.unwrap_or(MaxDustHTLCExposure::FixedLimitMsat(max_dust_htlc_exposure_msat));
560+
Ok(Self {
561+
forwarding_fee_proportional_millionths,
562+
accept_underpaying_htlcs,
563+
forwarding_fee_base_msat,
564+
cltv_expiry_delta,
565+
max_dust_htlc_exposure_msat,
566+
force_close_avoidance_max_fee_satoshis,
567+
})
568+
}
569+
}
487570

488571
/// A parallel struct to [`ChannelConfig`] to define partial updates.
489572
#[allow(missing_docs)]
490573
pub struct ChannelConfigUpdate {
491574
pub forwarding_fee_proportional_millionths: Option<u32>,
492575
pub forwarding_fee_base_msat: Option<u32>,
493576
pub cltv_expiry_delta: Option<u16>,
494-
pub max_dust_htlc_exposure_msat: Option<u64>,
577+
pub max_dust_htlc_exposure_msat: Option<MaxDustHTLCExposure>,
495578
pub force_close_avoidance_max_fee_satoshis: Option<u64>,
496579
}
497580

@@ -546,12 +629,17 @@ impl Default for LegacyChannelConfig {
546629

547630
impl crate::util::ser::Writeable for LegacyChannelConfig {
548631
fn write<W: crate::util::ser::Writer>(&self, writer: &mut W) -> Result<(), crate::io::Error> {
632+
let max_dust_htlc_exposure_msat_fixed_limit = match self.options.max_dust_htlc_exposure_msat {
633+
MaxDustHTLCExposure::FixedLimitMsat(limit) => limit,
634+
MaxDustHTLCExposure::FeeRateMultiplier(_) => 5_000_000,
635+
};
549636
write_tlv_fields!(writer, {
550637
(0, self.options.forwarding_fee_proportional_millionths, required),
551-
(1, self.options.max_dust_htlc_exposure_msat, (default_value, 5_000_000)),
638+
(1, max_dust_htlc_exposure_msat_fixed_limit, (default_value, 5_000_000)),
552639
(2, self.options.cltv_expiry_delta, required),
553640
(3, self.options.force_close_avoidance_max_fee_satoshis, (default_value, 1000)),
554641
(4, self.announced_channel, required),
642+
(5, Some(self.options.max_dust_htlc_exposure_msat), option),
555643
(6, self.commit_upfront_shutdown_pubkey, required),
556644
(8, self.options.forwarding_fee_base_msat, required),
557645
});
@@ -562,21 +650,25 @@ impl crate::util::ser::Writeable for LegacyChannelConfig {
562650
impl crate::util::ser::Readable for LegacyChannelConfig {
563651
fn read<R: crate::io::Read>(reader: &mut R) -> Result<Self, crate::ln::msgs::DecodeError> {
564652
let mut forwarding_fee_proportional_millionths = 0;
565-
let mut max_dust_htlc_exposure_msat = 5_000_000;
653+
let mut max_dust_htlc_exposure_msat_fixed_limit = 5_000_000;
566654
let mut cltv_expiry_delta = 0;
567655
let mut force_close_avoidance_max_fee_satoshis = 1000;
568656
let mut announced_channel = false;
569657
let mut commit_upfront_shutdown_pubkey = false;
570658
let mut forwarding_fee_base_msat = 0;
659+
let mut max_dust_htlc_exposure_enum = None;
571660
read_tlv_fields!(reader, {
572661
(0, forwarding_fee_proportional_millionths, required),
573-
(1, max_dust_htlc_exposure_msat, (default_value, 5_000_000u64)),
662+
(1, max_dust_htlc_exposure_msat_fixed_limit, (default_value, 5_000_000u64)),
574663
(2, cltv_expiry_delta, required),
575664
(3, force_close_avoidance_max_fee_satoshis, (default_value, 1000u64)),
576665
(4, announced_channel, required),
666+
(5, max_dust_htlc_exposure_enum, option),
577667
(6, commit_upfront_shutdown_pubkey, required),
578668
(8, forwarding_fee_base_msat, required),
579669
});
670+
let max_dust_htlc_exposure_msat = max_dust_htlc_exposure_enum
671+
.unwrap_or(MaxDustHTLCExposure::FixedLimitMsat(max_dust_htlc_exposure_msat_fixed_limit));
580672
Ok(Self {
581673
options: ChannelConfig {
582674
forwarding_fee_proportional_millionths,

lightning/src/util/ser_macros.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,7 @@ macro_rules! impl_writeable_tlv_based_enum {
981981
f()
982982
}),*
983983
$($tuple_variant_id => {
984-
Ok($st::$tuple_variant_name(Readable::read(reader)?))
984+
Ok($st::$tuple_variant_name($crate::util::ser::Readable::read(reader)?))
985985
}),*
986986
_ => {
987987
Err($crate::ln::msgs::DecodeError::UnknownRequiredFeature)

0 commit comments

Comments
 (0)