From 04840a37eaabc92df055998eec8c75b8e27c246f Mon Sep 17 00:00:00 2001 From: Amperstrand Date: Sat, 5 Sep 2026 22:35:09 +0200 Subject: [PATCH 1/2] 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. From 0ef2286179e561d08e6d7a0021351b52d892d2da Mon Sep 17 00:00:00 2001 From: Amperstrand Date: Tue, 15 Sep 2026 17:42:12 +0200 Subject: [PATCH 2/2] tests: allow l2 a sub-dust reserve in test_zeroreserve_mixed The new accepter-side check (dust_limit <= their reserve) rejects l1's 0sat-reserve open at l2, whose dust limit is the chainparams 546 -- and a stock accepter has no legal dust_limit against a 0sat reserve at all (the spec floors dust at 354). l2 sets --dev-allowdustreserve, the same crutch its sibling tests already use on every node; mixed/all zeroreserve tests pass on both networks. Changelog-None: test-only Signed-off-by: Amperstrand --- tests/test_opening.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_opening.py b/tests/test_opening.py index ce391632f65c..cd6e38ccf04a 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -2379,7 +2379,9 @@ def test_zeroreserve_mixed(node_factory, bitcoind): 'reserve': '0sat', 'dev-allowdustreserve': True, }, { - 'dev-allowdustreserve': False, + # l2 accepts l1's zero reserve, which is below l2's dust + # limit: spec-illegal unless allowed for testing. + 'dev-allowdustreserve': True, }, { 'dev-allowdustreserve': False, }