From db75048289c5f5c9afb2aaf155da7104086cdf17 Mon Sep 17 00:00:00 2001 From: enaples Date: Mon, 9 Feb 2026 14:20:29 +1030 Subject: [PATCH 1/5] tests: test reproduce #8863 [ Neated and added xfail -- RR ] --- tests/test_opening.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_opening.py b/tests/test_opening.py index a8090f9ac697..fb4f2a984d89 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -13,6 +13,35 @@ import time +@pytest.mark.xfail(strict=True) +@unittest.skipIf(TEST_NETWORK != 'regtest', "requires regtest") +def test_opening_dualfund_with_unknown_feerates(node_factory, bitcoind): + """ + Test dualfund openchannel when feerates are unknown (like on signet/testnet with empty mempool). + """ + opts = { + 'ignore-fee-limits': True, + 'feerates': None, + 'dev-no-fake-fees': True, + 'experimental-dual-fund': None + } + + l1, l2 = node_factory.get_nodes(2, opts=opts) + + l1.fundwallet(FUNDAMOUNT) + + # Connect peers + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + + # Verify fee estimation is failing + l1.daemon.wait_for_log('Unable to estimate any fees') + l2.daemon.wait_for_log('Unable to estimate any fees') + + # Open channel l1 <-> l2 + l1.rpc.fundchannel(l2.info['id'], 100000, feerate='253perkw', minconf=0)['txid'] + l2.rpc.listpeerchannels(l1.info['id'])['channels'] + + def find_next_feerate(node, peer): chan = only_one(node.rpc.listpeerchannels(peer.info['id'])['channels']) return chan['next_feerate'] From 03c2e6c3ee1003e224cad4ef31c0a786fb79d0cd Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 9 Feb 2026 14:20:55 +1030 Subject: [PATCH 2/5] pytest: test that we refuse incoming open requests if we have no feerates. Signed-off-by: Rusty Russell --- tests/test_opening.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_opening.py b/tests/test_opening.py index fb4f2a984d89..ab29e2efaec5 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -2981,3 +2981,29 @@ def test_zeroconf_withhold(node_factory, bitcoind, stay_withheld, mutual_close): wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['state'] == 'CLOSINGD_COMPLETE') else: wait_for(lambda: only_one(l1.rpc.listpeerchannels()['channels'])['state'] == 'AWAITING_UNILATERAL') + + +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +@pytest.mark.xfail(strict=True) +def test_opening_incoming_unknown_feerates(node_factory, bitcoind): + """ + Don't allow incoming channels if we can't estimate feerates. + """ + nofee_opts = {'ignore-fee-limits': True, + 'feerates': None, + 'dev-no-fake-fees': True} + + l1, l2 = node_factory.get_nodes(2, opts=[{}, nofee_opts]) + + l1.fundwallet(FUNDAMOUNT) + + # Connect peers + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + + # Verify fee estimation is failing + l2.daemon.wait_for_log('Unable to estimate any fees') + + # Open channel l1 <-> l2: l2 should refuse! + with pytest.raises(RpcError, match=r'They sent.*Cannot accept channel: feerates unknown'): + l1.rpc.fundchannel(l2.info['id'], 100000) From e91310226f7f4c3bef41de91522a98101da1e637 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 9 Feb 2026 14:22:08 +1030 Subject: [PATCH 3/5] lightningd: refuse to let a peer open a channel if we have no fee estimates. This seems a sensible precaution anyway. Changelog-Changed: lightningd: we don't allow new incoming channels if we cannot estimate fees (rather than assuming minfee). Signed-off-by: Rusty Russell --- lightningd/chaintopology.c | 5 +++++ lightningd/chaintopology.h | 3 +++ lightningd/dual_open_control.c | 9 +++++++++ lightningd/opening_control.c | 10 ++++++++++ 4 files changed, 27 insertions(+) diff --git a/lightningd/chaintopology.c b/lightningd/chaintopology.c index 6ab4c659706b..b73acf53e31a 100644 --- a/lightningd/chaintopology.c +++ b/lightningd/chaintopology.c @@ -381,6 +381,11 @@ static void watch_for_unconfirmed_txs(struct lightningd *ld, /* Mutual recursion via timer. */ static void next_updatefee_timer(struct chain_topology *topo); +bool unknown_feerates(const struct chain_topology *topo) +{ + return tal_count(topo->feerates[0]) == 0; +} + static u32 interp_feerate(const struct feerate_est *rates, u32 blockcount) { const struct feerate_est *before = NULL, *after = NULL; diff --git a/lightningd/chaintopology.h b/lightningd/chaintopology.h index 8515cb5bdaf3..ff0a448bdad3 100644 --- a/lightningd/chaintopology.h +++ b/lightningd/chaintopology.h @@ -183,6 +183,9 @@ u32 smoothed_feerate_for_deadline(const struct chain_topology *topo, u32 blockco /* Get feerate to hit this *block number*. */ u32 feerate_for_target(const struct chain_topology *topo, u64 deadline); +/* Has our feerate estimation failed altogether? */ +bool unknown_feerates(const struct chain_topology *topo); + /* Get range of feerates to insist other side abide by for normal channels. * If we have to guess, sets *unknown to true, otherwise false. */ u32 feerate_min(struct lightningd *ld, bool *unknown); diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index ed75f5d71193..502dfdf350b7 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -2090,6 +2090,15 @@ static void accepter_got_offer(struct subd *dualopend, return; } + /* Don't allow opening if we don't know any fees; even if + * ignore-feerates is set. */ + if (unknown_feerates(dualopend->ld->topology)) { + subd_send_msg(dualopend, + take(towire_dualopend_fail(NULL, "Cannot accept channel: feerates unknown"))); + tal_free(payload); + return; + } + /* As a convenience to the plugin, we provide our current known * min + max feerates. Ideally, the plugin will fail to * contribute funds if the peer's feerate range is outside of diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index ae302630f071..c6f06071e29e 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -871,6 +871,16 @@ static void opening_got_offer(struct subd *openingd, return; } + /* Don't allow opening if we don't know any fees; even if + * ignore-feerates is set. */ + if (unknown_feerates(openingd->ld->topology)) { + subd_send_msg(openingd, + take(towire_openingd_got_offer_reply(NULL, "Cannot accept channel: feerates unknown", + NULL, NULL, NULL, 0))); + tal_free(payload); + return; + } + tal_add_destructor2(openingd, openchannel_payload_remove_openingd, payload); plugin_hook_call_openchannel(openingd->ld, NULL, payload); } From c6d3123d8fe092d95ecf80f89afcd96e5cc80941 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 9 Feb 2026 14:24:05 +1030 Subject: [PATCH 4/5] lightningd: don't hand 0 to dualopend as anchor feerate if we cannot estimate fees. Fixes: https://github.com/ElementsProject/lightning/issues/8847 Reported-by: https://github.com/mdance Changelog-EXPERIMENTAL: fixed crash with dual funding if we cannot estimate fees. --- lightningd/dual_open_control.c | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index 502dfdf350b7..81af3906496d 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -3055,6 +3055,7 @@ static struct command_result *openchannel_init(struct command *cmd, { u32 *our_upfront_shutdown_script_wallet_index; u32 found_wallet_index; + u32 anchor_feerate; struct channel *channel; struct open_attempt *oa; int fds[2]; @@ -3102,12 +3103,19 @@ static struct command_result *openchannel_init(struct command *cmd, } else our_upfront_shutdown_script_wallet_index = NULL; + /* 0 from this means "unknown" */ + anchor_feerate = unilateral_feerate(cmd->ld->topology, true); + if (anchor_feerate == 0) { + anchor_feerate = get_feerate_floor(cmd->ld->topology); + assert(anchor_feerate); + } + oa->open_msg = towire_dualopend_opener_init(oa, psbt, amount, oa->our_upfront_shutdown_script, our_upfront_shutdown_script_wallet_index, feerate_per_kw, - unilateral_feerate(cmd->ld->topology, true), + anchor_feerate, feerate_per_kw_funding, channel->channel_flags, amount_sat_is_zero(request_amt) ? @@ -3812,7 +3820,7 @@ static struct command_result *json_queryrates(struct command *cmd, struct peer *peer; struct channel *channel; u32 *feerate_per_kw_funding; - u32 *feerate_per_kw; + u32 *feerate_per_kw, anchor_feerate; struct amount_sat *amount, *request_amt; struct wally_psbt *psbt; struct open_attempt *oa; @@ -3900,12 +3908,19 @@ static struct command_result *json_queryrates(struct command *cmd, } else our_upfront_shutdown_script_wallet_index = NULL; + /* 0 from this means "unknown" */ + anchor_feerate = unilateral_feerate(cmd->ld->topology, true); + if (anchor_feerate == 0) { + anchor_feerate = get_feerate_floor(cmd->ld->topology); + assert(anchor_feerate); + } + oa->open_msg = towire_dualopend_opener_init(oa, psbt, *amount, oa->our_upfront_shutdown_script, our_upfront_shutdown_script_wallet_index, *feerate_per_kw, - unilateral_feerate(cmd->ld->topology, true), + anchor_feerate, *feerate_per_kw_funding, channel->channel_flags, amount_sat_is_zero(*request_amt) ? From c6528dacec0ab080a76175b82c599e40e210f8c2 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 9 Feb 2026 14:30:38 +1030 Subject: [PATCH 5/5] common: allow current_feerate under 253 in marginal_feerate(). We don't allow this for now (by spec!), but in case we ever did we should not try to enforce it here! Signed-off-by: Rusty Russell --- common/fee_states.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/fee_states.c b/common/fee_states.c index 3d1a8976e664..1626ea824c0b 100644 --- a/common/fee_states.c +++ b/common/fee_states.c @@ -172,7 +172,9 @@ u32 marginal_feerate(u32 current_feerate) if (current_feerate == 0) return 0; #endif - assert(current_feerate >= minfeerate); + /* This could happen in future if we celebrate sub-sat summer! */ + if (current_feerate < minfeerate) + current_feerate = minfeerate; if (current_feerate > maxfeerate) return current_feerate * 1.1;