Skip to content

lightningd: default public htlc_maximum_msat to 25% of channel capacity - #9311

Draft
nGoline wants to merge 4 commits into
ElementsProject:masterfrom
nGoline:htlc-max-public-quarter-capacity
Draft

lightningd: default public htlc_maximum_msat to 25% of channel capacity#9311
nGoline wants to merge 4 commits into
ElementsProject:masterfrom
nGoline:htlc-max-public-quarter-capacity

Conversation

@nGoline

@nGoline nGoline commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Implements the "Oakland" privacy proposal from the Lightning Dev Summit (reaffirmed at the Vienna summit): for publicly announced channels, default the advertised htlc_maximum_msat to 25% of channel capacity (capped by what we can actually send) when the operator has not set --htlc-maximum-msat. A maximum well below the publicly-known capacity makes probing for where payments flow significantly harder.

Scope:

  • Only public (announced) channels are affected; private channels, whose capacity is not public, keep the full spendable default.
  • Only newly created channels: existing channels keep their stored value.
  • Operators can still raise it to the full spendable amount via --htlc-maximum-msat or setchannel.

Closes #9173

Andezion
Andezion previously approved these changes Jul 20, 2026

@Andezion Andezion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does htlc_maximum_msat get updated?
I looked at update_channel_from_inflight() in peer_control.c:2251. It updates funding_sats and the reserve after a splice, but I could not find where it also updates htlc_maximum_msat (or htlc_minimum_msat). I checked channel_gossip.c too and did not see anything there either

If I'm right - then

Splice-in (channel gets bigger) - htlc_maximum_msatstays the same old number. So it becomes a smaller and smaller part of the new, bigger capacity. This does not break any rule, but the channel ends up more limited than the 25% default was supposed to give, and the node owner has no way to know this happened. Also - if different channels end up with very different max/capacity numbers just because some were spliced and some were not, could that give away information too?

Splice-out (channel gets smaller) - could htlc_maximum_msat end up bigger than the new, smaller capacity? That would break the BOLT7 rule that says htlc_maximum_msat must be <= capacity. It looks like this would fix itself the next time lightningd restarts (because channel_htlc_maximum_default() checks and lowers the stored value again when the channel is loaded), or if someone runs setchannel. But until then, could a bad channel_update get sent out to the network?

What do you think?

Comment thread lightningd/channel.h
* 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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

channel_htlc_maximum_default() doesn't clamp the result against channel->htlc_minimum_msat. With the previous capacity default this was effectively unreachable, but with a 25% ofcapacity default it's now realistic for small public channels combined with a non-default --htlc-minimum-msat, producing a channel_update with htlc_maximum_msat < htlc_minimum_msat (channel becomes unroutable). Should this function also do deflt = amount_msat_max(deflt, channel->htlc_minimum_msat) before the final cap, or at least log a warning when that happens?

@Andezion
Andezion force-pushed the htlc-max-public-quarter-capacity branch from 9271abb to 16fbc32 Compare July 20, 2026 12:25
@madelinevibes madelinevibes added this to the v26.09 milestone Jul 31, 2026
nGoline added 4 commits August 7, 2026 08:50
…nels

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
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.
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.
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.
@nGoline
nGoline force-pushed the htlc-max-public-quarter-capacity branch from 16fbc32 to 97384b8 Compare August 7, 2026 13:40
@nGoline

nGoline commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

@Andezion thanks, both of your points were real.

On the first: channel_htlc_maximum_default() did not clamp against htlc_minimum_msat, and the value goes straight onto the wire in gossip_generation.c with nothing else guarding it (setchannel only clamps against htlc_max_possible_send()). BOLT#7 makes it a MUST in both directions, and receivers SHOULD ignore this channel during route considerations when it is violated. With the old capacity-minus-reserve default you needed htlc_minimum_msat above ~99% of capacity to trip it, so it was effectively unreachable; at 25% it is not. Fixed in the last commit, with a test.

On the second: you are right that update_channel_from_inflight() never updates htlc_maximum_msat, and right that a restart re-clamps it (the stored value goes back through min(stored, cap) on load). But that staleness is already present on master, where the default was capacity minus reserve and went just as stale; this PR changes the degree, not the kind. It is also not a one-liner, because once the default is computed it is stored, and the stored value is indistinguishable from an operator's explicit setchannel. The AMOUNT_MSAT(-1ULL) sentinel only lives in ld->config, so recomputing on splice would silently clobber user intent. That wants a separate PR and probably an "explicitly set" flag, so I would rather not fold it in here.

@nGoline

nGoline commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Regenerating the RPC doc examples fails, and not cosmetically. A 500,000sat payment over a ~1M sat channel can no longer be routed at all:

code 205: We could not find a usable set of paths. The shortest path is 116x1x1,
but 116x1x1/1 exceeds htlc_maximum_msat ~250085376msat

The cause is in plugins/askrene/child/mcf.c:

u64 cap_on_capacity =
    MIN(amount_msat_ratio_floor(gossmap_chan_htlc_max(c, dir), params->accuracy),
        amount_msat_ratio_ceil(params->amount, params->accuracy));

askrene bounds the total min-cost-flow through a channel direction by htlc_maximum_msat. But BOLT#7 defines that field as a limit on the maximum value it will send through this channel for a single HTLC, not a ceiling on what a payment may push through the channel in aggregate. So MPP cannot split around it: the constraint applies to the summed flow, not per part.

The practical effect is that this PR does not mean "each HTLC is at most 25% of capacity", it means "a channel carries at most 25% of its capacity per payment". That is what turned CI red here, rather than a handful of stale test constants.

@Lagrang3, as askrene owner, could you weigh in on whether that bound is deliberate? I can see an argument for it as a conservative proxy (a flow you cannot deliver as one HTLC is not obviously deliverable as several, given max_accepted_htlcs and in-flight limits), but as written it also constrains payments that would split perfectly well.

Three ways forward as I see it:

  1. Model htlc_max per-HTLC in askrene and allow the solver to split across it. This looks correct to me and is arguably a pre-existing limitation that this PR merely makes bite on every channel, but it would need to land first and it is your call whether that is tractable.
  2. Pick a less aggressive default, so it stays clear of realistic single payments. Unblocks immediately, weakens the privacy property.
  3. Accept the reduction and update the affected tests and examples. Large change, and it ships a real routing regression.

I lean towards 1, with 2 as the pragmatic fallback if the flow model is not something we want to touch now. Happy to do the work either way, but I do not want to guess at askrene's intent here.

Marking this as not ready until that is settled.

@nGoline
nGoline marked this pull request as draft August 7, 2026 14:04
@nGoline

nGoline commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

The latest run tells a story: 53 distinct tests fail across all six integration shards, all six ASan shards and eleven of twelve valgrind shards, and nearly every one is a payment or getroutes call rejected with exceeds htlc_maximum_msat ~250085376msat. The rest (test_coinmoves, test_parallel_channels_reserve asserting 'unpaid' == 'paid') are assertion mismatches downstream of payments that did not complete.

Two details that make me think this is a shared assumption rather than one stray line:

  • The clearest case is xpay with no maxparts constraint paying 500,000sat over a ~1M sat channel whose htlc_maximum_msat is ~250,000sat. Two parts of 250,000sat would fit comfortably, but the solver rejects it outright, so the bound really is on summed flow rather than per HTLC.
  • renepay fails on the same topologies with minflow couldn't find a feasible flow: failed to find a feasible flow: find_admissible_path failed, and test_renepay.py::test_htlc_max and test_htlcmax0 are among the failures. So renepay's flow solver appears to model htlc_maximum_msat the same way.

@Lagrang3
Lagrang3 self-requested a review August 7, 2026 19:00
@Lagrang3

Lagrang3 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

@nGoline, we had issues in the past trying many htlcs through the same channel with a low htlc_max.
The simplest solution was to limit the MCF capacity for channels to be at most the advertized htlc_max
(#7159).
Also @renepickhardt was a supporter of this measure for a different reason (#6905 (comment)).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reduce the default htlc_maximum_msat to 25% of channel capacity

4 participants