splice: Fix weight calculations & use opening feerate
What changed, and why it matters
This commit fixes how Core Lightning estimates the size (and therefore the required mining fee) of Bitcoin transactions used in channel splicing. Before, the code underestimated input weights for common 2-of-2 multisig splicing inputs and used a less useful 'maximum feerate' as a safety cap. The patch adds better size-estimation options, switches the safety cap to the 'opening feerate,' and aligns the math between the different components that negotiate a splice. The main risk is that incorrect fee math could cause a splice transaction to be rejected by the network (too low fee) or to overpay/underpay, potentially disrupting channel operations or funds movement.
Treat as a correctness fix with operational-security implications. Review the updated weight calculations against real splicing transactions, run splicing tests with varied input types (P2WSH 2-of-2, P2WPKH, P2TR), and verify that the new opening-feerate cap behaves safely under volatile mempool conditions. Monitor for any follow-up commits that further adjust these calculations.
Security signals we found
Incorrect transaction weight estimation could lead to insufficient fees, causing transactions to stall or be rejected
Use of 'max feerate' as a fee cap could permit overpayment or unsafe fee acceptance; replaced with opening feerate
Splicing involves collaborative transaction construction where one peer's miscalculation can affect the other's funds
Fixes are framed as matching estimated weight against the actually created transaction, implying prior discrepancy
No explicit vulnerability identifier, exploit proof, or security advisory is present in the commit or supplied references
Evidence from the diff
The change refactors psbt_input_get_weight() to accept an enum PSBT_GUESS parameter, allowing callers to choose between a zero-witness fallback and a P2WSH 2-of-2 multisig assumption. It updates channeld, openingd/dualopend, and the spender splice plugin to use the new signature and to consistently account for the channel input/output weights. channeld now uses peer->feerate_opening instead of peer->feerate_max as the local sanity cap for splice fees, and removes the penalty-fee/multiplier logic. Additional debug logging is added throughout the weight/fee calculations. The commit is described by the author as fixing mismatches discovered while auditing feerate calculations against the actually created transaction.
Changed components
bitcoin/psbt.cbitcoin/psbt.hchanneld/channeld.clightningd/channel_control.copeningd/dualopend.cplugins/spender/splice.cInspect captured patch +201 / −77
diff --git a/bitcoin/psbt.c b/bitcoin/psbt.c
index ea4a6c2..6856459 100644
--- a/bitcoin/psbt.c
+++ b/bitcoin/psbt.c
@@ -7,6 +7,7 @@
#include <ccan/ccan/mem/mem.h>
#include <common/utils.h>
#include <wally_psbt.h>
+#include <wally_psbt_members.h>
#include <wire/wire.h>
@@ -480,6 +481,28 @@ void psbt_input_set_witscript(struct wally_psbt *psbt, size_t in, const u8 *wscr
tal_wally_end(psbt);
}
+const u8 *psbt_input_get_witscript(const tal_t *ctx,
+ const struct wally_psbt *psbt,
+ size_t in)
+{
+ size_t witscript_len, written_len;
+ u8 *witscript;
+ if (wally_psbt_get_input_witness_script_len(psbt, in, &witscript_len) != WALLY_OK)
+ abort();
+ witscript = tal_arr(ctx, u8, witscript_len);
+ if (wally_psbt_get_input_witness_script(psbt, in, witscript, witscript_len, &written_len) != WALLY_OK)
+ abort();
+ if (witscript_len != written_len)
+ abort();
+ return witscript;
+}
+
+bool psbt_input_get_ecdsa_sig(const tal_t *ctx,
+ const struct wally_psbt *psbt,
+ size_t in,
+ const struct pubkey *pubkey,
+ struct bitcoin_signature **sig);
+
void psbt_elements_input_set_asset(struct wally_psbt *psbt, size_t in,
struct amount_asset *asset)
{
@@ -592,10 +615,16 @@ struct amount_sat psbt_input_get_amount(const struct wally_psbt *psbt,
}
size_t psbt_input_get_weight(const struct wally_psbt *psbt,
- size_t in)
+ size_t in,
+ enum PSBT_GUESS guess)
{
size_t weight;
const struct wally_map_item *redeem_script;
+ struct wally_psbt_input *input = &psbt->inputs[in];
+ struct wally_tx_output *utxo_out = NULL;
+
+ if (input->utxo)
+ utxo_out = &input->utxo->outputs[input->index];
redeem_script = wally_map_get_integer(&psbt->inputs[in].psbt_fields, /* PSBT_IN_REDEEM_SCRIPT */ 0x04);
@@ -605,8 +634,20 @@ size_t psbt_input_get_weight(const struct wally_psbt *psbt,
weight +=
(redeem_script->value_len +
varint_size(redeem_script->value_len)) * 4;
+ } else if ((guess & PSBT_GUESS_2OF2)
+ && utxo_out
+ && is_p2wsh(utxo_out->script, utxo_out->script_len, NULL)) {
+ weight = bitcoin_tx_input_weight(false,
+ bitcoin_tx_2of2_input_witness_weight());
+ } else if (utxo_out
+ && is_p2wpkh(utxo_out->script, utxo_out->script_len, NULL)) {
+ weight = bitcoin_tx_input_weight(false,
+ bitcoin_tx_input_witness_weight(UTXO_P2SH_P2WPKH));
+ } else if (utxo_out
+ && is_p2tr(utxo_out->script, utxo_out->script_len, NULL)) {
+ weight = bitcoin_tx_input_weight(false,
+ bitcoin_tx_input_witness_weight(UTXO_P2TR));
} else {
- /* zero scriptSig length */
weight += varint_size(0) * 4;
}
diff --git a/bitcoin/psbt.h b/bitcoin/psbt.h
index a44bba0..b5b186c 100644
--- a/bitcoin/psbt.h
+++ b/bitcoin/psbt.h
@@ -207,6 +207,10 @@ WARN_UNUSED_RESULT bool psbt_input_get_ecdsa_sig(const tal_t *ctx,
void psbt_input_set_witscript(struct wally_psbt *psbt, size_t in, const u8 *wscript);
+const u8 *psbt_input_get_witscript(const tal_t *ctx,
+ const struct wally_psbt *psbt,
+ size_t in);
+
/* psbt_input_set_unknown - Set the given Key-Value in the psbt's input keymap
* @ctx - tal context for allocations
* @in - psbt input to set key-value on
@@ -265,9 +269,20 @@ void psbt_output_set_unknown(const tal_t *ctx,
struct amount_sat psbt_input_get_amount(const struct wally_psbt *psbt,
size_t in);
-/* psbt_input_get_weight - Calculate the tx weight for input index `in` */
+enum PSBT_GUESS {
+ PSBT_GUESS_ZERO = 0x0, /* Assume unknown is 0 bytes (fallback) */
+ PSBT_GUESS_2OF2 = 0x1, /* Assume P2WSH is 2of2 multisig (req prevtx) */
+};
+
+/* psbt_input_get_weight - Calculate the tx weight for input index `in`.
+ *
+ * @psbt - psbt
+ * @in - index of input who's weight you want
+ * @guess - How to guess if we have incomplete information
+ * */
size_t psbt_input_get_weight(const struct wally_psbt *psbt,
- size_t in);
+ size_t in,
+ enum PSBT_GUESS guess);
/* psbt_output_get_amount - Returns the value of this output
*
diff --git a/channeld/channeld.c b/channeld/channeld.c
index 65831fb..55b38e6 100644
--- a/channeld/channeld.c
+++ b/channeld/channeld.c
@@ -3229,47 +3229,70 @@ static struct wally_psbt_output *find_channel_output(struct peer *peer,
return NULL;
}
-static size_t calc_weight(enum tx_role role, const struct wally_psbt *psbt)
+static size_t calc_weight(enum tx_role role, const struct wally_psbt *psbt,
+ bool log_math)
{
- size_t weight = 0;
+ size_t lweight = 0, weight = 0;
- /* BOLT #2:
- * The *initiator* is responsible for paying the fees for the following fields,
- * to be referred to as the `common fields`.
- *
- * - version
- * - segwit marker + flag
- * - input count
- * - output count
- * - locktime
- */
- if (role == TX_INITIATOR)
- weight += bitcoin_tx_core_weight(psbt->num_inputs,
- psbt->num_outputs);
+ if (log_math)
+ status_debug("Counting tx weight;");
/* BOLT #2:
* The rest of the transaction bytes' fees are the responsibility of
* the peer who contributed that input or output via `tx_add_input` or
* `tx_add_output`, at the agreed upon `feerate`.
*/
- for (size_t i = 0; i < psbt->num_inputs; i++)
+ for (size_t i = 0; i < psbt->num_inputs; i++) {
if (is_initiators_serial(&psbt->inputs[i].unknowns)) {
if (role == TX_INITIATOR)
- weight += psbt_input_get_weight(psbt, i);
+ weight += psbt_input_get_weight(psbt, i, PSBT_GUESS_2OF2);
}
- else
+ else {
if (role != TX_INITIATOR)
- weight += psbt_input_get_weight(psbt, i);
+ weight += psbt_input_get_weight(psbt, i, PSBT_GUESS_2OF2);
+ }
+ if (log_math)
+ status_debug(" Adding input"
+ " %lu; weight: %lu", i, weight - lweight);
+ lweight = weight;
+ }
- for (size_t i = 0; i < psbt->num_outputs; i++)
+ for (size_t i = 0; i < psbt->num_outputs; i++) {
if (is_initiators_serial(&psbt->outputs[i].unknowns)) {
if (role == TX_INITIATOR)
weight += psbt_output_get_weight(psbt, i);
}
- else
+ else {
if (role != TX_INITIATOR)
weight += psbt_output_get_weight(psbt, i);
+ }
+ if (log_math)
+ status_debug(" Adding output"
+ " %lu; weight: %lu", i, weight - lweight);
+ lweight = weight;
+ }
+ /* BOLT #2:
+ * The *initiator* is responsible for paying the fees for the following fields,
+ * to be referred to as the `common fields`.
+ *
+ * - version
+ * - segwit marker + flag
+ * - input count
+ * - output count
+ * - locktime
+ */
+ if (role == TX_INITIATOR) {
+ weight += bitcoin_tx_core_weight(psbt->num_inputs,
+ psbt->num_outputs);
+ if (log_math)
+ status_debug(" Adding bitcoin_tx_core_weight;"
+ " weight: %lu", weight - lweight);
+ lweight = weight;
+ }
+
+ if (log_math)
+ status_debug("Total weight: %lu", weight);
return weight;
}
@@ -3370,8 +3393,7 @@ static struct amount_sat check_balances(struct peer *peer,
{
struct amount_sat min_initiator_fee, min_accepter_fee,
max_initiator_fee, max_accepter_fee,
- funding_amount_res, min_multiplied,
- initiator_penalty_fee, accepter_penalty_fee;
+ funding_amount_res;
struct amount_msat funding_amount,
initiator_fee, accepter_fee;
struct amount_msat in[NUM_TX_ROLES], out[NUM_TX_ROLES],
@@ -3533,33 +3555,26 @@ static struct amount_sat check_balances(struct peer *peer,
"amount_sat_less / amount_sat_sub mismtach");
min_initiator_fee = amount_tx_fee(peer->splicing->feerate_per_kw,
- calc_weight(TX_INITIATOR, psbt));
+ calc_weight(TX_INITIATOR, psbt, false));
min_accepter_fee = amount_tx_fee(peer->splicing->feerate_per_kw,
- calc_weight(TX_ACCEPTER, psbt));
+ calc_weight(TX_ACCEPTER, psbt, false));
/* As a safeguard max feerate is checked (only) locally, if it's
* particularly high we fail and tell the user but allow them to
* override with `splice_force_feerate` */
- max_accepter_fee = amount_tx_fee(peer->feerate_max,
- calc_weight(TX_ACCEPTER, psbt));
- max_initiator_fee = amount_tx_fee(peer->feerate_max,
- calc_weight(TX_INITIATOR, psbt));
- initiator_penalty_fee = amount_tx_fee(peer->feerate_penalty,
- calc_weight(TX_INITIATOR, psbt));
- accepter_penalty_fee = amount_tx_fee(peer->feerate_penalty,
- calc_weight(TX_ACCEPTER, psbt));
-
- /* Sometimes feerate_max is some absurdly high value, in that case we
- * give a fee warning based of a multiple of the min value. */
- amount_sat_mul(&min_multiplied, min_accepter_fee, 5);
- max_accepter_fee = SAT_MIN(min_multiplied, max_accepter_fee);
- if (amount_sat_greater(accepter_penalty_fee, max_accepter_fee))
- max_accepter_fee = accepter_penalty_fee;
-
- amount_sat_mul(&min_multiplied, min_initiator_fee, 5);
- max_initiator_fee = SAT_MIN(min_multiplied, max_initiator_fee);
- if (amount_sat_greater(initiator_penalty_fee, max_initiator_fee))
- max_initiator_fee = initiator_penalty_fee;
+ max_accepter_fee = amount_tx_fee(peer->feerate_opening,
+ calc_weight(TX_ACCEPTER, psbt, false));
+ max_initiator_fee = amount_tx_fee(peer->feerate_opening,
+ calc_weight(TX_INITIATOR, psbt, opener));
+
+ if (opener) {
+ status_debug("User specified fee of %s. Splice feerate %"PRIu32
+ " * weight %lu / 1000 = %s",
+ fmt_amount_m_as_sat(tmpctx, initiator_fee),
+ peer->feerate_splice,
+ calc_weight(TX_INITIATOR, psbt, false),
+ fmt_amount_sat(tmpctx, max_initiator_fee));
+ }
/* Check initiator fee */
if (amount_msat_less_sat(initiator_fee, min_initiator_fee)) {
@@ -3576,12 +3591,24 @@ static struct amount_sat check_balances(struct peer *peer,
&& amount_msat_greater_sat(initiator_fee, max_initiator_fee)) {
msg = towire_channeld_splice_feerate_error(NULL, initiator_fee,
true);
+ status_debug("Our own fee (%s) is too high to use without"
+ " forcing. Splice feerate %"PRIu32
+ " x weight %lu / 1000 = %s (max)",
+ fmt_amount_m_as_sat(tmpctx, initiator_fee),
+ peer->feerate_splice,
+ calc_weight(TX_INITIATOR, psbt, false),
+ fmt_amount_sat(tmpctx, max_initiator_fee));
+
wire_sync_write(MASTER_FD, take(msg));
+
splice_abort(peer,
- "Our own fee (%s) was too high, max without"
- " forcing is %s.",
- fmt_amount_msat(tmpctx, initiator_fee),
- fmt_amount_sat(tmpctx, max_initiator_fee));
+ "Our own fee (%s) is too high to use without"
+ " forcing. Splice feerate %"PRIu32
+ " x weight %lu / 1000 = %s (max)",
+ fmt_amount_m_as_sat(tmpctx, initiator_fee),
+ peer->feerate_splice,
+ calc_weight(TX_INITIATOR, psbt, false),
+ fmt_amount_sat(tmpctx, max_initiator_fee));
}
/* Check accepter fee */
if (amount_msat_less_sat(accepter_fee, min_accepter_fee)) {
@@ -3589,10 +3616,13 @@ static struct amount_sat check_balances(struct peer *peer,
false);
wire_sync_write(MASTER_FD, take(msg));
splice_abort(peer,
- "%s fee (%s) was too low, must be at least %s",
- opener ? "Your" : "Our",
- fmt_amount_msat(tmpctx, accepter_fee),
- fmt_amount_sat(tmpctx, min_accepter_fee));
+ "%s fee (%s) was too low, must be at least %s"
+ " weight: %"PRIu64", feerate_max: %"PRIu32,
+ opener ? "Your" : "Our",
+ fmt_amount_msat(tmpctx, accepter_fee),
+ fmt_amount_sat(tmpctx, min_accepter_fee),
+ calc_weight(TX_INITIATOR, psbt, false),
+ peer->feerate_opening);
}
if (!peer->splicing->force_feerate && !opener
&& amount_msat_greater_sat(accepter_fee, max_accepter_fee)) {
@@ -4047,6 +4077,9 @@ static void resume_splice_negotiation(struct peer *peer,
peer->splicing = tal_free(peer->splicing);
+ if (our_role == TX_INITIATOR)
+ calc_weight(TX_INITIATOR, current_psbt, true);
+
final_tx = bitcoin_tx_with_psbt(tmpctx, current_psbt);
msg = towire_channeld_splice_confirmed_signed(tmpctx, final_tx,
new_output_index);
diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c
index aac597e..e8d6480 100644
--- a/lightningd/channel_control.c
+++ b/lightningd/channel_control.c
@@ -617,9 +617,10 @@ static void send_splice_tx(struct channel *channel,
u8* tx_bytes = linearize_tx(tmpctx, tx);
log_debug(channel->log,
- "Broadcasting splice tx %s for channel %s.",
+ "Broadcasting splice tx %s for channel %s. Final weight %lu",
tal_hex(tmpctx, tx_bytes),
- fmt_channel_id(tmpctx, &channel->cid));
+ fmt_channel_id(tmpctx, &channel->cid),
+ bitcoin_tx_weight(tx));
struct send_splice_info *info = tal(NULL, struct send_splice_info);
diff --git a/openingd/dualopend.c b/openingd/dualopend.c
index 2b3ff44..8c4cdbb 100644
--- a/openingd/dualopend.c
+++ b/openingd/dualopend.c
@@ -818,14 +818,14 @@ static char *check_balances(const tal_t *ctx,
assert(ok);
initiator_weight +=
- psbt_input_get_weight(psbt, i);
+ psbt_input_get_weight(psbt, i, PSBT_GUESS_ZERO);
} else {
ok = amount_sat_add(&accepter_inputs,
accepter_inputs, amt);
assert(ok);
accepter_weight +=
- psbt_input_get_weight(psbt, i);
+ psbt_input_get_weight(psbt, i, PSBT_GUESS_ZERO);
}
}
tot_output_amt = AMOUNT_SAT(0);
diff --git a/plugins/spender/splice.c b/plugins/spender/splice.c
index e500070..005b930 100644
--- a/plugins/spender/splice.c
+++ b/plugins/spender/splice.c
@@ -125,8 +125,8 @@ static struct command_result *do_fail(struct command *cmd,
splice_cmd->wetrun = false;
plugin_log(cmd->plugin, LOG_DBG,
- "splice_error(psbt:%p, splice_cmd_stat:%p)",
- splice_cmd->psbt, splice_cmd);
+ "splice_error(psbt:%p, splice_cmd:%p, str: %s)",
+ splice_cmd->psbt, splice_cmd, str ?: "");
abort_pkg = tal(cmd->plugin, struct abort_pkg);
abort_pkg->splice_cmd = tal_steal(abort_pkg, splice_cmd);
@@ -464,41 +464,71 @@ static size_t calc_weight(struct splice_cmd *splice_cmd,
bool simulate_wallet_outputs)
{
struct splice_script_result *action;
+ struct plugin *plugin = splice_cmd->cmd->plugin;
struct wally_psbt *psbt = splice_cmd->psbt;
- size_t weight = 0;
+ size_t lweight = 0, weight = 0;
size_t extra_inputs = 0;
size_t extra_outputs = 0;
+ plugin_log(plugin, LOG_DBG, "Counting potenetial tx weight;");
+
/* BOLT #2:
* The rest of the transaction bytes' fees are the responsibility of
* the peer who contributed that input or output via `tx_add_input` or
* `tx_add_output`, at the agreed upon `feerate`.
*/
- for (size_t i = 0; i < psbt->num_inputs; i++)
- weight += psbt_input_get_weight(psbt, i);
+ for (size_t i = 0; i < psbt->num_inputs; i++) {
+ weight += psbt_input_get_weight(psbt, i, PSBT_GUESS_2OF2);
+ plugin_log(plugin, LOG_DBG, " Adding input; weight: %lu",
+ weight - lweight);
+ lweight = weight;
+ }
- for (size_t i = 0; i < psbt->num_outputs; i++)
- weight += psbt_output_get_weight(psbt, i);
+ /* Count the splice input manually */
+ for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
+ action = splice_cmd->actions[i];
+ if (splice_cmd->actions[i]->channel_id) {
+ weight += bitcoin_tx_input_weight(false,
+ bitcoin_tx_2of2_input_witness_weight());
+ plugin_log(plugin, LOG_DBG, " Adding input"
+ " (simulated channel); weight:"
+ " %lu", weight - lweight);
+ lweight = weight;
+ extra_inputs++;
+ }
+ }
- /* Count the splice input & outputs manually */
+ /* Count the splice outputs manually */
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
action = splice_cmd->actions[i];
if (simulate_wallet_outputs && action->onchain_wallet) {
if (!amount_sat_is_zero(action->in_sat) || action->in_ppm) {
weight += bitcoin_tx_output_weight(BITCOIN_SCRIPTPUBKEY_P2TR_LEN);
extra_outputs++;
+ plugin_log(plugin, LOG_DBG, " Adding output"
+ " (simulated wallet); weight:"
+ " %lu", weight - lweight);
+ lweight = weight;
}
-
- } else if (splice_cmd->actions[i]->channel_id) {
+ assert(!splice_cmd->actions[i]->channel_id);
+ }
+ if (splice_cmd->actions[i]->channel_id) {
weight += bitcoin_tx_output_weight(BITCOIN_SCRIPTPUBKEY_P2WSH_LEN);
- weight += bitcoin_tx_input_weight(true,
- bitcoin_tx_2of2_input_witness_weight());
- extra_inputs++;
+ plugin_log(plugin, LOG_DBG, " Adding output"
+ " (simulated channel); weight:"
+ " %lu", weight - lweight);
+ lweight = weight;
+
extra_outputs++;
}
}
- /* DTODO make a test to confirm weight calculation is correct */
+ for (size_t i = 0; i < psbt->num_outputs; i++) {
+ weight += psbt_output_get_weight(psbt, i);
+ plugin_log(plugin, LOG_DBG, " Adding output; weight: %lu",
+ weight - lweight);
+ lweight = weight;
+ }
/* BOLT #2:
* The *initiator* is responsible for paying the fees for the following fields,
@@ -512,7 +542,11 @@ static size_t calc_weight(struct splice_cmd *splice_cmd,
*/
weight += bitcoin_tx_core_weight(psbt->num_inputs + extra_inputs,
psbt->num_outputs + extra_outputs);
+ plugin_log(plugin, LOG_DBG, " Adding bitcoin_tx_core_weight;"
+ " weight: %lu", weight - lweight);
+ lweight = weight;
+ plugin_log(plugin, LOG_DBG, " Total weight: %lu", weight);
return weight;
}
@@ -921,11 +955,11 @@ static struct command_result *continue_splice(struct command *cmd,
plugin_log(cmd->plugin, LOG_INFORM,
"Splice fee is %s at %"PRIu32" perkw (%.02f sat/vB) "
- "on tx where our personal vbytes are %.02f",
+ "on tx where our weight units are %lu",
fmt_amount_sat(tmpctx, onchain_fee),
splice_cmd->feerate_per_kw,
4 * splice_cmd->feerate_per_kw / 1000.0f,
- weight / 4.0f);
+ weight);
result = calc_in_ppm_and_fee(cmd, splice_cmd, onchain_fee);
if (result)
Why this scored 42/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.