Skip to content

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

Description

@Bonnai2112

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 <id>: 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:

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:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions