From 52b7f860fd2f84a805a3bc1750454241f0fe140e Mon Sep 17 00:00:00 2001 From: Amperstrand Date: Sat, 5 Sep 2026 22:35:09 +0200 Subject: [PATCH] openingd: fail open_channel at receipt when our dust limit exceeds their reserve BOLT #2 requires the accept_channel sender to set dust_limit_satoshis less than or equal to channel_reserve_satoshis from the open_channel message. fundee_channel() quotes this exact MUST in its comment block but implements no check for it: with the chainparams dust limit (546) and an opener reserve below that, CLN replies accept_channel with dust_limit_satoshis=546, violating the sender MUST. Spec-strict peers (e.g. an LDK opener at the 354-sat spec-minimum dust limit) then fail the channel, with nothing above DEBUG on our side recording why. Mirror the existing opener-side check (openingd.c:446-455) in the accepter path, respecting --dev-allowdustreserve like both neighboring checks. The in-suite test sends a true sub-546 reserve by running the opener under --dev-allowdustreserve (a stock fundchannel self-bumps to its dust limit, mirroring test_zeroreserve's construction); it fails on master and passes with this change. Changelog-Fixes: #9439 Fixes: #9439 --- openingd/openingd.c | 18 ++++++++++++++++++ tests/test_opening.py | 21 +++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/openingd/openingd.c b/openingd/openingd.c index a0585c6cfd9d..9af7e6c03da0 100644 --- a/openingd/openingd.c +++ b/openingd/openingd.c @@ -993,6 +993,24 @@ static u8 *fundee_channel(struct state *state, const u8 *open_channel_msg) return NULL; } + /* BOLT #2: + * + * The sender: + *... + * - MUST set `dust_limit_satoshis` less than or equal to + * `channel_reserve_satoshis` from the `open_channel` message. + */ + if (!state->allowdustreserve && + amount_sat_greater(state->localconf.dust_limit, + state->remoteconf.channel_reserve)) { + negotiation_failed(state, + "Our dust limit %s" + " would be above their reserve %s", + fmt_amount_sat(tmpctx, state->localconf.dust_limit), + fmt_amount_sat(tmpctx, state->remoteconf.channel_reserve)); + return NULL; + } + /* These checks are the same whether we're opener or accepter... */ if (!check_config_bounds(tmpctx, state->funding_sats, state->feerate_per_kw, diff --git a/tests/test_opening.py b/tests/test_opening.py index e23283f7251c..ce391632f65c 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -2266,6 +2266,27 @@ def test_zeroconf_multichan_forward(node_factory): .format(normal_scid, zeroconf_scid)) +def test_dust_limit_above_reserve(node_factory, bitcoind): + """BOLT #2: the accept_channel sender MUST set dust_limit_satoshis + to less than or equal to channel_reserve_satoshis from the + open_channel message (ElementsProject/lightning#9439). + + A stock opener self-bumps its reserve up to its own dust limit, so + the violating open is only reachable when the opener runs + --dev-allowdustreserve and sends its true sub-dust reserve: the + accepter's chainparams dust limit (546) then exceeds it, and the + accepter must fail the channel instead of replying accept_channel. + """ + l1 = node_factory.get_node(options={'dev-allowdustreserve': True}) + l2 = node_factory.get_node() + + l1.fundwallet(10**7) + l1.connect(l2) + + with pytest.raises(RpcError, match='would be above their reserve'): + l1.rpc.fundchannel(l2.info['id'], 10**5, reserve='354sat') + + def test_zeroreserve(node_factory, bitcoind): """Ensure we can set the reserves.