ElementsProject / ElementsProject/lightning

openingd: accept_channel violates BOLT 2 when the peer's channel_reserve_satoshis is below our dust limit (silent open failure)

Open
#9,439 1 comment 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.