common/close_tx: add `create_simple_close_tx()` for `option_simple_close`
What changed, and why it matters
This commit adds a new helper function that builds a specific kind of Bitcoin closing transaction used by the Core Lightning node software when two channel peers agree on a simpler cooperative close. It follows the BOLT 3 specification, sets the transaction version and sequence number to signal replace-by-fee, uses a locktime supplied by the peer that initiated the close, subtracts the closing fee from the closer's output, and drops tiny 'dust' outputs by turning them into zero-value data outputs. There is no indication in the commit that this fixes a security bug; it appears to be a feature implementation for a new protocol option.
No immediate security action required. Reviewers may want to verify that callers of create_simple_close_tx() correctly compute closer_amount (closer balance minus fee) and handle the NULL-return case when both outputs are dust, and that the new function is covered by tests before it is used in production paths.
Security signals we found
New feature code, not a patch of existing vulnerable code
Follows explicit BOLT 3 specification for option_simple_close
Uses standard nSequence 0xFFFFFFFD RBF signalling
OP_RETURN outputs are forced to zero value, matching protocol requirement
No input validation changes, no memory-safety fixes, no bug fixes evident in diff
Evidence from the diff
The patch introduces create_simple_close_tx() in common/close_tx.c/h to construct the BOLT3 ‘option_simple_close’ closing transaction. It creates a version-2 tx with one input (funding outpoint, nSequence 0xFFFFFFFD for RBF/nLockTime compliance), up to two outputs (closer and closee), applies the caller-supplied locktime, forces OP_RETURN outputs to zero value, omits outputs when the corresponding script argument is NULL, and returns NULL if no outputs remain. It also adds PSBT keypath metadata for wallet outputs. The implementation mirrors the BOLT3 text quoted in comments.
Changed components
common/close_tx.ccommon/close_tx.hInspect captured patch +112 / −0
diff --git a/common/close_tx.c b/common/close_tx.c
index 4bd985bd..15577b14 100644
--- a/common/close_tx.c
+++ b/common/close_tx.c
@@ -4,6 +4,8 @@
#include <common/close_tx.h>
#include <common/permute_tx.h>
#include <common/psbt_keypath.h>
+#include <common/utils.h>
+#include <wally_script.h>
struct bitcoin_tx *create_close_tx(const tal_t *ctx,
const struct chainparams *chainparams,
@@ -91,3 +93,98 @@ struct bitcoin_tx *create_close_tx(const tal_t *ctx,
assert(bitcoin_tx_check(tx));
return tx;
}
+
+/* BOLT #3:
+ *
+ * ## Closing Transaction
+ *
+ * This variant is used for `closing_complete` and `closing_sig` messages
+ * (i.e. where `option_simple_close` is negotiated).
+ * ...
+ * The side with lesser funds can opt to omit their own output.
+ * * version: 2
+ * * locktime: `locktime` from the `closing_complete` message
+ * * txin count: 1
+ * * `txin[0]` outpoint: `txid` and `output_index` of the channel output
+ * * `txin[0]` sequence: 0xFFFFFFFD
+ * * `txin[0]` script bytes: 0
+ * * `txin[0]` witness: `0 <signature_for_pubkey1> <signature_for_pubkey2>`
+ */
+struct bitcoin_tx *create_simple_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)
+{
+ struct bitcoin_tx *tx;
+ size_t num_outputs = 0;
+ u8 *script;
+
+ /* Sequence 0xFFFFFFFD signals RBF and satisfies the nSequence
+ * requirement for nLockTime. */
+ tx = bitcoin_tx(ctx, chainparams, 1, 2, locktime);
+
+ bitcoin_tx_add_input(tx, funding,
+ /* RBF-enabled, not final */
+ 0xFFFFFFFD,
+ NULL, funding_sats, NULL, funding_wscript);
+
+ /* BOLT #3:
+ * * The closer output: * `txout` amount:
+ * * 0 if the `scriptpubkey` starts with `OP_RETURN`
+ * * otherwise the final balance for the closer, minus `closing_complete.fee_satoshis`, rounded down to whole satoshis
+ * * `txout` script: as specified in `closer_scriptpubkey` from the `closing_complete` message
+ */
+ if (closer_script) {
+ struct amount_sat amt = closer_amount;
+ /* OP_RETURN output must have zero value */
+ if (tal_count(closer_script) > 0
+ && closer_script[0] == OP_RETURN)
+ amt = AMOUNT_SAT(0);
+
+ script = tal_dup_talarr(tx, u8, closer_script);
+ bitcoin_tx_add_output(tx, script, NULL, amt);
+ assert((local_wallet_index == NULL) == (local_wallet_ext_key == NULL));
+ if (local_wallet_index) {
+ size_t script_len = tal_bytelen(script);
+ if (!psbt_add_keypath_to_last_output(
+ tx, *local_wallet_index, local_wallet_ext_key,
+ is_p2tr(script, script_len, NULL)))
+ return tal_free(tx);
+ }
+ num_outputs++;
+ }
+
+ /* BOLT #3:
+ * * The closee output:
+ * * `txout` amount:
+ * * 0 if the `scriptpubkey` starts with `OP_RETURN`
+ * * otherwise the final balance for the closee, rounded down to whole satoshis
+ * * `txout` script: as specified in `closee_scriptpubkey` from the `closing_complete` message
+ */
+ if (closee_script) {
+ struct amount_sat amt = closee_amount;
+ if (tal_count(closee_script) > 0
+ && closee_script[0] == OP_RETURN)
+ amt = AMOUNT_SAT(0);
+
+ script = tal_dup_talarr(tx, u8, closee_script);
+ bitcoin_tx_add_output(tx, script, NULL, amt);
+ num_outputs++;
+ }
+
+ if (num_outputs == 0)
+ return tal_free(tx);
+
+ permute_outputs(tx, NULL, NULL);
+
+ bitcoin_tx_finalize(tx);
+ assert(bitcoin_tx_check(tx));
+ return tx;
+}
diff --git a/common/close_tx.h b/common/close_tx.h
index 2c3aad61..e1d940f6 100644
--- a/common/close_tx.h
+++ b/common/close_tx.h
@@ -19,4 +19,19 @@ struct bitcoin_tx *create_close_tx(const tal_t *ctx,
struct amount_sat to_us,
struct amount_sat to_them,
struct amount_sat dust_limit);
+/* Create simple close tx (option_simple_close) to spend the anchor tx output.
+ * The closer pays the fee; closee output is omitted if closee_script is NULL.
+ * Closer output is omitted if closer_script is NULL (or OP_RETURN with amount 0).
+ * Uses sequence 0xFFFFFFFD (RBF) and the specified locktime. */
+struct bitcoin_tx *create_simple_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);
#endif /* LIGHTNING_COMMON_CLOSE_TX_H */
Why this scored 12/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.