ElementsProject / ElementsProject/lightning
openingd: accept_channel violates BOLT 2 when the peer's channel_reserve_satoshis is below our dust limit (silent open failure)
- Dominant language
- C
- Stars
- 3.1k
- Forks
- 1k
- Avg merge
- 4d 10h
- Merged PRs (30d)
- 13
Description
### Summary
When a peer opens a channel to us and sets `channel_reserve_satoshis` below our own dust limit (546 sat), `openingd` does not fail the negotiation. It replies with an `accept_channel` carrying `dust_limit_satoshis = 546`, which violates a BOLT 2 *MUST* for the sender of `accept_channel`. A spec-compliant peer then has to fail the channel — as it *MUST* — and the operator sees no reason for it anywhere in our logs.
This is both a conformance bug and, in practice, an interop wall with LDK-based LSPs, whose default dust limit is 354 sat.
### Reproduction
Real capture, `log-level=io`, CLN **v25.02**, signet (Mutinynet), counterparty is an LSPS1 LSP with a 354 sat dust limit. The node runs a remote signer, which plays no part here — the negotiation ends before anything beyond basepoints is requested.
Their `open_channel` (`0x0020`):
```
funding_satoshis 150000
push_msat 0
dust_limit_satoshis 354
channel_reserve_satoshis 354 <-- reserve required from us
htlc_minimum_msat 1
feerate_per_kw 750
to_self_delay 144
max_accepted_htlcs 10
```
Our `accept_channel` (`0x0021`):
```
dust_limit_satoshis 546 <-- 546 > 354
channel_reserve_satoshis 1500
htlc_minimum_msat 0
minimum_depth 1
to_self_delay 6
max_accepted_htlcs 483
```
170 ms later they sent `WIRE_ERROR` and the negotiation was aborted:
```
openingd-chan#2: peer_in WIRE_OPEN_CHANNEL
openingd-chan#2: Setting their reserve to 1500sat
openingd-chan#2: peer_out WIRE_ACCEPT_CHANNEL
openingd-chan#2: billboard: Incoming channel: accepted, now waiting for them to create funding tx
openingd-chan#2: peer_in WIRE_ERROR
openingd-chan#2: aborted opening negotiation: They sent ERROR channel : funding failed due to internal error
openingd-chan#2: Status closed, but not exited. Killing
```
At `log-level=info` nothing at all is logged about the cause. From the operator's point of view the channel open simply never happens, repeatedly, with no diagnosable reason — that is what makes this worth reporting beyond the conformance point.
### The requirements
BOLT 2, sender of `accept_channel`:
> MUST set `dust_limit_satoshis` less than or equal to `channel_reserve_satoshis` from the `open_channel` message.
BOLT 2, receiver of `accept_channel`:
> if `channel_reserve_satoshis` from the `open_channel` message is less than `dust_limit_satoshis`: MUST fail the channel.
So the peer is not merely entitled to reject us, it is required to.
### Where it comes from
`openingd/openingd.c` has the symmetric check, gated by `allowdustreserve`:
```c
if (!state->allowdustreserve
&& amount_sat_greater(state->remoteconf.dust_limit,
state->localconf.channel_reserve))
negotiation_failed(state, "Our channel reserve %s"
" would be below their dust %s", ...);
```
but there appears to be no check in the other direction — our `dust_limit` against *their* `channel_reserve` — which is precisely the one BOLT 2 imposes on the message we are about to send. Still the case in v26.06.6, where `dust_limit` is also unchanged at 546 for every network in `bitcoin/chainparams.c`.
### Suggested minimal fix
Mirror the existing check in the fundee path, under the same `allowdustreserve` gate so opt-in setups keep their behaviour:
```c
if (!state->allowdustreserve
&& amount_sat_greater(state->localconf.dust_limit,
state->remoteconf.channel_reserve))
negotiation_failed(state, "Their channel reserve %s"
" would be below our dust %s", ...);
```
This does not make the channel open succeed — it cannot, see below — but it turns a silently invalid message into an explicit, diagnosable refusal.
### The broader question
Even with that check, no CLN node can complete an opening where the peer sets the client reserve to 354 sat, since 546 is a constant with no configuration knob. LDK's default dust limit is 354, and at least one LSPS1 provider sets the client reserve to exactly its own dust limit — which works for LDK clients and can never work for a Core Lightning one, at any channel size, since the comparison does not involve capacity.
Is keeping `dust_limit` fixed at 546 the intended long-term position? Would lowering it, or making it configurable within safe bounds, be considered for interop with LDK-based LSPs — or is the expectation that such providers raise the client reserve instead? Happy to take that reading back to the provider if the latter is the answer.
### Possibly related
`openingd/dualopend.c` has no reserve/dust cross-check and no `allowdustreserve`. In v2 the reserve is derived (1% of the total) rather than negotiated, so the `accept_channel` requirement does not map directly — but for a dual-funded channel under ~54 600 sat the derived reserve is itself below 546. I have not tested that path; flagging it in case it deserves a look.
### Versions
- Observed on v25.02 (signet), wire capture above.
- Code inspected on v26.06.6: same constant, same single-direction check.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in openingd/openingd.c at the existing reserve-versus-dust check and trace the fundee path that builds accept_channel. Confirm how the corresponding check should behave under allowdustreserve, then verify that a peer reserve below the local dust limit produces an explicit negotiation failure rather than an invalid accept_channel; the dualopend.c path is noted for separate review.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 72/100