Skip to content

closingd: implement option_simple_close (BOLT2)#9104

Open
nGoline wants to merge 8 commits into
ElementsProject:masterfrom
nGoline:simple-close
Open

closingd: implement option_simple_close (BOLT2)#9104
nGoline wants to merge 8 commits into
ElementsProject:masterfrom
nGoline:simple-close

Conversation

@nGoline

@nGoline nGoline commented May 1, 2026

Copy link
Copy Markdown
Collaborator

Implements the option_simple_close mutual close protocol from BOLT2 (feature bit 60/61), replacing the legacy iterative closing_signed fee negotiation for peers that support it.

Background

The existing mutual close protocol requires both sides to agree on a single fee via closing_signed messages. 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 with closing_sig, and both broadcast their respective versions. Two valid-but-conflicting transactions enter the mempool, whichever confirms first wins. The closer (the one who sent closing_complete)
bears the fee.

What this adds

  • common/features: OPT_SIMPLE_CLOSE (bit 60/61), registered in feature_styles[] for dev-force-features support. Not in default_features() yet: opt-in only.
  • common/close_tx: create_simple_close_tx() builds the BOLT3 simple closing transaction: sequence 0xFFFFFFFD (RBF), locktime from closing_complete, closer pays fee, dust outputs omitted, zero-value OP_RETURN when both outputs are dust.
  • channeld: stubs that call peer_failed_warn() if closing_complete or closing_sig arrive inside channeld (they should only ever reach simpleclosed).
  • closingd/simpleclosed: new subdaemon lightning_simpleclosed that implements the full BOLT2 exchange loop, including TLV variant selection, signature verification, and both-sides broadcast.
  • lightningd:
    • simple_close_control.c starts the daemon and handles its wire messages; channel_control.c routes to it when OPT_SIMPLE_CLOSE is negotiated;
    • peer_control.c adds drop_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.py using--dev-force-features=+60:

  • test_simple_close_basic: happy path, state transitions, on-chain settlement
  • test_simple_close_closer_pays_fee: fee deducted from closer's output only
  • test_simple_close_dust_output_omitted: dust output omitted
  • test_simple_close_no_feature_fallback: without bit 60, legacy closingd is used
  • test_simple_close_restart: re-transmit our simple_close transaction after restart
  • test_simple_close_closee_path: the closee (peer) transaction is stored

Enabling

lightningd --dev-force-features=+60

To enable by default in a future release, add OPTIONAL_FEATURE(OPT_SIMPLE_CLOSE) to default_features() in
lightningd/lightningd.c.

Checklist

@nGoline nGoline self-assigned this May 1, 2026
@nGoline nGoline requested a review from rustyrussell May 1, 2026 18:26
@madelinevibes madelinevibes added this to the 26.06 milestone May 4, 2026
@nGoline nGoline force-pushed the simple-close branch 2 times, most recently from ff9b5a6 to 9f9896b Compare May 4, 2026 12:23
@nGoline nGoline marked this pull request as ready for review May 5, 2026 17:25
@madelinevibes madelinevibes modified the milestones: v26.06, v26.09 May 11, 2026

@rustyrussell rustyrussell left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Comment thread tests/test_closing.py Outdated
Comment thread tests/test_closing.py Outdated
Comment thread tests/test_closing.py
Comment thread tests/test_closing.py Outdated
Comment thread tests/test_closing.py Outdated
Comment thread closingd/simpleclosed_wire.csv Outdated
Comment thread lightningd/peer_control.c Outdated
Comment thread lightningd/simple_close_control.c Outdated
Comment thread lightningd/simple_close_control.c Outdated
Comment thread lightningd/simple_close_control.c Outdated
@nGoline nGoline requested a review from cdecker as a code owner May 12, 2026 20:32
@nGoline nGoline force-pushed the simple-close branch 6 times, most recently from a4c8771 to aa8a412 Compare May 14, 2026 14:19
@nGoline

nGoline commented May 14, 2026

Copy link
Copy Markdown
Collaborator Author

@rustyrussell I reviewed all your comments and fixed the code. Waiting for your review.

@nGoline nGoline force-pushed the simple-close branch 2 times, most recently from 4d784c5 to 569b63e Compare May 18, 2026 20:41
@nGoline nGoline removed the 26.06 RC label May 21, 2026
@nGoline nGoline requested review from daywalker90 and removed request for cdecker June 18, 2026 18:59
@nGoline nGoline added the Status::Ready for Review The work has been completed and is now awaiting evaluation or approval. label Jun 18, 2026
@nGoline nGoline requested review from cdecker and sangbida and removed request for daywalker90 June 19, 2026 10:25
Comment thread tests/test_closing.py
Comment thread tests/test_closing.py
Comment thread .msggen.json Outdated
Comment thread closingd/simpleclosed.c
Comment thread lightningd/simple_close_control.c Outdated
@nGoline nGoline requested a review from sangbida July 1, 2026 13:52
@nGoline nGoline force-pushed the simple-close branch 2 times, most recently from d5ba114 to cb95481 Compare July 1, 2026 16:01
@nGoline

nGoline commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

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

@sangbida sangbida left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ack cb95481

nGoline added 8 commits July 2, 2026 13:30
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.
@nGoline

nGoline commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Just updated the branch against master. ack should still be valid.

@nGoline nGoline added Status::Release Ready ✅🤩 and removed Status::Ready for Review The work has been completed and is now awaiting evaluation or approval. labels Jul 2, 2026
@nGoline nGoline removed the request for review from cdecker July 2, 2026 18:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement option_simple_close (BOLT 2 Simple Closing Negotiation)

4 participants