From b35c7d4b985750f62678356ac70923498036e56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Mon, 13 Jul 2026 14:36:24 -0300 Subject: [PATCH 1/4] pytest: expect htlc_maximum_msat defaults for public and private channels The Oakland privacy proposal (Lightning Dev Summit) observes that probing for where payments went is much harder when the htlc maximum is well below the publicly-known channel capacity. Default public channels to 25% of capacity, and leave private channels, whose capacity is not public, at everything they can send. Assert both, update the setchannel tests to expect the new figure, and xfail until the next commit implements it. Changelog-None --- tests/test_pay.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/test_pay.py b/tests/test_pay.py index fdf09cda03f9..d643dd9342c3 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -2051,6 +2051,7 @@ def test_pay_routeboost(node_factory, bitcoind): assert ret['successful_parts'] == 1 +@pytest.mark.xfail(strict=True) def test_setchannel_usage(node_factory, bitcoind): # TEST SETUP # @@ -2069,8 +2070,8 @@ def test_setchannel_usage(node_factory, bitcoind): DEF_BASE = 10 DEF_BASE_MSAT = Millisatoshi(DEF_BASE) DEF_PPM = 100 - # Minus reserve - MAX_HTLC = Millisatoshi(int(FUNDAMOUNT * 1000 * 0.99)) + # Public channels default htlc_maximum_msat to 25% of capacity. + MAX_HTLC = Millisatoshi(int(FUNDAMOUNT * 1000 * 0.25)) l1, l2, l3 = node_factory.get_nodes(3, opts={'fee-base': DEF_BASE, 'fee-per-satoshi': DEF_PPM}) @@ -2089,7 +2090,7 @@ def channel_get_config(scid): db_fees = l1.db_query('SELECT feerate_base, feerate_ppm, htlc_maximum_msat FROM channels;') assert(db_fees[0]['feerate_base'] == DEF_BASE) assert(db_fees[0]['feerate_ppm'] == DEF_PPM) - # This will be the capacity - reserves: + # This will be 25% of the capacity: assert(db_fees[0]['htlc_maximum_msat'] == MAX_HTLC) # this is also what listpeers should return channel = only_one(l1.rpc.listpeerchannels()['channels']) @@ -2580,6 +2581,29 @@ def test_setchannel_startup_opts(node_factory, bitcoind): assert result[1]['htlc_maximum_msat'] == Millisatoshi(5) +@pytest.mark.xfail(strict=True) +def test_htlc_maximum_msat_default(node_factory, bitcoind): + """Public channels default htlc_maximum_msat to 25% of capacity, private + channels to everything we can send""" + # A public channel's capacity is known from the funding output, so we + # advertise well below it to make probing harder. + PUBLIC_MAX = Millisatoshi(int(FUNDAMOUNT * 1000 * 0.25)) + l1, l2 = node_factory.line_graph(2, wait_for_announce=True) + + scid = only_one(l1.rpc.listpeerchannels()['channels'])['short_channel_id'] + assert only_one(l1.rpc.listpeerchannels()['channels'])['maximum_htlc_out_msat'] == PUBLIC_MAX + # Both directions default the same way. + wait_for(lambda: [c['htlc_maximum_msat'] for c in l1.rpc.listchannels(scid)['channels']] == [PUBLIC_MAX, PUBLIC_MAX]) + + # A private channel has no publicly-known capacity to correlate against, + # so we advertise everything we can send: capacity minus their reserve. + PRIVATE_MAX = Millisatoshi(int(FUNDAMOUNT * 1000 * 0.99)) + l3, l4 = node_factory.line_graph(2, announce_channels=False) + + assert only_one(l3.rpc.listpeerchannels()['channels'])['maximum_htlc_out_msat'] == PRIVATE_MAX + assert only_one(l4.rpc.listpeerchannels()['channels'])['maximum_htlc_out_msat'] == PRIVATE_MAX + + @pytest.mark.parametrize("anchors", [False, True]) def test_channel_spendable(node_factory, bitcoind, anchors): """Test that spendable_msat is accurate""" From f9832dd6fd2919cd2545ee1f99b51f24f017f8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Mon, 13 Jul 2026 14:46:16 -0300 Subject: [PATCH 2/4] lightningd: default public htlc_maximum_msat to 25% of channel capacity For publicly announced channels, when --htlc-maximum-msat is not set, advertise 25% of the channel capacity (capped by what we can actually send) rather than the full capacity: a maximum well below the publicly known capacity makes probing for where payments flow much harder. Private channels, whose capacity is not public, keep the full default. Changelog-Changed: Config: public channels now default `htlc-maximum-msat` to 25% of capacity (was full capacity), for better payment privacy. --- doc/lightningd-config.5.md | 18 +++++++++++++--- lightningd/channel.c | 38 +++++++++++++++++++++++++++------- lightningd/channel.h | 7 +++++++ lightningd/dual_open_control.c | 6 ++++-- tests/test_pay.py | 10 +++++---- 5 files changed, 63 insertions(+), 16 deletions(-) diff --git a/doc/lightningd-config.5.md b/doc/lightningd-config.5.md index aaa3e8fb8bc7..ba69bae4ee2b 100644 --- a/doc/lightningd-config.5.md +++ b/doc/lightningd-config.5.md @@ -415,9 +415,21 @@ RPC call lightning-setchannel(7). * **htlc-maximum-msat**=*MILLISATOSHI* - Default: unset (no limit). Sets the maximum allowed HTLC value for newly created -channels. If you want to change the `htlc_maximum_msat` for existing channels, -use the RPC call lightning-setchannel(7). + Sets the maximum allowed HTLC value for newly created channels. If unset +(the default), public channels advertise 25% of the channel capacity: a +maximum well below the publicly-known capacity makes it harder to probe for +where payments are flowing. Private channels, whose capacity is not public, +advertise the full amount they can send. Either way this is capped at what we +can actually send, and an explicit value here overrides the default (up to +that cap). If you want to change the `htlc_maximum_msat` for existing +channels, use the RPC call lightning-setchannel(7), for example: + + lightning-cli listpeerchannels | \ + jq -r '.channels[] | select(.private == false and .short_channel_id) | + "\(.short_channel_id) \((.total_msat // 0) / 4 | floor)"' | \ + while read scid max; do + lightning-cli setchannel "$scid" htlcmax="${max}msat" + done * **announce-addr-discovered**=*BOOL* diff --git a/lightningd/channel.c b/lightningd/channel.c index 692d81980150..0e47808bfd97 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -489,6 +489,34 @@ struct amount_msat htlc_max_possible_send(const struct channel *channel) return lower_bound_msat; } +struct amount_msat channel_htlc_maximum_default(const struct channel *channel, + struct amount_msat configured_max) +{ + struct amount_msat cap = htlc_max_possible_send(channel); + struct amount_msat deflt; + + /* If the operator set --htlc-maximum-msat, honour it (the sentinel + * AMOUNT_MSAT(-1ULL) means "unset"). Otherwise pick a default. */ + if (!amount_msat_eq(configured_max, AMOUNT_MSAT(-1ULL))) + deflt = configured_max; + else if (channel->channel_flags & CHANNEL_FLAGS_ANNOUNCE_CHANNEL) { + /* Oakland privacy proposal (Lightning Dev Summit): probing for + * where payments went is much harder if the htlc maximum is + * well below the channel capacity. For public channels the + * capacity is known from the funding output, so default to 25% + * of it. The spec only requires htlc_maximum_msat <= capacity. */ + if (!amount_sat_to_msat(&deflt, channel->funding_sats)) + return cap; + deflt = amount_msat_div(deflt, 4); + } else + /* Private channels have no publicly-known capacity to correlate + * against, so we advertise the full amount we can send. */ + deflt = cap; + + /* Never advertise more than we could actually send. */ + return amount_msat_min(deflt, cap); +} + struct channel *new_channel(struct peer *peer, u64 dbid, /* NULL or stolen */ struct wallet_shachain *their_shachain, @@ -567,7 +595,7 @@ struct channel *new_channel(struct peer *peer, u64 dbid, bool withheld) { struct channel *channel = tal(peer->ld, struct channel); - struct amount_msat htlc_min, htlc_max; + struct amount_msat htlc_min; bool anysegwit = !chainparams->is_elements && feature_negotiated(peer->ld->our_features, peer->their_features, @@ -694,11 +722,8 @@ struct channel *new_channel(struct peer *peer, u64 dbid, channel->htlc_minimum_msat = htlc_min; else channel->htlc_minimum_msat = htlc_minimum_msat; - htlc_max = htlc_max_possible_send(channel); - if (amount_msat_less(htlc_max, htlc_maximum_msat)) - channel->htlc_maximum_msat = htlc_max; - else - channel->htlc_maximum_msat = htlc_maximum_msat; + channel->htlc_maximum_msat = channel_htlc_maximum_default(channel, + htlc_maximum_msat); list_add_tail(&peer->channels, &channel->list); channel->rr_number = peer->ld->rr_counter++; @@ -1350,4 +1375,3 @@ const u8 *channel_update_for_error(const tal_t *ctx, return channel_gossip_update_for_error(ctx, channel); } - diff --git a/lightningd/channel.h b/lightningd/channel.h index 8e3a499ad09c..7c4bf77eb12b 100644 --- a/lightningd/channel.h +++ b/lightningd/channel.h @@ -977,6 +977,13 @@ const u8 *channel_update_for_error(const tal_t *ctx, struct amount_msat htlc_max_possible_send(const struct channel *channel); +/* Default htlc_maximum_msat to advertise for a new channel, given the + * configured --htlc-maximum-msat (AMOUNT_MSAT(-1ULL) if unset). Public + * channels default to 25% of capacity for privacy; private channels and an + * explicit setting use the full amount, all capped at what we can send. */ +struct amount_msat channel_htlc_maximum_default(const struct channel *channel, + struct amount_msat configured_max); + /* Given features, what channel_type do we want? */ struct channel_type *desired_channel_type(const tal_t *ctx, const struct feature_set *our_features, diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index 7c440e59098b..cf3c0b81c81d 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -1255,7 +1255,8 @@ wallet_update_channel(struct lightningd *ld, channel->msat_to_us_max = our_msat; channel->lease_expiry = lease_expiry; channel->htlc_minimum_msat = channel->channel_info.their_config.htlc_minimum; - channel->htlc_maximum_msat = htlc_max_possible_send(channel); + channel->htlc_maximum_msat = channel_htlc_maximum_default(channel, + ld->config.htlc_maximum_msat); tal_free(channel->lease_commit_sig); channel->lease_commit_sig = tal_steal(channel, lease_commit_sig); @@ -1486,7 +1487,8 @@ wallet_commit_channel(struct lightningd *ld, channel->lease_chan_max_msat = lease_chan_max_msat; channel->lease_chan_max_ppt = lease_chan_max_ppt; channel->htlc_minimum_msat = channel_info->their_config.htlc_minimum; - channel->htlc_maximum_msat = htlc_max_possible_send(channel); + channel->htlc_maximum_msat = channel_htlc_maximum_default(channel, + ld->config.htlc_maximum_msat); /* Filled in when we have PSBT for inflight */ channel->funding_psbt = NULL; diff --git a/tests/test_pay.py b/tests/test_pay.py index d643dd9342c3..1b16388efc1b 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -2051,7 +2051,6 @@ def test_pay_routeboost(node_factory, bitcoind): assert ret['successful_parts'] == 1 -@pytest.mark.xfail(strict=True) def test_setchannel_usage(node_factory, bitcoind): # TEST SETUP # @@ -2307,7 +2306,8 @@ def test_setchannel_routing(node_factory, bitcoind): # - htlc max is honored DEF_BASE = 1 DEF_PPM = 10 - MAX_HTLC = Millisatoshi(int(FUNDAMOUNT * 1000 * 0.99)) + # Public channels default htlc_maximum_msat to 25% of capacity. + MAX_HTLC = Millisatoshi(int(FUNDAMOUNT * 1000 * 0.25)) MIN_HTLC = Millisatoshi(0) l1, l2, l3 = node_factory.line_graph( @@ -2419,6 +2419,8 @@ def test_setchannel_zero(node_factory, bitcoind): # - payment can be done using zero fees DEF_BASE = 1 DEF_PPM = 10 + # This is the cap (capacity minus reserve) that setchannel clamps to, + # not the default: below we try to set htlcmax above it. MAX_HTLC = Millisatoshi(int(FUNDAMOUNT * 1000 * 0.99)) l1, l2, l3 = node_factory.line_graph( @@ -2470,7 +2472,8 @@ def test_setchannel_restart(node_factory, bitcoind): DEF_BASE = 1 DEF_PPM = 10 MIN_HTLC = Millisatoshi(0) - MAX_HTLC = Millisatoshi(int(FUNDAMOUNT * 1000 * 0.99)) + # Public channels default htlc_maximum_msat to 25% of capacity. + MAX_HTLC = Millisatoshi(int(FUNDAMOUNT * 1000 * 0.25)) OPTS = {'may_reconnect': True, 'fee-base': DEF_BASE, 'fee-per-satoshi': DEF_PPM} l1, l2, l3 = node_factory.line_graph(3, announce_channels=True, wait_for_announce=True, opts=OPTS) @@ -2581,7 +2584,6 @@ def test_setchannel_startup_opts(node_factory, bitcoind): assert result[1]['htlc_maximum_msat'] == Millisatoshi(5) -@pytest.mark.xfail(strict=True) def test_htlc_maximum_msat_default(node_factory, bitcoind): """Public channels default htlc_maximum_msat to 25% of capacity, private channels to everything we can send""" From 7c336293f827961f905656841c440135939dc43f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Fri, 7 Aug 2026 08:20:51 -0300 Subject: [PATCH 3/4] pytest: htlc_maximum_msat must not fall below htlc_minimum_msat The 25% default is computed without reference to htlc_minimum_msat, so a node configured with an htlc-minimum-msat above a quarter of the channel capacity advertises a channel_update with htlc_maximum_msat below htlc_minimum_msat. Peers SHOULD ignore such a channel when routing, so the channel silently becomes unusable. Previously the default was capacity minus reserve, which made this effectively unreachable; at 25% it is not. --- tests/test_pay.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_pay.py b/tests/test_pay.py index 1b16388efc1b..a51320b9fb6f 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -2606,6 +2606,20 @@ def test_htlc_maximum_msat_default(node_factory, bitcoind): assert only_one(l4.rpc.listpeerchannels()['channels'])['maximum_htlc_out_msat'] == PRIVATE_MAX +@pytest.mark.xfail(strict=True) +def test_htlc_maximum_msat_not_below_minimum(node_factory, bitcoind): + """BOLT #7 requires htlc_maximum_msat >= htlc_minimum_msat, so the 25% + public default must not undercut a higher htlc-minimum-msat""" + # 25% of capacity is 250000000msat, so this minimum sits above the default. + HTLC_MIN = Millisatoshi(300000000) + l1, l2 = node_factory.line_graph(2, opts={'htlc-minimum-msat': HTLC_MIN}) + + for n in (l1, l2): + chan = only_one(n.rpc.listpeerchannels()['channels']) + assert chan['minimum_htlc_out_msat'] == HTLC_MIN + assert chan['maximum_htlc_out_msat'] >= chan['minimum_htlc_out_msat'] + + @pytest.mark.parametrize("anchors", [False, True]) def test_channel_spendable(node_factory, bitcoind, anchors): """Test that spendable_msat is accurate""" From 97384b8889135b88facc8373d74652748a04c428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Fri, 7 Aug 2026 08:42:40 -0300 Subject: [PATCH 4/4] lightningd: never default htlc_maximum_msat below htlc_minimum_msat Floor the default at channel->htlc_minimum_msat before capping it at what we can send. A routable channel is worth more than the privacy margin the 25% default buys, and the spec makes it a MUST either way. Where htlc_minimum_msat exceeds what we can send at all, no valid value exists; log it rather than advertising an unroutable channel silently. Also route the funding_sats overflow path through the same floor and cap instead of returning early. --- doc/lightningd-config.5.md | 4 +++- lightningd/channel.c | 29 ++++++++++++++++++++++++++--- lightningd/channel.h | 4 +++- tests/test_pay.py | 1 - 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/doc/lightningd-config.5.md b/doc/lightningd-config.5.md index ba69bae4ee2b..d8490de9ab74 100644 --- a/doc/lightningd-config.5.md +++ b/doc/lightningd-config.5.md @@ -421,7 +421,9 @@ maximum well below the publicly-known capacity makes it harder to probe for where payments are flowing. Private channels, whose capacity is not public, advertise the full amount they can send. Either way this is capped at what we can actually send, and an explicit value here overrides the default (up to -that cap). If you want to change the `htlc_maximum_msat` for existing +that cap). It is also raised to `htlc_minimum_msat` where that is higher, +since the spec requires the maximum to be at least the minimum. If you want +to change the `htlc_maximum_msat` for existing channels, use the RPC call lightning-setchannel(7), for example: lightning-cli listpeerchannels | \ diff --git a/lightningd/channel.c b/lightningd/channel.c index 0e47808bfd97..698c972fcf09 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -504,15 +504,38 @@ struct amount_msat channel_htlc_maximum_default(const struct channel *channel, * where payments went is much harder if the htlc maximum is * well below the channel capacity. For public channels the * capacity is known from the funding output, so default to 25% - * of it. The spec only requires htlc_maximum_msat <= capacity. */ + * of it. */ if (!amount_sat_to_msat(&deflt, channel->funding_sats)) - return cap; - deflt = amount_msat_div(deflt, 4); + deflt = cap; + else + deflt = amount_msat_div(deflt, 4); } else /* Private channels have no publicly-known capacity to correlate * against, so we advertise the full amount we can send. */ deflt = cap; + /* BOLT #7: + * + * - MUST set `htlc_maximum_msat` to the maximum value it will send through this channel for a single HTLC. + * - MUST set this to less than or equal to the channel capacity. + * - MUST set this to less than or equal to `max_htlc_value_in_flight_msat` it received from the peer. + * - MUST set this to greater than or equal to `htlc_minimum_msat`. + */ + /* A quarter of the capacity can land below a high htlc_minimum_msat: + * a routable channel beats the stronger privacy margin. */ + if (amount_msat_less(deflt, channel->htlc_minimum_msat)) + deflt = channel->htlc_minimum_msat; + + /* htlc_max_possible_send() covers both upper bounds above. If it is + * itself below htlc_minimum_msat then no value satisfies the spec, so + * say so rather than advertising an unroutable channel silently. */ + if (amount_msat_less(cap, channel->htlc_minimum_msat)) + log_unusual(channel->log, + "htlc_minimum_msat %s exceeds the most we can send" + " (%s): channel_update will not be routable", + fmt_amount_msat(tmpctx, channel->htlc_minimum_msat), + fmt_amount_msat(tmpctx, cap)); + /* Never advertise more than we could actually send. */ return amount_msat_min(deflt, cap); } diff --git a/lightningd/channel.h b/lightningd/channel.h index 7c4bf77eb12b..bfc7302d08c5 100644 --- a/lightningd/channel.h +++ b/lightningd/channel.h @@ -980,7 +980,9 @@ struct amount_msat htlc_max_possible_send(const struct channel *channel); /* Default htlc_maximum_msat to advertise for a new channel, given the * configured --htlc-maximum-msat (AMOUNT_MSAT(-1ULL) if unset). Public * channels default to 25% of capacity for privacy; private channels and an - * explicit setting use the full amount, all capped at what we can send. */ + * explicit setting use the full amount, all capped at what we can send and + * floored at channel->htlc_minimum_msat (which must therefore already be + * set). */ struct amount_msat channel_htlc_maximum_default(const struct channel *channel, struct amount_msat configured_max); diff --git a/tests/test_pay.py b/tests/test_pay.py index a51320b9fb6f..2b7035f302d5 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -2606,7 +2606,6 @@ def test_htlc_maximum_msat_default(node_factory, bitcoind): assert only_one(l4.rpc.listpeerchannels()['channels'])['maximum_htlc_out_msat'] == PRIVATE_MAX -@pytest.mark.xfail(strict=True) def test_htlc_maximum_msat_not_below_minimum(node_factory, bitcoind): """BOLT #7 requires htlc_maximum_msat >= htlc_minimum_msat, so the 25% public default must not undercut a higher htlc-minimum-msat"""