closingd: implement option_simple_close (BOLT2)#9104
Conversation
ff9b5a6 to
9f9896b
Compare
rustyrussell
left a comment
There was a problem hiding this comment.
As you'll see, I've gone through this with a fine-toothed comb and picked all the nits I could find!
It's generally excellent, with a few things requiring cleanup (and obviously, some folding of those fix commits). Great work!
| /* Broadcast it (rebroadcast on restarts as needed). */ | ||
| broadcast_tx(channel, ld->topology, channel, tx, NULL, false, 0, | ||
| NULL, NULL, NULL); |
There was a problem hiding this comment.
No. You need to check that it is a valid transaction: by policy we don't trust our subdaemons (much! We should also check it actually goes where we expect it, and pays us, but you'll note that check is missing already in closing_control!). You then need to set it as as our channel's last tx using:
channel_set_last_tx(channel, tx, &sig);
wallet_channel_save(ld->wallet, channel);
Otherwise it won't restore coming back. The closing logic will then handle broadcast, normally.
There was a problem hiding this comment.
I've based this implementation heavily on legacy close... Thanks for the heads-up!
There was a problem hiding this comment.
I was able to add a few checks, let me know if it's necessary for now, we can always get back to this when adding checks to closing_control.
- exactly 1 input spending
channel->funding, every output goes toshutdown_scriptpubkey[LOCAL]or[REMOTE](also skipping the validation for Elements' outputs) - peer's signature is valid for this exact tx over the funding wscript
a4c8771 to
aa8a412
Compare
|
@rustyrussell I reviewed all your comments and fixed the code. Waiting for your review. |
4d784c5 to
569b63e
Compare
|
|
||
| # The channel must resolve as a MUTUAL close — not as a unilateral close, | ||
| # which would happen if l1 had rebroadcast the commitment tx instead. | ||
| l1.daemon.wait_for_log('Resolved FUNDING_TRANSACTION/FUNDING_OUTPUT by MUTUAL_CLOSE') |
There was a problem hiding this comment.
This test could also check if the correct mutual close tx got selected based on their fee. You could do this by asserting on channel->last_tx
There was a problem hiding this comment.
channel->last_tx here is always our own closer tx, not a fee-based pick. Both handle_simpleclosed_got_sig(closer tx) and handle_simpleclosed_closee_broadcast(closee tx) call channel_set_last_tx, and closing_sig always arrives after the peer's closing_complete, so the closer tx is the one stored last.
The fee comparison lives in the delay heuristic (it decides broadcast timing, not which tx is stored), and test_simple_close_delay_broadcast already covers that path via its log assertion. So there's no fee-based last_tx selection to assert on in this test.
| "added": "v26.08", | ||
| "deprecated": null | ||
| }, |
There was a problem hiding this comment.
I think the release will be 26.09?
There was a problem hiding this comment.
That's correct, fixed!
| if (got_peer_complete) { | ||
| status_debug("Got another closing_complete " | ||
| "(peer bumping fee): re-signing"); | ||
| } |
There was a problem hiding this comment.
It might be good to have a test for this case?
There was a problem hiding this comment.
This branch only runs when a peer sends a second closing_complete to bump the fee. CLN's simpleclosed sends closing_complete exactly once and never RBF-bumps, so two CLN nodes can't reach it. It's kept for interop with peers that do bump. A test would need a mock peer that re-sends closing_complete, so I'll defer coverage until CLN itself gains closing fee-bumping.
| txs[0] = channel->last_tx; | ||
| resolve_close_command(ld, channel, true, txs); | ||
| notleak(new_reltimer(ld->timers, channel, | ||
| time_from_sec(3600), |
There was a problem hiding this comment.
nit: magic number, could use a comment or be a constant
Four tests for the `option_simple_close` protocol (BOLT ElementsProject#2, bit 60). `test_simple_close_no_feature_fallback` exercises the existing legacy `closingd` path and passes now; the other three are marked xfail until the implementation lands: - test_simple_close_basic: happy path; both nodes exchange closing_complete/closing_sig, each broadcasts two conflicting txs, the winner confirms, both nodes detect their output CONFIRMED - test_simple_close_closer_pays_fee: closer bears the fee, closee receives their exact pre-close balance - test_simple_close_dust_output_omitted: closee output below dust is omitted from the closing tx (closer_output_only variant) - test_simple_close_no_feature_fallback: without bit 60, nodes fall back to legacy closingd - test_simple_close_restart: re-transmit our simple_close transaction after restart - test_simple_close_closee_path: the closee (peer) transaction is stored
features.h: Reserves bits 60/61 for `option_simple_close` per BOLT2.
features.c: Add `OPT_SIMPLE_CLOSE` to `feature_styles[]` and declare the correct `feature_name`.
tests/test_closing.py: Update options on `test_simple_close_...` to include `{experimental-simple-close: None}`.
lightningd/options.c: Register noarg option for simple close.
doc/schemas/listconfigs.json: add `experimental-simple-close` to config targeting v26.08.
doc/lightningd-config.5.md: Specify `experimental-simple-close` option.
contrib/pyln-testing/pyln/testing/utils.py: Allow setting `EXPERIMENTAL_SIMPLE_CLOSE` on tests.
Generated files after modifying sources.
…ose` Adds the BOLT3 simple closing transaction builder: - sequence 0xFFFFFFFD (RBF-signalling) - locktime from closing_complete - closer pays fee (their output is reduced) - dust outputs are omitted and a zero-value OP_RETURN is used when both outputs would be dust.
When `option_simple_close` is negotiated the master launches `simpleclosed` after `channeld` exits, so `closing_complete` and `closing_sig` should never arrive inside `channeld`. Add stubs that call peer_failed_warn() with an informative message rather than hitting the default unknown-message path.
New subdaemon implementing the BOLT2 simple close protocol, replacing `lightning_closingd` when `option_simple_close` is negotiated: - Each peer independently sends `closing_complete` with their fee proposal; - The other side signs it and sends `closing_sig`; - Both sides broadcast two conflicting closing transactions and whichever confirms first wins. Key protocol details: - Closer pays the fee and closee receives their exact channel balance; - TLV variants selected per BOLT2: `closer_output_only`, `closer_and_closee_outputs`, `closee_output_only`; - Sequence 0xFFFFFFFD enables RBF via re-sending `closing_complete`; - Script mismatch on `closee_scriptpubkey` warns and fails to reconnect; common/shutdown_scriptpubkey.h/c: removed `static` from `is_valid_op_return` so it can be used in `simpleclosed.c`
Adds the master-side glue for the `simpleclosed` subdaemon and removes the xfail markers from the integration tests: - simple_close_control.c: - starts the daemon with feerate bounds and shutdown scripts; - handles SIMPLECLOSED_GOT_SIG (broadcast closer tx), SIMPLECLOSED_CLOSEE_BROADCAST (broadcast closee tx), and SIMPLECLOSED_COMPLETE (advance state, resolve close RPC) - channel_control.c: route to peer_start_simpleclosed() instead of peer_start_closingd() when OPT_SIMPLE_CLOSE is negotiated; - peer_control.c: drop_to_chain_simple_close() sets up the funding-spend watch and resolves the close RPC without broadcasting the commitment tx, avoids it RBF-replacing the mutual close txs; - resend_closing_transactions() uses the same variant on restart Changelog-Experimental: Protocol: implement `option_simple_close` (BOLT2) for simpler one-shot mutual close fee negotiation. Enable with --dev-force-features=+60.
… 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.
|
I don't know what's happening. 3 runs gets cancelled at 10%: https://github.com/ElementsProject/lightning/actions/runs/28530856367/job/84619960038?pr=9104 |
Implements the
option_simple_closemutual close protocol from BOLT2 (feature bit 60/61), replacing the legacy iterativeclosing_signedfee negotiation for peers that support it.Background
The existing mutual close protocol requires both sides to agree on a single fee via
closing_signedmessages. If their fee sources diverge (e.g. during a fee spike) they can loop indefinitely and stall.The new protocol is simpler: each peer independently proposes their own closing transaction via
closing_complete, the other side signs it and replies withclosing_sig, and both broadcast their respective versions. Two valid-but-conflicting transactions enter the mempool, whichever confirms first wins. The closer (the one who sentclosing_complete)bears the fee.
What this adds
common/features:OPT_SIMPLE_CLOSE(bit 60/61), registered infeature_styles[]fordev-force-featuressupport. Not indefault_features()yet: opt-in only.common/close_tx:create_simple_close_tx()builds the BOLT3 simple closing transaction: sequence0xFFFFFFFD(RBF), locktime fromclosing_complete, closer pays fee, dust outputs omitted, zero-valueOP_RETURNwhen both outputs are dust.channeld: stubs that callpeer_failed_warn()ifclosing_completeorclosing_sigarrive insidechanneld(they should only ever reachsimpleclosed).closingd/simpleclosed: new subdaemonlightning_simpleclosedthat implements the full BOLT2 exchange loop, including TLV variant selection, signature verification, and both-sides broadcast.lightningd:simple_close_control.cstarts the daemon and handles its wire messages;channel_control.croutes to it whenOPT_SIMPLE_CLOSEis negotiated;peer_control.caddsdrop_to_chain_simple_close()which sets up the on-chain watch and resolves the close RPC without re-broadcasting the commitment tx (which would otherwise RBF-replace the mutual close txs).Testing
Four integration tests in
tests/test_closing.pyusing--dev-force-features=+60:test_simple_close_basic: happy path, state transitions, on-chain settlementtest_simple_close_closer_pays_fee: fee deducted from closer's output onlytest_simple_close_dust_output_omitted: dust output omittedtest_simple_close_no_feature_fallback: without bit 60, legacyclosingdis usedtest_simple_close_restart: re-transmit our simple_close transaction after restarttest_simple_close_closee_path: the closee (peer) transaction is storedEnabling
To enable by default in a future release, add
OPTIONAL_FEATURE(OPT_SIMPLE_CLOSE)todefault_features()inlightningd/lightningd.c.Checklist
Changelog-Experimental:in commit 979a285)check-source-boltoption_simple_close(BOLT 2 Simple Closing Negotiation) #9099lightning-downgrade: not applicable: no database migrations or schema changes