closingd: add `lightning_simpleclosed` for `option_simple_close`
What changed, and why it matters
This commit adds a new sub-program (lightning_simpleclosed) to Core Lightning that implements a new, simpler way for two Lightning nodes to agree on closing a payment channel together. It is a feature addition, not a documented security fix. The code introduces new message handling, transaction signing, and fee logic, which naturally carries implementation risk, but the commit message and diff do not describe any known vulnerability or attack.
Treat as a normal feature commit. Reviewers should audit the new simple-close protocol implementation for correct signature verification, fee capping, dust handling, and OP_RETURN edge cases, especially around the closer/closer_amount and closee_amount computations and the two-conflicting-transactions broadcast behavior.
Security signals we found
New subdaemon handling channel-close funds and signatures
Signature validation on closing_complete / closing_sig via check_tx_sig
Fee sanity checks against local/remote balances
Scriptpubkey validation and mismatch warnings
No vendor disclosure of security relevance in commit or references
Evidence from the diff
The patch introduces closingd/simpleclosed.c and its wire protocol to implement BOLT 2’s option_simple_close. Each peer sends a closing_complete TLV with proposed fee and signatures for one or more closing transaction variants; the counterparty replies with closing_sig. The daemon validates scripts, fee bounds, signature fields, and reconstructs the signed closing transaction before handing it to lightningd for broadcast. A helper in common/shutdown_scriptpubkey.c is made non-static so the new daemon can call is_valid_op_return().
Changed components
closingd/simpleclosed.cclosingd/simpleclosed_wire.csvclosingd/Makefilecommon/shutdown_scriptpubkey.ccommon/shutdown_scriptpubkey.hInspect captured patch +841 / −6
diff --git a/closingd/Makefile b/closingd/Makefile
index 4d961d28..a128163f 100644
--- a/closingd/Makefile
+++ b/closingd/Makefile
@@ -8,15 +8,26 @@ CLOSINGD_SRC := closingd/closingd.c \
CLOSINGD_OBJS := $(CLOSINGD_SRC:.c=.o)
$(CLOSINGD_OBJS): $(CLOSINGD_HEADERS)
+# Simple close daemon
+SIMPLECLOSED_HEADERS := closingd/simpleclosed_wiregen.h
+
+SIMPLECLOSED_SRC := closingd/simpleclosed.c \
+ $(SIMPLECLOSED_HEADERS:.h=.c)
+
+SIMPLECLOSED_OBJS := $(SIMPLECLOSED_SRC:.c=.o)
+$(SIMPLECLOSED_OBJS): $(SIMPLECLOSED_HEADERS)
+
# Make sure these depend on everything.
-ALL_C_SOURCES += $(CLOSINGD_SRC)
-ALL_C_HEADERS += $(CLOSINGD_HEADERS)
-ALL_PROGRAMS += lightningd/lightning_closingd
+ALL_C_SOURCES += $(CLOSINGD_SRC) $(SIMPLECLOSED_SRC)
+ALL_C_HEADERS += $(CLOSINGD_HEADERS) $(SIMPLECLOSED_HEADERS)
+ALL_PROGRAMS += lightningd/lightning_closingd lightningd/lightning_simpleclosed
# Here's what lightningd depends on
-LIGHTNINGD_CONTROL_HEADERS += closingd/closingd_wiregen.h
-LIGHTNINGD_CONTROL_OBJS += closingd/closingd_wiregen.o
+LIGHTNINGD_CONTROL_HEADERS += closingd/closingd_wiregen.h closingd/simpleclosed_wiregen.h
+LIGHTNINGD_CONTROL_OBJS += closingd/closingd_wiregen.o closingd/simpleclosed_wiregen.o
lightningd/lightning_closingd: $(CLOSINGD_OBJS) $(HSMD_CLIENT_OBJS) libcommon.a
+lightningd/lightning_simpleclosed: $(SIMPLECLOSED_OBJS) $(HSMD_CLIENT_OBJS) libcommon.a
+
-include closingd/test/Makefile
diff --git a/closingd/simpleclosed.c b/closingd/simpleclosed.c
new file mode 100644
index 00000000..72a42adb
--- /dev/null
+++ b/closingd/simpleclosed.c
@@ -0,0 +1,767 @@
+/* Main simple-close daemon: implements option_simple_close (BOLT #2).
+ * Runs after channeld signals shutdown_complete, replacing closingd
+ * when option_simple_close is negotiated. */
+#include "config.h"
+#include <bitcoin/pubkey.h>
+#include <bitcoin/script.h>
+#include <bitcoin/signature.h>
+#include <ccan/cast/cast.h>
+#include <ccan/mem/mem.h>
+#include <ccan/tal/str/str.h>
+#include <closingd/simpleclosed_wiregen.h>
+#include <common/close_tx.h>
+#include <common/memleak.h>
+#include <common/peer_billboard.h>
+#include <common/peer_failed.h>
+#include <common/peer_io.h>
+#include <common/per_peer_state.h>
+#include <common/read_peer_msg.h>
+#include <common/shutdown_scriptpubkey.h>
+#include <common/status.h>
+#include <common/subdaemon.h>
+#include <common/utils.h>
+#include <errno.h>
+#include <hsmd/hsmd_wiregen.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <wally_script.h>
+#include <wire/peer_wire.h>
+#include <wire/wire_sync.h>
+
+/* stdin == requests from master, 3 == peer fd, 4 == hsmd fd */
+#define REQ_FD STDIN_FILENO
+#define PEER_FD 3
+#define HSM_FD 4
+
+/* Approx weight of a simple-close tx with both outputs (vbytes * 4 for weight).
+ * Input: 41vb, witness: ~222wu/4=55.5vb, outputs: ~65vb each, overhead: 11vb
+ * Total ~236 vbytes = ~704 weight + witness ~222 = ~926wu, round to 900. */
+#define SIMPLE_CLOSE_WEIGHT 900
+
+static const u8 *hsm_req(const tal_t *ctx, const u8 *req TAKES)
+{
+ u8 *msg;
+ if (!wire_sync_write(HSM_FD, req))
+ status_failed(STATUS_FAIL_HSM_IO,
+ "Writing to HSM: %s",
+ strerror(errno));
+ msg = wire_sync_read(ctx, HSM_FD);
+ if (!msg)
+ status_failed(STATUS_FAIL_HSM_IO,
+ "Reading from HSM: %s",
+ strerror(errno));
+ return msg;
+}
+
+static struct per_peer_state *pps;
+
+/* Tell master we got peer's closing_sig for our tx; block for txid reply. */
+static struct bitcoin_txid master_got_sig(struct bitcoin_tx *tx,
+ const struct bitcoin_signature *sig)
+{
+ struct bitcoin_txid txid;
+ u8 *msg;
+
+ msg = towire_simpleclosed_got_sig(tmpctx, tx, sig);
+ if (!wire_sync_write(REQ_FD, take(msg)))
+ status_failed(STATUS_FAIL_MASTER_IO,
+ "Writing got_sig: %s",
+ strerror(errno));
+
+ msg = wire_sync_read(tmpctx, REQ_FD);
+ if (!msg)
+ status_failed(STATUS_FAIL_MASTER_IO,
+ "Reading got_sig_reply: %s",
+ strerror(errno));
+ if (!fromwire_simpleclosed_got_sig_reply(msg, &txid))
+ status_failed(STATUS_FAIL_MASTER_IO,
+ "Bad got_sig_reply: %s",
+ tal_hex(tmpctx, msg));
+ return txid;
+}
+
+/* Build one closing tx variant. closer_script or closee_script may be NULL
+ * to omit that output. */
+static struct bitcoin_tx *make_close_tx(const tal_t *ctx,
+ u32 *local_wallet_index,
+ const struct ext_key *local_wallet_ext_key,
+ const u8 *closer_script,
+ const u8 *closee_script,
+ const u8 *funding_wscript,
+ const struct bitcoin_outpoint *funding,
+ struct amount_sat funding_sats,
+ struct amount_sat closer_amount,
+ struct amount_sat closee_amount,
+ u32 locktime)
+{
+ return create_simple_close_tx(ctx, local_wallet_index, local_wallet_ext_key,
+ closer_script, closee_script, funding_wscript, funding, funding_sats,
+ closer_amount, closee_amount, locktime);
+}
+
+/* Sign a simple close tx via HSM; returns bitcoin_signature. */
+static struct bitcoin_signature sign_tx(const tal_t *ctx,
+ struct bitcoin_tx *tx,
+ const struct pubkey *remote_fundingkey)
+{
+ struct bitcoin_signature sig;
+ const u8 *msg;
+
+ msg = hsm_req(tmpctx,
+ take(towire_hsmd_sign_mutual_close_tx(NULL, tx, remote_fundingkey)));
+ if (!fromwire_hsmd_sign_tx_reply(msg, &sig))
+ status_failed(STATUS_FAIL_HSM_IO,
+ "Bad sign_mutual_close_tx reply %s",
+ tal_hex(tmpctx, msg));
+ return sig;
+}
+
+/* Build and send closing_complete as the closer.
+ * Returns the TLVs we sent (so we can validate the closing_sig reply).
+ * Also writes the computed fee into *fee_out. */
+static struct tlv_closing_tlvs *send_closing_complete(
+ const struct channel_id *channel_id,
+ const struct bitcoin_outpoint *funding,
+ struct amount_sat funding_sats,
+ const struct pubkey *local_fundingkey,
+ const struct pubkey *remote_fundingkey,
+ u32 *local_wallet_index,
+ const struct ext_key *local_wallet_ext_key,
+ struct amount_sat local_sat,
+ struct amount_sat remote_sat,
+ struct amount_sat dust_limit,
+ u32 feerate_perkw,
+ const u8 *local_script,
+ const u8 *remote_script,
+ u32 locktime,
+ struct amount_sat *fee_out)
+{
+ const u8 *funding_wscript;
+ struct amount_sat fee, closer_amount, closee_amount;
+ bool is_lesser, closee_dust, closer_dust;
+ struct bitcoin_tx *tx_both, *tx_closer_only, *tx_closee_only;
+ struct bitcoin_signature sig;
+ struct tlv_closing_tlvs *tlvs;
+ u8 *msg;
+
+ funding_wscript = bitcoin_redeem_2of2(tmpctx,
+ local_fundingkey, remote_fundingkey);
+
+ /* Fee: feerate * weight / 1000, capped at our balance. */
+ fee = amount_sat((u64)feerate_perkw * SIMPLE_CLOSE_WEIGHT / 1000);
+ if (amount_sat_greater(fee, local_sat))
+ fee = local_sat; /* Can't pay more than we have */
+
+ if (!amount_sat_sub(&closer_amount, local_sat, fee))
+ closer_amount = AMOUNT_SAT(0);
+ closee_amount = remote_sat;
+
+ is_lesser = amount_sat_less(local_sat, remote_sat);
+ closee_dust = amount_sat_less(closee_amount, dust_limit)
+ && !is_valid_op_return(remote_script, tal_bytelen(remote_script));
+ closer_dust = amount_sat_less(closer_amount, dust_limit)
+ && !is_valid_op_return(local_script, tal_bytelen(local_script));
+
+ tlvs = tlv_closing_tlvs_new(tmpctx);
+
+ /* BOLT #2:
+ * The sender of `closing_complete` (aka. "the closer"):
+ * ...
+ * - If the local outstanding balance (in millisatoshi) is less than the remote outstanding balance:
+ * - MUST NOT set `closer_output_only`.
+ */
+ if (is_lesser) {
+ /* BOLT #2:
+ * ...
+ * - MUST set `closee_output_only` if the local output amount is dust.
+ * - MAY set `closee_output_only` if it considers the local output amount uneconomical AND its `closer_scriptpubkey` is not `OP_RETURN`.
+ */
+ if (closer_dust) {
+ /* Our output is dust: sign tx with only closee output */
+ tx_closee_only = make_close_tx(tmpctx, NULL, NULL, NULL,
+ remote_script, funding_wscript, funding, funding_sats,
+ AMOUNT_SAT(0), closee_amount, locktime);
+ if (tx_closee_only) {
+ sig = sign_tx(tmpctx, tx_closee_only, remote_fundingkey);
+ tlvs->closee_output_only = tal(tlvs, secp256k1_ecdsa_signature);
+ *tlvs->closee_output_only = sig.s;
+ }
+ } else {
+ /* Both outputs present; closee can use this. */
+ tx_both = make_close_tx(tmpctx, local_wallet_index,
+ local_wallet_ext_key, local_script, remote_script,
+ funding_wscript, funding, funding_sats, closer_amount,
+ closee_amount, locktime);
+ if (tx_both) {
+ sig = sign_tx(tmpctx, tx_both, remote_fundingkey);
+ tlvs->closer_and_closee_outputs = tal(tlvs,
+ secp256k1_ecdsa_signature);
+ *tlvs->closer_and_closee_outputs = sig.s;
+ }
+ }
+ } else {
+ /* BOLT #2:
+ * - Otherwise (not lesser amount, cannot remove its own output):
+ * - MUST NOT set `closee_output_only`.
+ * - If it considers the local output amount uneconomical:
+ * - MAY send a `closer_scriptpubkey` that is a valid `OP_RETURN` script.
+ * - If it does, the output value MUST be set to zero so that all funds go to fees, as specified in [BOLT #3]...
+ */
+ tx_closer_only = make_close_tx(tmpctx, local_wallet_index,
+ local_wallet_ext_key, local_script, NULL, funding_wscript, funding,
+ funding_sats, closer_amount, AMOUNT_SAT(0), locktime);
+ if (tx_closer_only) {
+ sig = sign_tx(tmpctx, tx_closer_only, remote_fundingkey);
+ tlvs->closer_output_only = tal(tlvs, secp256k1_ecdsa_signature);
+ *tlvs->closer_output_only = sig.s;
+ }
+
+ /* BOLT #2:
+ * - If the closee's output amount is dust:
+ * - MUST set `closer_output_only`.
+ * - MUST NOT set `closer_and_closee_outputs`.
+ * - Otherwise:
+ * - MUST set both `closer_output_only` and `closer_and_closee_outputs`.
+ */
+ if (!closee_dust) {
+ /* Also sign the both-outputs variant. */
+ tx_both = make_close_tx(tmpctx, local_wallet_index,
+ local_wallet_ext_key, local_script, remote_script,
+ funding_wscript, funding, funding_sats, closer_amount,
+ closee_amount, locktime);
+ if (tx_both) {
+ sig = sign_tx(tmpctx, tx_both, remote_fundingkey);
+ tlvs->closer_and_closee_outputs = tal(tlvs,
+ secp256k1_ecdsa_signature);
+ *tlvs->closer_and_closee_outputs = sig.s;
+ }
+ }
+ }
+
+ /* Sanity: must have at least one sig to send. */
+ if (!tlvs->closer_output_only && !tlvs->closee_output_only
+ && !tlvs->closer_and_closee_outputs)
+ status_failed(STATUS_FAIL_INTERNAL_ERROR,
+ "No valid closing tx variant could be built "
+ "(local=%s remote=%s fee=%s dust=%s)",
+ fmt_amount_sat(tmpctx, local_sat),
+ fmt_amount_sat(tmpctx, remote_sat),
+ fmt_amount_sat(tmpctx, fee),
+ fmt_amount_sat(tmpctx, dust_limit));
+
+ msg = towire_closing_complete(tmpctx, channel_id, local_script,
+ remote_script, fee, locktime, tlvs);
+ peer_write(pps, take(msg));
+
+ *fee_out = fee;
+ status_debug("Sent closing_complete: closer=%s closee=%s fee=%s",
+ fmt_amount_sat(tmpctx, closer_amount),
+ fmt_amount_sat(tmpctx, closee_amount),
+ fmt_amount_sat(tmpctx, fee));
+ return tlvs;
+}
+
+static struct bitcoin_signature check_their_sig(
+ const secp256k1_ecdsa_signature *raw_sig,
+ const struct bitcoin_tx *tx,
+ const u8 *funding_wscript,
+ const struct pubkey *remote_fundingkey,
+ const struct channel_id *their_cid,
+ const char *whatfield)
+{
+ struct bitcoin_signature sig;
+ sig.sighash_type = SIGHASH_ALL;
+ sig.s = *raw_sig;
+ if (!check_tx_sig(tx, 0, NULL, funding_wscript, remote_fundingkey, &sig))
+ peer_failed_warn(pps, their_cid,
+ "Bad %s sig: tx=%s funding_wscript=%s fundingkey=%s",
+ whatfield,
+ fmt_bitcoin_tx(tmpctx, tx),
+ tal_hex(tmpctx, funding_wscript),
+ fmt_pubkey(tmpctx, remote_fundingkey));
+ return sig;
+}
+
+static secp256k1_ecdsa_signature *our_sig(const tal_t *ctx,
+ struct bitcoin_tx *tx,
+ const struct pubkey *remote_fundingkey,
+ struct bitcoin_signature *sig_out,
+ const char *whatfield)
+{
+ secp256k1_ecdsa_signature *result;
+ *sig_out = sign_tx(tmpctx, tx, remote_fundingkey);
+ result = tal(ctx, secp256k1_ecdsa_signature);
+ *result = sig_out->s;
+ status_debug("Signed %s: tx=%s", whatfield, fmt_bitcoin_tx(tmpctx, tx));
+ return result;
+}
+
+/* As closee: receive peer's closing_complete, sign the right variant,
+ * send closing_sig, and broadcast. Returns the tx we signed. */
+static struct bitcoin_tx *handle_closing_complete(
+ const struct bitcoin_outpoint *funding,
+ struct amount_sat funding_sats,
+ const struct pubkey *local_fundingkey,
+ const struct pubkey *remote_fundingkey,
+ u32 *local_wallet_index,
+ const struct ext_key *local_wallet_ext_key,
+ struct amount_sat local_sat,
+ struct amount_sat dust_limit,
+ const u8 *our_last_script,
+ const u8 *msg)
+{
+ struct channel_id their_cid = {};
+ u8 *closer_script, *closee_script;
+ struct amount_sat fee_sat;
+ u32 locktime;
+ struct tlv_closing_tlvs *tlvs;
+ struct tlv_closing_tlvs *reply_tlvs;
+ struct bitcoin_tx *chosen_tx;
+ struct bitcoin_signature sig;
+ struct amount_sat closer_amount, closee_amount;
+ const u8 *funding_wscript;
+ u8 *reply;
+ bool anysegwit = true, option_simple_close = true;
+ struct amount_sat remote_sat;
+
+ if (!fromwire_closing_complete(tmpctx, msg, &their_cid, &closer_script,
+ &closee_script, &fee_sat, &locktime, &tlvs))
+ peer_failed_warn(pps, &their_cid,
+ "Bad closing_complete: %s",
+ tal_hex(tmpctx, msg));
+
+ /* BOLT #2:
+ * The receiver of `closing_complete` (aka. "the closee"):
+ * ...
+ * - If `closee_scriptpubkey` does not match the last script it sent (from `closing_complete` or from the initial `shutdown`):
+ * - SHOULD ignore `closing_complete`.
+ * - SHOULD send a `warning`.
+ * - SHOULD close the connection.
+ */
+ if (!scripteq(closee_script, our_last_script)) {
+ peer_failed_warn(pps, &their_cid,
+ "closing_complete closee_script %s != our script %s: "
+ "reconnect to resync",
+ tal_hex(tmpctx, closee_script),
+ tal_hex(tmpctx, our_last_script));
+ }
+
+ /* BOLT #2:
+ * The receiver of `closing_complete` (aka. "the closee"):
+ * ...
+ * - If `closer_scriptpubkey` is invalid (as detailed in the [`shutdown` requirements](#closing-initiation-shutdown)):
+ * - SHOULD ignore `closing_complete`.
+ * - SHOULD send a `warning`.
+ * - SHOULD close the connection.
+ */
+ if (!valid_shutdown_scriptpubkey(closer_script, anysegwit, false,
+ option_simple_close))
+ peer_failed_warn(pps, &their_cid,
+ "Invalid closer_scriptpubkey in closing_complete: %s",
+ tal_hex(tmpctx, closer_script));
+
+ funding_wscript = bitcoin_redeem_2of2(tmpctx, local_fundingkey,
+ remote_fundingkey);
+
+ /* Compute amounts: closer pays fee, closee gets remote_sat. */
+ if (!amount_sat_sub(&remote_sat, funding_sats, local_sat))
+ remote_sat = AMOUNT_SAT(0);
+
+ /* BOLT #2:
+ * The receiver of `closing_complete` (aka. "the closee"):
+ * - If `fee_satoshis` is greater than the closer's outstanding balance:
+ * - MUST either send a `warning` and close the connection, or send an `error` and fail the channel.
+ */
+ if (is_valid_op_return(closer_script, tal_bytelen(closer_script)))
+ closer_amount = AMOUNT_SAT(0);
+ else if (!amount_sat_sub(&closer_amount, remote_sat, fee_sat))
+ peer_failed_warn(pps, &their_cid, "Closer fee %s exceeds their balance %s",
+ fmt_amount_sat(tmpctx, fee_sat),
+ fmt_amount_sat(tmpctx, remote_sat));
+
+ if (is_valid_op_return(closee_script, tal_bytelen(closee_script)))
+ closee_amount = AMOUNT_SAT(0);
+ else
+ closee_amount = local_sat;
+
+ bool our_output_dust = amount_sat_less(closee_amount, dust_limit)
+ && !is_valid_op_return(closee_script, tal_bytelen(closee_script));
+
+ reply_tlvs = tlv_closing_tlvs_new(tmpctx);
+
+ struct bitcoin_signature their_sig;
+
+ /* BOLT #2:
+ * The receiver of `closing_complete` (aka. "the closee"):
+ * ...
+ * - Select a signature for validation:
+ * - If the local output amount is dust:
+ * - MUST use `closer_output_only`.
+ * - Otherwise, if it considers the local output amount uneconomical AND its `closee_scriptpubkey` is not `OP_RETURN`:
+ * - MUST use `closer_output_only`.
+ * - Otherwise, if `closer_and_closee_outputs` is present:
+ * - MUST use `closer_and_closee_outputs`.
+ * - Otherwise:
+ * - MUST use `closee_output_only`.
+ * - If the selected signature field does not exist:
+ * - MUST either send a `warning` and close the connection, or send an `error` and fail the channel.
+ * - If the signature field is not valid for the corresponding closing transaction specified in [BOLT #3]...:
+ * - MUST either send a `warning` and close the connection, or send an `error` and fail the channel.
+ */
+ if (our_output_dust) {
+ if (!tlvs->closer_output_only)
+ peer_failed_warn(pps, &their_cid,
+ "Our output is dust but no "
+ "closer_output_only sig provided");
+ chosen_tx = make_close_tx(tmpctx, local_wallet_index,
+ local_wallet_ext_key, closer_script, NULL, funding_wscript,
+ funding, funding_sats, closer_amount, AMOUNT_SAT(0), locktime);
+ reply_tlvs->closer_output_only = our_sig(reply_tlvs, chosen_tx,
+ remote_fundingkey, &sig, "closer_output_only");
+ their_sig = check_their_sig(tlvs->closer_output_only, chosen_tx,
+ funding_wscript, remote_fundingkey, &their_cid,
+ "closer_output_only");
+ } else if (tlvs->closer_and_closee_outputs) {
+ chosen_tx = make_close_tx(tmpctx, local_wallet_index,
+ local_wallet_ext_key, closer_script, closee_script,
+ funding_wscript, funding, funding_sats, closer_amount,
+ closee_amount, locktime);
+ reply_tlvs->closer_and_closee_outputs = our_sig(reply_tlvs, chosen_tx,
+ remote_fundingkey, &sig, "closer_and_closee_outputs");
+ their_sig = check_their_sig(tlvs->closer_and_closee_outputs, chosen_tx,
+ funding_wscript, remote_fundingkey, &their_cid,
+ "closer_and_closee_outputs");
+ } else {
+ if (!tlvs->closee_output_only)
+ peer_failed_warn(pps, &their_cid, "No valid sig variant in "
+ "closing_complete");
+ chosen_tx = make_close_tx(tmpctx, local_wallet_index,
+ local_wallet_ext_key, NULL, closee_script, funding_wscript,
+ funding, funding_sats, AMOUNT_SAT(0), closee_amount, locktime);
+ reply_tlvs->closee_output_only = our_sig(reply_tlvs, chosen_tx,
+ remote_fundingkey, &sig, "closee_output_only");
+ their_sig = check_their_sig(tlvs->closee_output_only, chosen_tx,
+ funding_wscript, remote_fundingkey, &their_cid,
+ "closee_output_only");
+ }
+
+ /* BOLT #2:
+ * The receiver of `closing_complete` (aka. "the closee"):
+ * ...
+ * - MUST send `closing_sig` with a single valid signature in the same TLV field as the `closing_complete`.
+ */
+ reply = towire_closing_sig(tmpctx, &their_cid, closer_script,
+ closee_script, fee_sat, locktime, reply_tlvs);
+ peer_write(pps, take(reply));
+
+ status_debug("Sent closing_sig (as closee), notifying master");
+
+ /* Tell master to validate, store, and handle broadcast. */
+ wire_sync_write(REQ_FD,
+ take(towire_simpleclosed_closee_broadcast(NULL,
+ chosen_tx, &their_sig)));
+ return chosen_tx;
+}
+
+/* As closer: validate the closing_sig we got back.
+ * Returns the closing tx; writes peer's sig to *their_sig_out. */
+static struct bitcoin_tx *handle_closing_sig(
+ const struct bitcoin_outpoint *funding,
+ struct amount_sat funding_sats,
+ const struct pubkey *local_fundingkey,
+ const struct pubkey *remote_fundingkey,
+ u32 *local_wallet_index,
+ const struct ext_key *local_wallet_ext_key,
+ struct amount_sat local_sat,
+ struct amount_sat remote_sat,
+ struct amount_sat dust_limit,
+ const u8 *sent_closer_script,
+ const u8 *sent_closee_script,
+ struct amount_sat sent_fee,
+ u32 sent_locktime,
+ const struct tlv_closing_tlvs *sent_tlvs,
+ const u8 *msg,
+ struct bitcoin_signature *their_sig_out)
+{
+ struct channel_id their_cid = {};
+ u8 *closer_script, *closee_script;
+ struct amount_sat fee_sat;
+ u32 locktime;
+ struct tlv_closing_tlvs *tlvs;
+ const u8 *funding_wscript;
+ struct amount_sat closer_amount, closee_amount;
+
+ if (!fromwire_closing_sig(tmpctx, msg, &their_cid,&closer_script,
+ &closee_script, &fee_sat, &locktime, &tlvs))
+ peer_failed_warn(pps, &their_cid,
+ "Bad closing_sig: %s",
+ tal_hex(tmpctx, msg));
+
+ /* BOLT #2:
+ * The receiver of `closing_sig`:
+ * - If `closer_scriptpubkey`, `closee_scriptpubkey`, `fee_satoshis` or `locktime` don't match what was sent in `closing_complete`:
+ * - MUST either send a `warning` and close the connection, or send an `error` and fail the channel.
+ */
+ if (!scripteq(closer_script, sent_closer_script)
+ || !scripteq(closee_script, sent_closee_script)
+ || !amount_sat_eq(fee_sat, sent_fee)
+ || locktime != sent_locktime)
+ peer_failed_warn(pps, &their_cid, "closing_sig fields don't match our "
+ "closing_complete (script/fee/locktime mismatch)");
+
+ /* BOLT #2:
+ * The receiver of `closing_sig`:
+ * ...
+ * - If `tlvs` does not contain exactly one signature:
+ * - MUST either send a `warning` and close the connection, or send an `error` and fail the channel.
+ */
+ int nsigs = (tlvs->closer_output_only ? 1 : 0)
+ + (tlvs->closee_output_only ? 1 : 0)
+ + (tlvs->closer_and_closee_outputs ? 1 : 0);
+ if (nsigs != 1)
+ peer_failed_warn(pps, &their_cid,
+ "closing_sig must have exactly one sig, got %d",
+ nsigs);
+
+ if (tlvs->closer_output_only && !sent_tlvs->closer_output_only)
+ peer_failed_warn(pps, &their_cid,
+ "closing_sig closer_output_only not in our "
+ "closing_complete");
+ if (tlvs->closee_output_only && !sent_tlvs->closee_output_only)
+ peer_failed_warn(pps, &their_cid,
+ "closing_sig closee_output_only not in our "
+ "closing_complete");
+ if (tlvs->closer_and_closee_outputs
+ && !sent_tlvs->closer_and_closee_outputs)
+ peer_failed_warn(pps, &their_cid,
+ "closing_sig closer_and_closee_outputs not in "
+ "our closing_complete");
+
+ funding_wscript = bitcoin_redeem_2of2(tmpctx, local_fundingkey,
+ remote_fundingkey);
+
+ if (is_valid_op_return(closer_script, tal_bytelen(closer_script)))
+ closer_amount = AMOUNT_SAT(0);
+ else if (!amount_sat_sub(&closer_amount, local_sat, fee_sat))
+ closer_amount = AMOUNT_SAT(0);
+
+ if (is_valid_op_return(closee_script, tal_bytelen(closee_script)))
+ closee_amount = AMOUNT_SAT(0);
+ else
+ closee_amount = remote_sat;
+
+ /* Reconstruct the tx that was signed. */
+ struct bitcoin_tx *signed_tx;
+ struct bitcoin_signature their_sig;
+ their_sig.sighash_type = SIGHASH_ALL;
+
+ if (tlvs->closer_output_only) {
+ their_sig.s = *tlvs->closer_output_only;
+ signed_tx = make_close_tx(tmpctx, local_wallet_index,
+ local_wallet_ext_key, closer_script, NULL, funding_wscript,
+ funding, funding_sats, closer_amount, AMOUNT_SAT(0), locktime);
+ } else if (tlvs->closee_output_only) {
+ their_sig.s = *tlvs->closee_output_only;
+ signed_tx = make_close_tx(tmpctx, NULL, NULL, NULL, closee_script,
+ funding_wscript, funding, funding_sats, AMOUNT_SAT(0),
+ closee_amount, locktime);
+ } else {
+ their_sig.s = *tlvs->closer_and_closee_outputs;
+ signed_tx = make_close_tx(tmpctx, local_wallet_index,
+ local_wallet_ext_key, closer_script, closee_script,
+ funding_wscript, funding, funding_sats, closer_amount,
+ closee_amount, locktime);
+ }
+
+ /* BOLT #2:
+ * The receiver of `closing_sig`:
+ * ...
+ * - If the signature field is not valid for the corresponding closing transaction specified in [BOLT #3]...:
+ * - MUST either send a `warning` and close the connection, or send an `error` and fail the channel.
+ */
+ if (!check_tx_sig(signed_tx, 0, NULL, funding_wscript,
+ remote_fundingkey, &their_sig))
+ peer_failed_warn(pps, &their_cid,
+ "Bad signature in closing_sig: tx=%s funding_wscript=%s fundingkey=%s",
+ fmt_bitcoin_tx(tmpctx, signed_tx),
+ tal_hex(tmpctx, funding_wscript),
+ fmt_pubkey(tmpctx, remote_fundingkey));
+
+ status_debug("closing_sig verified OK, sending to master");
+ *their_sig_out = their_sig;
+ return signed_tx;
+}
+
+int main(int argc, char *argv[])
+{
+ setup_locale();
+
+ const tal_t *ctx = tal(NULL, char);
+ u8 *msg;
+ struct pubkey local_fundingkey, remote_fundingkey;
+ struct bitcoin_outpoint funding;
+ struct amount_sat funding_sats, local_sat, remote_sat, dust_limit;
+ u32 feerate_perkw;
+ u32 *local_wallet_index;
+ struct ext_key *local_wallet_ext_key;
+ u8 *local_script, *remote_script;
+ struct channel_id channel_id;
+ enum side opener;
+ bool got_peer_complete, got_our_sig;
+ struct tlv_closing_tlvs *sent_tlvs;
+ u8 *sent_closer_script, *sent_closee_script;
+ struct amount_sat sent_fee;
+ u32 sent_locktime = 0;
+
+ subdaemon_setup(argc, argv);
+
+ status_setup_sync(REQ_FD);
+
+ msg = wire_sync_read(tmpctx, REQ_FD);
+ if (!fromwire_simpleclosed_init(ctx, msg, &chainparams, &channel_id,
+ &funding, &funding_sats, &local_fundingkey, &remote_fundingkey,
+ &local_sat, &remote_sat, &dust_limit, &feerate_perkw,
+ &local_wallet_index, &local_wallet_ext_key, &local_script,
+ &remote_script, &opener))
+ status_failed(STATUS_FAIL_MASTER_IO,
+ "Bad simpleclosed_init: %s",
+ tal_hex(tmpctx, msg));
+
+ /* stdin == requests, 3 == peer, 4 = hsmd */
+ pps = notleak(new_per_peer_state(ctx));
+ per_peer_state_set_fd(pps, PEER_FD);
+
+ status_debug("Simple close starting: local=%s remote=%s",
+ fmt_amount_sat(tmpctx, local_sat),
+ fmt_amount_sat(tmpctx, remote_sat));
+
+ /* Send our own closing_complete as the closer. */
+ sent_closer_script = local_script;
+ sent_closee_script = remote_script;
+ sent_locktime = 0; /* nLockTime = 0 for initial proposal */
+
+ sent_tlvs = send_closing_complete(&channel_id, &funding, funding_sats,
+ &local_fundingkey, &remote_fundingkey, local_wallet_index,
+ local_wallet_ext_key, local_sat, remote_sat, dust_limit,
+ feerate_perkw, local_script, remote_script, sent_locktime,
+ &sent_fee);
+ /* sent_tlvs is allocated on tmpctx inside send_closing_complete; steal
+ * it onto ctx so it survives clean_tmpctx() calls in the exchange loop. */
+ tal_steal(ctx, sent_tlvs);
+
+ /* Exchange loop: wait for peer's closing_complete and our closing_sig. */
+ got_peer_complete = false;
+ got_our_sig = false;
+
+ while (!got_peer_complete || !got_our_sig) {
+ clean_tmpctx();
+ msg = peer_read(tmpctx, pps);
+
+ switch (fromwire_peektype(msg)) {
+ case WIRE_CLOSING_COMPLETE:
+ /* Peer sends us their closing_complete: we are the closee. */
+ if (got_peer_complete) {
+ status_debug("Got another closing_complete "
+ "(peer bumping fee): re-signing");
+ }
+ handle_closing_complete(&funding, funding_sats,
+ &local_fundingkey, &remote_fundingkey, local_wallet_index,
+ local_wallet_ext_key, local_sat, dust_limit, local_script, msg);
+ got_peer_complete = true;
+ break;
+
+ case WIRE_CLOSING_SIG: {
+ /* Peer replies to our closing_complete: we are the closer. */
+ struct bitcoin_tx *closing_tx;
+ struct bitcoin_txid txid;
+ struct bitcoin_signature their_sig;
+
+ closing_tx = handle_closing_sig(&funding,
+ funding_sats, &local_fundingkey, &remote_fundingkey,
+ local_wallet_index, local_wallet_ext_key, local_sat,
+ remote_sat, dust_limit, sent_closer_script, sent_closee_script,
+ sent_fee, sent_locktime, sent_tlvs, msg, &their_sig);
+ txid = master_got_sig(closing_tx, &their_sig);
+ status_debug("Closer tx stored by master: %s",
+ fmt_bitcoin_txid(tmpctx, &txid));
+ got_our_sig = true;
+ break;
+ }
+
+ case WIRE_CLOSING_SIGNED:
+ /* Peer is still running legacy close — shouldn't happen
+ * if option_simple_close was negotiated. */
+ peer_failed_warn(pps, &channel_id,
+ "Got legacy closing_signed but "
+ "option_simple_close was negotiated");
+ break;
+
+ /* These are all swallowed by connectd */
+ case WIRE_PROTOCOL_BATCH_ELEMENT:
+ case WIRE_CHANNEL_ANNOUNCEMENT:
+ case WIRE_CHANNEL_UPDATE:
+ case WIRE_NODE_ANNOUNCEMENT:
+ case WIRE_QUERY_SHORT_CHANNEL_IDS:
+ case WIRE_QUERY_CHANNEL_RANGE:
+ case WIRE_REPLY_CHANNEL_RANGE:
+ case WIRE_GOSSIP_TIMESTAMP_FILTER:
+ case WIRE_REPLY_SHORT_CHANNEL_IDS_END:
+ case WIRE_PING:
+ case WIRE_PONG:
+ case WIRE_WARNING:
+ case WIRE_ERROR:
+ case WIRE_ONION_MESSAGE:
+ case WIRE_PEER_STORAGE:
+ case WIRE_PEER_STORAGE_RETRIEVAL:
+ /* Ignore unknown messages (e.g. gossip). */
+ status_debug("Ignoring message type %d in simple close",
+ fromwire_peektype(msg));
+ break;
+
+ /* These are unexpected messages. Warn the peer */
+ case WIRE_INIT:
+ case WIRE_TX_ADD_INPUT:
+ case WIRE_TX_ADD_OUTPUT:
+ case WIRE_TX_REMOVE_INPUT:
+ case WIRE_TX_REMOVE_OUTPUT:
+ case WIRE_TX_COMPLETE:
+ case WIRE_TX_SIGNATURES:
+ case WIRE_TX_INIT_RBF:
+ case WIRE_TX_ACK_RBF:
+ case WIRE_TX_ABORT:
+ case WIRE_OPEN_CHANNEL:
+ case WIRE_ACCEPT_CHANNEL:
+ case WIRE_FUNDING_CREATED:
+ case WIRE_FUNDING_SIGNED:
+ case WIRE_CHANNEL_READY:
+ case WIRE_OPEN_CHANNEL2:
+ case WIRE_ACCEPT_CHANNEL2:
+ case WIRE_STFU:
+ case WIRE_SPLICE_INIT:
+ case WIRE_SPLICE_ACK:
+ case WIRE_SPLICE_LOCKED:
+ case WIRE_SHUTDOWN:
+ case WIRE_UPDATE_ADD_HTLC:
+ case WIRE_UPDATE_FULFILL_HTLC:
+ case WIRE_UPDATE_FAIL_HTLC:
+ case WIRE_UPDATE_FAIL_MALFORMED_HTLC:
+ case WIRE_START_BATCH:
+ case WIRE_COMMITMENT_SIGNED:
+ case WIRE_REVOKE_AND_ACK:
+ case WIRE_UPDATE_FEE:
+ case WIRE_UPDATE_BLOCKHEIGHT:
+ case WIRE_CHANNEL_REESTABLISH:
+ case WIRE_ANNOUNCEMENT_SIGNATURES:
+ peer_failed_warn(pps, &channel_id,
+ "Peer sent unexpected message %u (%s)",
+ fromwire_peektype(msg),
+ peer_wire_name(fromwire_peektype(msg)));
+ break;
+ }
+ }
+
+ wire_sync_write(REQ_FD, take(towire_simpleclosed_complete(NULL)));
+ tal_free(ctx);
+ daemon_shutdown();
+}
diff --git a/closingd/simpleclosed_wire.csv b/closingd/simpleclosed_wire.csv
new file mode 100644
index 00000000..c32bdabb
--- /dev/null
+++ b/closingd/simpleclosed_wire.csv
@@ -0,0 +1,49 @@
+#include <bitcoin/signature.h>
+#include <bitcoin/tx.h>
+#include <bitcoin/pubkey.h>
+#include <common/bip32.h>
+#include <common/channel_id.h>
+#include <common/cryptomsg.h>
+#include <common/status_wire.h>
+#include <common/htlc.h>
+#include <common/htlc_wire.h>
+#include <wally_bip32.h>
+
+# Begin! (passes peer_fd and hsm_fd)
+msgtype,simpleclosed_init,3001
+msgdata,simpleclosed_init,chainparams,chainparams,
+msgdata,simpleclosed_init,channel_id,channel_id,
+msgdata,simpleclosed_init,funding,bitcoin_outpoint,
+msgdata,simpleclosed_init,funding_satoshi,amount_sat,
+msgdata,simpleclosed_init,local_fundingkey,pubkey,
+msgdata,simpleclosed_init,remote_fundingkey,pubkey,
+msgdata,simpleclosed_init,local_sat,amount_sat,
+msgdata,simpleclosed_init,remote_sat,amount_sat,
+msgdata,simpleclosed_init,our_dust_limit,amount_sat,
+msgdata,simpleclosed_init,feerate_perkw,u32,
+msgdata,simpleclosed_init,local_wallet_index,?u32,
+msgdata,simpleclosed_init,local_wallet_ext_key,?ext_key,
+msgdata,simpleclosed_init,local_scriptpubkey_len,u16,
+msgdata,simpleclosed_init,local_scriptpubkey,u8,local_scriptpubkey_len
+msgdata,simpleclosed_init,remote_scriptpubkey_len,u16,
+msgdata,simpleclosed_init,remote_scriptpubkey,u8,remote_scriptpubkey_len
+msgdata,simpleclosed_init,opener,enum side,
+
+# simpleclosed tells master it got a valid closing_sig for our closing_complete;
+# master should broadcast this tx.
+msgtype,simpleclosed_got_sig,3002
+msgdata,simpleclosed_got_sig,tx,bitcoin_tx,
+msgdata,simpleclosed_got_sig,sig,bitcoin_signature,
+
+# Master replies with the txid (after storing/broadcasting).
+msgtype,simpleclosed_got_sig_reply,3102
+msgdata,simpleclosed_got_sig_reply,closing_txid,bitcoin_txid,
+
+# simpleclosed tells master it signed the peer's closing tx (as the closee);
+# master validates and stores, drop_to_chain handles broadcast.
+msgtype,simpleclosed_closee_broadcast,3003
+msgdata,simpleclosed_closee_broadcast,tx,bitcoin_tx,
+msgdata,simpleclosed_closee_broadcast,sig,bitcoin_signature,
+
+# Negotiations complete, exiting.
+msgtype,simpleclosed_complete,3004
diff --git a/common/shutdown_scriptpubkey.c b/common/shutdown_scriptpubkey.c
index 6c82baf2..57bf95e9 100644
--- a/common/shutdown_scriptpubkey.c
+++ b/common/shutdown_scriptpubkey.c
@@ -10,7 +10,7 @@
* * `6` to `75` inclusive followed by exactly that many bytes
* * `76` followed by `76` to `80` followed by exactly that many bytes
*/
-static bool is_valid_op_return(const u8 *scriptpubkey, size_t scriptpubkey_len)
+bool is_valid_op_return(const u8 *scriptpubkey, size_t scriptpubkey_len)
{
u8 v;
diff --git a/common/shutdown_scriptpubkey.h b/common/shutdown_scriptpubkey.h
index 9bc0f6f4..8380df13 100644
--- a/common/shutdown_scriptpubkey.h
+++ b/common/shutdown_scriptpubkey.h
@@ -23,6 +23,14 @@
/* We still allow them to specify an old-style P2PKH or P2SH (though we
* never will send such a thing!) if they're not using anchors. */
+/* BOLT #2:
+ * 4. if (and only if) `option_simple_close` is negotiated:
+ * * `OP_RETURN` followed by one of:
+ * * `6` to `75` inclusive followed by exactly that many bytes
+ * * `76` followed by `76` to `80` followed by exactly that many bytes
+ */
+bool is_valid_op_return(const u8 *scriptpubkey, size_t scriptpubkey_len);
+
bool valid_shutdown_scriptpubkey(const u8 *scriptpubkey,
bool anysegwit,
bool allow_oldstyle,
Why this scored 35/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.