Skip to content

Commit 569b63e

Browse files
committed
simpleclosed.c: add heuristic to delay our tx broadcast if our amount is less AND our fee is less than our peer's amount and fee
in case of a reboot the tx will be broadcast as usual. simple_close_control.c: add delay logic to `drop_to_chain` after 1 hour. test_closing.py: add test to verify the heuristic is being applied.
1 parent 688b602 commit 569b63e

4 files changed

Lines changed: 106 additions & 6 deletions

File tree

closingd/simpleclosed.c

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,8 @@ static struct bitcoin_tx *handle_closing_complete(
309309
struct amount_sat local_sat,
310310
struct amount_sat dust_limit,
311311
const u8 *our_last_script,
312-
const u8 *msg)
312+
const u8 *msg,
313+
struct amount_sat *their_fee_out)
313314
{
314315
struct channel_id their_cid = {};
315316
u8 *closer_script, *closee_script;
@@ -331,6 +332,8 @@ static struct bitcoin_tx *handle_closing_complete(
331332
"Bad closing_complete: %s",
332333
tal_hex(tmpctx, msg));
333334

335+
*their_fee_out = fee_sat;
336+
334337
/* BOLT #2:
335338
* The receiver of `closing_complete` (aka. "the closee"):
336339
* ...
@@ -611,7 +614,7 @@ int main(int argc, char *argv[])
611614
bool got_peer_complete, got_our_sig;
612615
struct tlv_closing_tlvs *sent_tlvs;
613616
u8 *sent_closer_script, *sent_closee_script;
614-
struct amount_sat sent_fee;
617+
struct amount_sat sent_fee, their_fee;
615618
u32 sent_locktime = 0;
616619

617620
subdaemon_setup(argc, argv);
@@ -667,7 +670,8 @@ int main(int argc, char *argv[])
667670
}
668671
handle_closing_complete(&funding, funding_sats,
669672
&local_fundingkey, &remote_fundingkey, local_wallet_index,
670-
local_wallet_ext_key, local_sat, dust_limit, local_script, msg);
673+
local_wallet_ext_key, local_sat, dust_limit, local_script,
674+
msg, &their_fee);
671675
got_peer_complete = true;
672676
break;
673677

@@ -761,7 +765,27 @@ int main(int argc, char *argv[])
761765
}
762766
}
763767

764-
wire_sync_write(REQ_FD, take(towire_simpleclosed_complete(NULL)));
768+
/* Decide whether to ask master to delay broadcasting our closer tx.
769+
* If our output (closer pays the full fee) is less than the peer's
770+
* output AND our proposed fee is lower than theirs, their tx has a
771+
* better chance of being mined first — give it an hour head-start.
772+
* Skip when closer_amount is zero: our output is dust and will be
773+
* omitted regardless, so there is nothing to protect by waiting. */
774+
struct amount_sat closer_amount;
775+
if (!amount_sat_sub(&closer_amount, local_sat, sent_fee))
776+
closer_amount = AMOUNT_SAT(0);
777+
bool delay_broadcast = amount_sat_greater(closer_amount, AMOUNT_SAT(0))
778+
&& amount_sat_less(closer_amount, remote_sat)
779+
&& amount_sat_less(sent_fee, their_fee);
780+
if (delay_broadcast)
781+
status_debug("Simple close: requesting delayed broadcast"
782+
" (our output %s < peer %s, our fee %s < their fee %s)",
783+
fmt_amount_sat(tmpctx, closer_amount),
784+
fmt_amount_sat(tmpctx, remote_sat),
785+
fmt_amount_sat(tmpctx, sent_fee),
786+
fmt_amount_sat(tmpctx, their_fee));
787+
788+
wire_sync_write(REQ_FD, take(towire_simpleclosed_complete(NULL, delay_broadcast)));
765789
tal_free(ctx);
766790
daemon_shutdown();
767791
}

closingd/simpleclosed_wire.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ msgdata,simpleclosed_closee_broadcast,sig,bitcoin_signature,
4747

4848
# Negotiations complete, exiting.
4949
msgtype,simpleclosed_complete,3004
50+
msgdata,simpleclosed_complete,delay_broadcast,bool,

lightningd/simple_close_control.c

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
#include <ccan/tal/str/str.h>
66
#include <closingd/simpleclosed_wiregen.h>
77
#include <common/fee_states.h>
8+
#include <common/memleak.h>
89
#include <common/shutdown_scriptpubkey.h>
10+
#include <common/timeout.h>
911
#include <errno.h>
1012
#include <hsmd/permissions.h>
1113
#include <inttypes.h>
@@ -153,9 +155,17 @@ static void handle_simpleclosed_closee_broadcast(struct channel *channel,
153155
fmt_bitcoin_txid(tmpctx, &txid));
154156
}
155157

158+
static void delayed_drop_to_chain(struct channel *channel)
159+
{
160+
log_info(channel->log, "Simple close: broadcast delay elapsed, broadcasting closing tx");
161+
drop_to_chain(channel->peer->ld, channel, true, NULL);
162+
}
163+
156164
static void handle_simpleclosed_complete(struct channel *channel, const u8 *msg)
157165
{
158-
if (!fromwire_simpleclosed_complete(msg)) {
166+
struct lightningd *ld = channel->peer->ld;
167+
bool delay_broadcast;
168+
if (!fromwire_simpleclosed_complete(msg, &delay_broadcast)) {
159169
channel_internal_error(channel,
160170
"bad simpleclosed_complete: %s",
161171
tal_hex(msg, msg));
@@ -176,7 +186,24 @@ static void handle_simpleclosed_complete(struct channel *channel, const u8 *msg)
176186
REASON_UNKNOWN,
177187
"Simple close complete");
178188

179-
drop_to_chain(channel->peer->ld, channel, true, NULL);
189+
if (delay_broadcast) {
190+
log_info(channel->log,
191+
"Simple close: delaying broadcast by 1 hour"
192+
" (peer has higher-fee tx)");
193+
/* Watch the funding outpoint now so onchaind starts when the
194+
* peer's higher-fee tx confirms. Also resolves any pending
195+
* `close` RPC immediately rather than blocking for an hour. */
196+
channel_watch_funding_out(ld, channel);
197+
const struct bitcoin_tx **txs
198+
= tal_arr(tmpctx, const struct bitcoin_tx *, 1);
199+
txs[0] = channel->last_tx;
200+
resolve_close_command(ld, channel, true, txs);
201+
notleak(new_reltimer(ld->timers, channel,
202+
time_from_sec(3600),
203+
delayed_drop_to_chain, channel));
204+
} else {
205+
drop_to_chain(ld, channel, true, NULL);
206+
}
180207
}
181208

182209
static unsigned int simpleclosed_msg(struct subd *sd, const u8 *msg,

tests/test_closing.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4320,6 +4320,54 @@ def test_simple_close_closee_path(node_factory, bitcoind):
43204320
wait_for(lambda: confirmed_txid in {o['txid'] for o in l2.rpc.listfunds()['outputs']})
43214321

43224322

4323+
def test_simple_close_delay_broadcast(node_factory, bitcoind, executor):
4324+
"""When the closer has less output AND proposes a lower fee than the peer,
4325+
it must log a 1-hour delay and let the peer's higher-fee tx get mined first.
4326+
The peer (l2) has the reversed conditions and must NOT delay.
4327+
4328+
We verify the delay via the log message only — we cannot wait an hour."""
4329+
# feerates[3] (100-block ECONOMICAL target) drives BOTH mutual_close_feerate
4330+
# AND the anchor commitment feerate used during channel open. The anchor
4331+
# path clamps to a floor of 1250 sat/kw, and l2 enforces a minimum of
4332+
# feerates[3]//2 = 7500//2 = 3750 sat/kw on the proposed commitment rate.
4333+
# So l1 needs feerates[3] >= 3750 to pass l2's open-channel check, yet
4334+
# still be strictly lower than l2's 7500 to trigger the delay heuristic.
4335+
# Setting per-node feerates at startup avoids smoothing: the first poll
4336+
# copies raw values directly with no exponential smoothing applied.
4337+
l1_opts = {'experimental-simple-close': None,
4338+
'feerates': (7500, 7500, 7500, 3750)}
4339+
l2_opts = {'experimental-simple-close': None,
4340+
'feerates': (7500, 7500, 7500, 7500)}
4341+
l1, l2 = node_factory.line_graph(2, opts=[l1_opts, l2_opts])
4342+
4343+
# Pay 600 000 sat l1 → l2: afterwards l1 ≈ 400 000 sat, l2 ≈ 600 000 sat.
4344+
l1.pay(l2, 600_000_000)
4345+
wait_for(lambda: only_one(l2.rpc.listpeerchannels()['channels'])['htlcs'] == [])
4346+
4347+
# l1's close RPC returns as soon as the mutual-close tx is stored, even
4348+
# though the broadcast itself is delayed 1 hour. Run it in a thread so
4349+
# the test can proceed without blocking.
4350+
fut = executor.submit(l1.rpc.close, l2.info['id'])
4351+
4352+
# l1 as closer: closer_amount < remote_sat (l2 has more)
4353+
# AND sent_fee (≈3750*weight/1000) < their_fee (≈7500*weight/1000) → delay.
4354+
l1.daemon.wait_for_log('Simple close: delaying broadcast by 1 hour')
4355+
4356+
# l2 as closee: no delay expected.
4357+
wait_for(lambda: only_one(l2.rpc.listpeerchannels()['channels'])['state']
4358+
== 'CLOSINGD_COMPLETE')
4359+
assert not l2.daemon.is_in_log('Simple close: delaying broadcast')
4360+
4361+
# l2 broadcasts immediately; wait until its tx is confirmed.
4362+
# We wait for the broadcast log rather than polling getrawmempool(),
4363+
# which can miss a just-submitted tx under the rpcproxy timing.
4364+
l2.daemon.wait_for_log('Broadcasting txid')
4365+
bitcoind.generate_block(1)
4366+
l1.daemon.wait_for_log('Resolved FUNDING_TRANSACTION/FUNDING_OUTPUT by MUTUAL_CLOSE')
4367+
l2.daemon.wait_for_log('Resolved FUNDING_TRANSACTION/FUNDING_OUTPUT by MUTUAL_CLOSE')
4368+
fut.result(timeout=10)
4369+
4370+
43234371
def test_simple_close_no_feature_fallback(node_factory, bitcoind, chainparams):
43244372
"""Without option_simple_close the nodes must fall back to legacy closingd
43254373
(iterative closing_signed fee negotiation) and produce a single mutually-

0 commit comments

Comments
 (0)