From 05a41bfdd27fd657e5d9930a678fcda63a80f709 Mon Sep 17 00:00:00 2001 From: Dusty Daemon Date: Wed, 13 Aug 2025 09:53:16 -0400 Subject: [PATCH 1/9] splice: enable and test mutli-channel splice script Turns on mutli channel splices in splice script and adds a test for it. Changelog-Changed: Splice script now supports splicing over multiple channels --- plugins/spender/splice.c | 9 --------- tests/test_splice.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/plugins/spender/splice.c b/plugins/spender/splice.c index 1c8ce72fa9ff..7a1735d755fa 100644 --- a/plugins/spender/splice.c +++ b/plugins/spender/splice.c @@ -1261,7 +1261,6 @@ validate_splice_cmd(struct splice_cmd *splice_cmd) { struct splice_script_result *action; int paying_fee_count = 0; - int channels = 0; for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) { action = splice_cmd->actions[i]; /* Taking fee from onchain wallet requires recursive looping @@ -1297,14 +1296,6 @@ validate_splice_cmd(struct splice_cmd *splice_cmd) JSONRPC2_INVALID_PARAMS, "Dynamic bitcoin address amounts" " not supported for now"); - if (action->channel_id) { - if (channels) - return command_fail(splice_cmd->cmd, - JSONRPC2_INVALID_PARAMS, - "Multi-channel splice not" - "supported for now"); - channels++; - } if (action->bitcoin_address) return command_fail(splice_cmd->cmd, JSONRPC2_INVALID_PARAMS, diff --git a/tests/test_splice.py b/tests/test_splice.py index a26c983166ad..24084e086692 100644 --- a/tests/test_splice.py +++ b/tests/test_splice.py @@ -195,3 +195,35 @@ def test_script_splice_in(node_factory, bitcoind, chainparams): l1.wait_for_channel_onchain(l2.info['id']) account_info = only_one([acct for acct in l1.rpc.bkpr_listbalances()['accounts'] if acct['account'] == account_id]) assert not account_info['account_closed'] + + +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +def test_script_two_chan_splice_in(node_factory, bitcoind): + l1, l2, l3 = node_factory.line_graph(3, fundamount=1000000, wait_for_announce=True, opts={'experimental-splicing': None}) + + chan_id1 = l2.get_channel_id(l1) + chan_id2 = l2.get_channel_id(l3) + + # l2 will splice funds into the channels with l1 and l3 at the same time + result = l2.rpc.splice(f"wallet -> 200999; 100000 -> {chan_id1}; 100000 -> {chan_id2}; * -> wallet", force_feerate=True, debug_log=True) + + l3.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') + l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') + l1.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') + + wait_for(lambda: len(list(bitcoind.rpc.getrawmempool(True).keys())) == 1) + assert result['txid'] in list(bitcoind.rpc.getrawmempool(True).keys()) + + bitcoind.generate_block(6, wait_for_mempool=1) + + l3.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + l2.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + l1.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + + inv = l2.rpc.invoice(10**2, '1', 'no_1') + l1.rpc.pay(inv['bolt11']) + + inv = l3.rpc.invoice(10**2, '2', 'no_2') + l2.rpc.pay(inv['bolt11']) From fa1da43a860348b65f86e2b4b4be5c3ed2cfa690 Mon Sep 17 00:00:00 2001 From: Dusty Daemon Date: Fri, 15 Aug 2025 21:43:16 -0400 Subject: [PATCH 2/9] tx: witness byte double counted `bitcoin_tx_input_weight` already adds the prefix byte for counting the witness items --- bitcoin/tx.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bitcoin/tx.c b/bitcoin/tx.c index 4c1a63de6c0a..7852ce738e5c 100644 --- a/bitcoin/tx.c +++ b/bitcoin/tx.c @@ -950,8 +950,7 @@ size_t bitcoin_tx_2of2_input_witness_weight(void) /* BOLT #03: * Signatures are 73 bytes long (the maximum length). */ - return 1 + /* Prefix: 4 elements to push on stack */ - (1 + 0) + /* [0]: witness-marker-and-flag */ + return (1 + 0) + /* [0]: witness-marker-and-flag */ (1 + 73) + /* [1] Party A signature and length prefix */ (1 + 73) + /* [2] Party B signature and length prefix */ (1 + 1 + /* [3] length prefix and numpushes (2) */ From 32527dda81ebbd87907391ef0e4282775c1015d0 Mon Sep 17 00:00:00 2001 From: Dusty Daemon Date: Fri, 15 Aug 2025 21:44:47 -0400 Subject: [PATCH 3/9] amount: Add decimal format for msats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In splicing logs we’re often outputting msat values next to sat values, making understanding the logs difficult. Adding a way to output msats as sats with a decimal makes these logs much easier to read. --- common/amount.c | 19 +++++++++++++++ common/amount.h | 3 +++ common/test/run-amount.c | 51 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/common/amount.c b/common/amount.c index 1eae69cefc8c..03a06f40856b 100644 --- a/common/amount.c +++ b/common/amount.c @@ -7,6 +7,7 @@ #include #include #include +#include #include bool amount_sat_to_msat(struct amount_msat *msat, @@ -64,6 +65,24 @@ char *fmt_amount_msat(const tal_t *ctx, struct amount_msat msat) return tal_fmt(ctx, "%"PRIu64"msat", msat.millisatoshis); } +#define LSIZE 5 + +char *fmt_amount_m_as_sat(const tal_t *ctx, struct amount_msat msat) +{ + char last[LSIZE] = {0}; + + if (msat.millisatoshis % MSAT_PER_SAT) { + snprintf(last, LSIZE, ".%03"PRIu64, + msat.millisatoshis % MSAT_PER_SAT); + while(last[strlen(last) - 1] == '0') + last[strlen(last) - 1] = 0; + } + + return tal_fmt(ctx, "%"PRIu64"%ssat", + msat.millisatoshis / MSAT_PER_SAT, + last); +} + const char *fmt_amount_sat_btc(const tal_t *ctx, struct amount_sat sat, bool append_unit) diff --git a/common/amount.h b/common/amount.h index b1cdbac1d570..a5bd6a027e90 100644 --- a/common/amount.h +++ b/common/amount.h @@ -223,6 +223,9 @@ const char *fmt_amount_msat_btc(const tal_t *ctx, /* => 1234msat */ char *fmt_amount_msat(const tal_t *ctx, struct amount_msat msat); +/* => 1234.12sat */ +char *fmt_amount_m_as_sat(const tal_t *ctx, struct amount_msat msat); + /* => 1.23456789btc (8 decimals!) */ const char *fmt_amount_sat_btc(const tal_t *ctx, struct amount_sat sat, diff --git a/common/test/run-amount.c b/common/test/run-amount.c index 0e9f295dc0c2..25a3f7f960a4 100644 --- a/common/test/run-amount.c +++ b/common/test/run-amount.c @@ -346,6 +346,57 @@ int main(int argc, char *argv[]) /* Overflowingly big. */ FAIL_SAT(&sat, "21000000000000000000000000.00000000btc"); + const char *partial_sats[] = + { + "10.111sat", + "10.11sat", + "10.1sat", + "10sat", + "10.001sat", + "10.01sat", + "10.1sat", + "1.111sat", + "1.11sat", + "1.1sat", + "1sat", + "0.111sat", + "0.011sat", + "0.001sat", + "0sat", + NULL, + }; + + u64 msat_amnts[] = + { + 10111, + 10110, + 10100, + 10000, + 10001, + 10010, + 10100, + 1111, + 1110, + 1100, + 1000, + 111, + 11, + 1, + 0, + }; + + assert(sizeof(partial_sats) / sizeof(partial_sats[0]) - 1 + == sizeof(msat_amnts) / sizeof(msat_amnts[0])); + + for (int i = 0; partial_sats[i]; i++) { + msat.millisatoshis = msat_amnts[i]; + printf("Does '%s' equal '%s'\n", + fmt_amount_m_as_sat(tmpctx, msat), + partial_sats[i]); + assert(streq(fmt_amount_m_as_sat(tmpctx, msat), + partial_sats[i])); + } + /* Test fmt_amount_msat_btc, fmt_amount_msat */ for (u64 i = 0; i <= UINT64_MAX / 10; i = i ? i * 10 : 1) { const char *with, *without; From 7a2cd756e87acad7711ca39243d25035a7b4a371 Mon Sep 17 00:00:00 2001 From: Dusty Daemon Date: Fri, 15 Aug 2025 21:47:47 -0400 Subject: [PATCH 4/9] channeld: Inform channeld about opening feerate Since channeld handles splicing, it makes sense that it knows the opening feerate now. This is needed for rounding out splice script errors and logging. --- channeld/channeld.c | 7 ++++++- channeld/channeld_wire.csv | 2 ++ lightningd/channel_control.c | 7 +++++-- tests/plugins/channeld_fakenet.c | 7 ++++--- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index fe7f7707f264..3d65e511f216 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -88,6 +88,9 @@ struct peer { /* Feerate to be used when creating penalty transactions. */ u32 feerate_penalty; + /* Feerate to be used when opening (or splicing) a channel. */ + u32 feerate_opening; + /* Local next per-commit point. */ struct pubkey next_local_per_commit; @@ -6282,7 +6285,8 @@ static void handle_feerates(struct peer *peer, const u8 *inmsg) if (!fromwire_channeld_feerates(inmsg, &feerate, &peer->feerate_min, &peer->feerate_max, - &peer->feerate_penalty)) + &peer->feerate_penalty, + &peer->feerate_opening)) master_badmsg(WIRE_CHANNELD_FEERATES, inmsg); /* BOLT #2: @@ -6653,6 +6657,7 @@ static void init_channel(struct peer *peer) &peer->feerate_min, &peer->feerate_max, &peer->feerate_penalty, + &peer->feerate_opening, &peer->their_commit_sig, &funding_pubkey[REMOTE], &points[REMOTE], diff --git a/channeld/channeld_wire.csv b/channeld/channeld_wire.csv index be5a5c6dee36..47c234392568 100644 --- a/channeld/channeld_wire.csv +++ b/channeld/channeld_wire.csv @@ -31,6 +31,7 @@ msgdata,channeld_init,fee_states,fee_states, msgdata,channeld_init,feerate_min,u32, msgdata,channeld_init,feerate_max,u32, msgdata,channeld_init,feerate_penalty,u32, +msgdata,channeld_init,feerate_opening,u32, msgdata,channeld_init,first_commit_sig,bitcoin_signature, msgdata,channeld_init,remote_fundingkey,pubkey, msgdata,channeld_init,remote_basepoints,basepoints, @@ -329,6 +330,7 @@ msgdata,channeld_feerates,feerate,u32, msgdata,channeld_feerates,min_feerate,u32, msgdata,channeld_feerates,max_feerate,u32, msgdata,channeld_feerates,penalty_feerate,u32, +msgdata,channeld_feerates,opening_feerate,u32, # master -> channeld: do you have a memleak? msgtype,channeld_dev_memleak,1033 diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index 93fb13ec16da..c3af58db6f99 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -94,7 +94,8 @@ void channel_update_feerates(struct lightningd *ld, const struct channel *channe msg = towire_channeld_feerates(NULL, feerate, min_feerate, max_feerate, - penalty_feerate(ld->topology)); + penalty_feerate(ld->topology), + opening_feerate(ld->topology)); subd_send_msg(channel->owner, take(msg)); } @@ -1863,6 +1864,7 @@ bool peer_start_channeld(struct channel *channel, min_feerate, max_feerate, penalty_feerate(ld->topology), + opening_feerate(ld->topology), &channel->last_sig, &channel->channel_info.remote_fundingkey, &channel->channel_info.theirbase, @@ -2654,7 +2656,8 @@ static struct command_result *json_dev_feerate(struct command *cmd, msg = towire_channeld_feerates(NULL, *feerate, feerate_min(cmd->ld, NULL), feerate_max(cmd->ld, NULL), - penalty_feerate(cmd->ld->topology)); + penalty_feerate(cmd->ld->topology), + opening_feerate(cmd->ld->topology)); subd_send_msg(channel->owner, take(msg)); response = json_stream_success(cmd); diff --git a/tests/plugins/channeld_fakenet.c b/tests/plugins/channeld_fakenet.c index fc08d2bf1352..51c702305548 100644 --- a/tests/plugins/channeld_fakenet.c +++ b/tests/plugins/channeld_fakenet.c @@ -963,10 +963,10 @@ static void handle_offer_htlc(struct info *info, const u8 *inmsg) static void handle_feerates(struct info *info, const u8 *inmsg) { - u32 feerate, min, max, penalty; + u32 feerate, min, max, penalty, opening; if (!fromwire_channeld_feerates(inmsg, &feerate, - &min, &max, &penalty)) + &min, &max, &penalty, &opening)) master_badmsg(WIRE_CHANNELD_FEERATES, inmsg); /* BOLT #2: @@ -1058,7 +1058,7 @@ static struct channel *handle_init(struct info *info, const u8 *init_msg) struct secret last_remote_per_commit_secret; struct penalty_base *pbases; struct channel_type *channel_type; - u32 feerate_min, feerate_max, feerate_penalty; + u32 feerate_min, feerate_max, feerate_penalty, feerate_opening; struct pubkey remote_per_commit; struct pubkey old_remote_per_commit; u32 commit_msec; @@ -1101,6 +1101,7 @@ static struct channel *handle_init(struct info *info, const u8 *init_msg) &feerate_min, &feerate_max, &feerate_penalty, + &feerate_opening, &their_commit_sig, &funding_pubkey[REMOTE], &points[REMOTE], From 42521f3ccc8bc23502f449d78c980326f8535418 Mon Sep 17 00:00:00 2001 From: Dusty Daemon Date: Fri, 15 Aug 2025 21:49:20 -0400 Subject: [PATCH 5/9] splice-script: channel id corner case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the rare chance a channel id starts with 02 or 03 we need to check of it’s referring to a channel before parsing it node id, instead of after. --- common/splice_script.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/common/splice_script.c b/common/splice_script.c index 453bc6ca5f3e..fb9f653b6ac2 100644 --- a/common/splice_script.c +++ b/common/splice_script.c @@ -1114,12 +1114,6 @@ static struct splice_script_error *type_data(const tal_t *ctx, input[i]->type = TOK_NODEID; input[i]->node_id = tal(input[i], struct node_id); - if (!node_id_from_hexstr(input[i]->str, - strlen(input[i]->str), - input[i]->node_id)) - return new_error(ctx, INVALID_NODEID, - input[i], - "type_data"); /* Rare corner case where channel begins with * prefix of 02 or 03 */ if (autocomplete_chan_id(input[i], channels, @@ -1137,6 +1131,12 @@ static struct splice_script_error *type_data(const tal_t *ctx, "type_data"); input[i]->type = TOK_CHANID; input[i]->node_id = tal_free(input[i]->node_id); + } else if (!node_id_from_hexstr(input[i]->str, + strlen(input[i]->str), + input[i]->node_id)) { + return new_error(ctx, INVALID_NODEID, + input[i], + "type_data"); } } else if (is_bitcoin_address(input[i]->str)) { input[i]->type = TOK_BTCADDR; From 286d5c66e33bdc3744a47986ecbc4a4c9600daeb Mon Sep 17 00:00:00 2001 From: Dusty Daemon Date: Fri, 15 Aug 2025 22:00:41 -0400 Subject: [PATCH 6/9] splice: Fix weight calculations & use opening feerate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Here we update `psbt_input_get_weight` to allow the caller to specify what kind of input size estimation they would like. Before it was just “zero witness bytes” which we now move to the default behavior and add an assumption option for P2WSH -> 2of2 multisig which is what we typically see in splicing. At the same time we move channeld over to using the opening feerate estimation instead of the less useful “max feerate.” We make splice script use this same feerate. After auditing the feerates for these two places against each other and the actually created transaction, we implement some fixes making them all match. While we’re here, we add relevant debug log messages. Changelog-Changed: Audited and updated splice’s feerate calculations to be precise to the byte — a critical change to accomodate splice script. --- bitcoin/psbt.c | 45 ++++++- bitcoin/psbt.h | 19 ++- bitcoin/test/run-bitcoin_block_from_hex.c | 9 ++ bitcoin/test/run-psbt-from-tx.c | 9 ++ bitcoin/test/run-tx-encode.c | 9 ++ channeld/channeld.c | 139 +++++++++++++--------- lightningd/channel_control.c | 5 +- openingd/dualopend.c | 4 +- plugins/spender/splice.c | 66 +++++++--- 9 files changed, 228 insertions(+), 77 deletions(-) diff --git a/bitcoin/psbt.c b/bitcoin/psbt.c index dff12b040fad..c255dc183c35 100644 --- a/bitcoin/psbt.c +++ b/bitcoin/psbt.c @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -483,6 +484,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) { @@ -595,10 +618,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); @@ -608,8 +637,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 a44bba06636e..b5b186c2ee7d 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/bitcoin/test/run-bitcoin_block_from_hex.c b/bitcoin/test/run-bitcoin_block_from_hex.c index 5f6bb9bb7bfc..ae32aa5d2a96 100644 --- a/bitcoin/test/run-bitcoin_block_from_hex.c +++ b/bitcoin/test/run-bitcoin_block_from_hex.c @@ -97,6 +97,15 @@ void towire_u8(u8 **pptr UNNEEDED, u8 v UNNEEDED) /* Generated stub for towire_u8_array */ void towire_u8_array(u8 **pptr UNNEEDED, const u8 *arr UNNEEDED, size_t num UNNEEDED) { fprintf(stderr, "towire_u8_array called!\n"); abort(); } +/* Generated stub for is_p2wsh */ +bool is_p2wsh(const u8 *script UNNEEDED, size_t script_len UNNEEDED, struct sha256 *addr UNNEEDED) +{ fprintf(stderr, "is_p2wsh called!\n"); abort(); } +/* Generated stub for is_p2tr */ +bool is_p2tr(const u8 *script UNNEEDED, size_t script_len UNNEEDED, u8 xonly_pubkey[32] UNNEEDED) +{ fprintf(stderr, "is_p2tr called!\n"); abort(); } +/* Generated stub for is_p2wpkh */ +bool is_p2wpkh(const u8 *script UNNEEDED, size_t script_len UNNEEDED, struct bitcoin_address *addr UNNEEDED) +{ fprintf(stderr, "is_p2wpkh called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ static const char block[] = diff --git a/bitcoin/test/run-psbt-from-tx.c b/bitcoin/test/run-psbt-from-tx.c index 2b55c420b0a2..d60eceba8971 100644 --- a/bitcoin/test/run-psbt-from-tx.c +++ b/bitcoin/test/run-psbt-from-tx.c @@ -80,6 +80,15 @@ size_t varint_put(u8 buf[VARINT_MAX_LEN] UNNEEDED, varint_t v UNNEEDED) /* Generated stub for varint_size */ size_t varint_size(varint_t v UNNEEDED) { fprintf(stderr, "varint_size called!\n"); abort(); } +/* Generated stub for is_p2wsh */ +bool is_p2wsh(const u8 *script UNNEEDED, size_t script_len UNNEEDED, struct sha256 *addr UNNEEDED) +{ fprintf(stderr, "is_p2wsh called!\n"); abort(); } +/* Generated stub for is_p2tr */ +bool is_p2tr(const u8 *script UNNEEDED, size_t script_len UNNEEDED, u8 xonly_pubkey[32] UNNEEDED) +{ fprintf(stderr, "is_p2tr called!\n"); abort(); } +/* Generated stub for is_p2wpkh */ +bool is_p2wpkh(const u8 *script UNNEEDED, size_t script_len UNNEEDED, struct bitcoin_address *addr UNNEEDED) +{ fprintf(stderr, "is_p2wpkh called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ /* This transaction has scriptSig data in it. diff --git a/bitcoin/test/run-tx-encode.c b/bitcoin/test/run-tx-encode.c index b28f3191c00c..8a70cf2ace54 100644 --- a/bitcoin/test/run-tx-encode.c +++ b/bitcoin/test/run-tx-encode.c @@ -98,6 +98,15 @@ void towire_u8(u8 **pptr UNNEEDED, u8 v UNNEEDED) /* Generated stub for towire_u8_array */ void towire_u8_array(u8 **pptr UNNEEDED, const u8 *arr UNNEEDED, size_t num UNNEEDED) { fprintf(stderr, "towire_u8_array called!\n"); abort(); } +/* Generated stub for is_p2wsh */ +bool is_p2wsh(const u8 *script UNNEEDED, size_t script_len UNNEEDED, struct sha256 *addr UNNEEDED) +{ fprintf(stderr, "is_p2wsh called!\n"); abort(); } +/* Generated stub for is_p2tr */ +bool is_p2tr(const u8 *script UNNEEDED, size_t script_len UNNEEDED, u8 xonly_pubkey[32] UNNEEDED) +{ fprintf(stderr, "is_p2tr called!\n"); abort(); } +/* Generated stub for is_p2wpkh */ +bool is_p2wpkh(const u8 *script UNNEEDED, size_t script_len UNNEEDED, struct bitcoin_address *addr UNNEEDED) +{ fprintf(stderr, "is_p2wpkh called!\n"); abort(); } /* AUTOGENERATED MOCKS END */ const char extended_tx[] = diff --git a/channeld/channeld.c b/channeld/channeld.c index 3d65e511f216..a74fcd53f3d9 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -3153,47 +3153,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; } @@ -3294,8 +3317,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], @@ -3444,33 +3466,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. Opening feerate %"PRIu32 + " * weight %lu / 1000 = %s", + fmt_amount_m_as_sat(tmpctx, initiator_fee), + peer->feerate_opening, + 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)) { @@ -3487,12 +3502,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. Opening feerate %"PRIu32 + " x weight %lu / 1000 = %s (max)", + fmt_amount_m_as_sat(tmpctx, initiator_fee), + peer->feerate_opening, + 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. Opening feerate %"PRIu32 + " x weight %lu / 1000 = %s (max)", + fmt_amount_m_as_sat(tmpctx, initiator_fee), + peer->feerate_opening, + 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)) { @@ -3500,10 +3527,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)) { @@ -3956,6 +3986,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 c3af58db6f99..1c9ef05d770e 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -596,9 +596,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 d140d37b6c4b..995767ff3271 100644 --- a/openingd/dualopend.c +++ b/openingd/dualopend.c @@ -819,14 +819,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 7a1735d755fa..a87fd11454cb 100644 --- a/plugins/spender/splice.c +++ b/plugins/spender/splice.c @@ -122,8 +122,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); @@ -457,41 +457,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, @@ -505,7 +535,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; } @@ -912,11 +946,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) From 0d25471c832aa16fd1199626eae6022b28c55bdd Mon Sep 17 00:00:00 2001 From: Dusty Daemon Date: Fri, 15 Aug 2025 22:01:36 -0400 Subject: [PATCH 7/9] splice-script: wetlog / debuglog fix When using wetlog we might not also have debug log enabled. --- plugins/spender/splice.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/spender/splice.c b/plugins/spender/splice.c index a87fd11454cb..f0df6657fd53 100644 --- a/plugins/spender/splice.c +++ b/plugins/spender/splice.c @@ -74,9 +74,11 @@ static struct command_result *unreserve_get_result(struct command *cmd, &splice_cmd->final_txid); } - json_array_start(response, "log"); - debug_log_to_json(response, splice_cmd->debug_log); - json_array_end(response); + if (splice_cmd->debug_log) { + json_array_start(response, "log"); + debug_log_to_json(response, splice_cmd->debug_log); + json_array_end(response); + } tal_free(abort_pkg); return command_finished(cmd, response); From a5ea89e6e73034ea0d5aaf10cd40bbb6b89a9b19 Mon Sep 17 00:00:00 2001 From: Dusty Daemon Date: Fri, 15 Aug 2025 22:02:44 -0400 Subject: [PATCH 8/9] splice-script: Memleak fix on failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In some splice script failure modes, some memory is leaked. These don’t occur in normal operation — just when an error occurs. This cleans up those memory leaks. --- plugins/spender/splice.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/spender/splice.c b/plugins/spender/splice.c index f0df6657fd53..f2b67e962381 100644 --- a/plugins/spender/splice.c +++ b/plugins/spender/splice.c @@ -181,7 +181,11 @@ static struct command_result *splice_error_pkg(struct command *cmd, const jsmntok_t *error, struct splice_index_pkg *pkg) { - return splice_error(cmd, methodname, buf, error, pkg->splice_cmd); + struct command_result *res = splice_error(cmd, methodname, buf, error, pkg->splice_cmd); + + tal_free(pkg); + + return res; } static struct command_result *calc_in_ppm_and_fee(struct command *cmd, @@ -783,6 +787,8 @@ static struct command_result *splice_signed_error_pkg(struct command *cmd, error->end - error->start); abort_pkg->code = -1; + tal_free(pkg); + return make_error(cmd, abort_pkg, "splice_signed_error"); } From a8b6647f899270ee2e0ce30c5186b9793cf9df33 Mon Sep 17 00:00:00 2001 From: Dusty Daemon Date: Fri, 15 Aug 2025 22:03:55 -0400 Subject: [PATCH 9/9] splice-script: Expand two chan splice in test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable feerate checks for the test now that they’re working well --- tests/test_splice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_splice.py b/tests/test_splice.py index 24084e086692..3ff8a0ee9173 100644 --- a/tests/test_splice.py +++ b/tests/test_splice.py @@ -207,7 +207,7 @@ def test_script_two_chan_splice_in(node_factory, bitcoind): chan_id2 = l2.get_channel_id(l3) # l2 will splice funds into the channels with l1 and l3 at the same time - result = l2.rpc.splice(f"wallet -> 200999; 100000 -> {chan_id1}; 100000 -> {chan_id2}; * -> wallet", force_feerate=True, debug_log=True) + result = l2.rpc.splice(f"wallet -> 200999; 100000 -> {chan_id1}; 100000 -> {chan_id2}") l3.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE')