splice script: Implement dynamic wallet & fee
What changed, and why it matters
This commit rewrites the fee and wallet-funding arithmetic for Core Lightning's experimental 'splice script' feature. It introduces a multi-pass solver so that wallet inputs, percentage-based contributions, and on-chain fees can be reconciled when users splice funds into or out of a Lightning channel. The change is large and touches delicate monetary calculations, but it is framed by the author as a feature implementation, not a security fix. There is no disclosed vulnerability or incident tied to this commit.
Treat this as a high-risk feature change rather than a confirmed vulnerability. Reviewers should audit the new multi-pass solver for off-by-one satoshi errors, rounding inconsistencies, and state-machine bugs (especially the SPLICE_CMD_PENDING transitions), and run the project's splice-script test suite against it. No immediate patch or mitigation is indicated by the supplied materials.
Security signals we found
Large rewrite of financial/fee arithmetic in an experimental feature
Recursive solver for wallet funding and fee attribution
Percentage-based (ppm) amount calculations with rounding handling
Dust-limit check added for wallet outputs
No explicit security fix language or CVE reference in commit
No external advisory or incident disclosure supplied
Evidence from the diff
The patch refactors calc_in_ppm_and_fee in plugins/spender/splice.c and adds handle_fee_and_ppm/handle_wallet_fund helpers. The solver now runs repeatedly: it resolves percentage-based (ppm) contributions, adds wallet inputs when needed, accounts for the fact that each added input increases the transaction’s fee, and finally validates that no extra or missing funds remain. It also adds dust-limit handling for wallet outputs and extra debug logging. The code is complex and changes how satoshi amounts, fees, and rounding are computed, which historically is a risky area for Bitcoin/Lightning software, but the diff itself does not show an obvious bug or exploit.
Changed components
plugins/spender/splice.cCore Lightning splice script / spender pluginon-chain wallet funding and fee calculation pathsInspect captured patch +629 / −61
diff --git a/plugins/spender/splice.c b/plugins/spender/splice.c
index 1ee5ca89..8ff30651 100644
--- a/plugins/spender/splice.c
+++ b/plugins/spender/splice.c
@@ -313,63 +313,222 @@ static struct command_result *notice_missing_funds(struct command *cmd,
return result; \
}
+ /* Because wallets increase the fee paying for the fee (due to inputs
+ * increasing transaction size), this process must be inherently recursive.
+ *
+ * Adding to the complication is that wallets may take a percentage of total
+ * funds in the splice before accomodating the fee.
+ *
+ * Finally, any percentage based receiver or contributor of funds may also be
+ * responsible for the fee.
+ *
+ * Supporting all this means we need to build a solver that can be executed
+ * repeatidly, solving what can be solved on each pass. Some answers inherently
+ * require answers from prior passes.
+ *
+ * This method can be called repeatidly and it will solve more of the splice
+ * each time. It must be called once at the end with `final_pass` set to true
+ * to resolve ambigious percentages and place fees in some cases.
+ *
+ * After calling with `final_pass` you are free to call it again and in this
+ * mode it works as an error checker, filling the `extra_funds`,
+ * `missing_funds`, and `non_wallet_demand` values for verification.
+ */
static struct command_result *calc_in_ppm_and_fee(struct command *cmd,
struct splice_cmd *splice_cmd,
- struct amount_sat onchain_fee)
+ struct amount_sat onchain_fee,
+ bool final_pass,
+ struct amount_sat *extra_funds,
+ struct amount_sat *missing_funds,
+ struct amount_sat *non_wallet_demand)
{
- struct splice_script_result *action;
- struct amount_sat out_sats = splice_cmd->initial_funds;
- bool is_any_paying_fee = false;
+ struct splice_script_result *action, *last_ppm_action;
+ struct amount_sat out_sats;
+ bool sub_fee_from_general;
+ struct needed_sats;
+ int ppm_actions;
+ struct splice_script_result *in_wallet, *out_wallet;
+
+
+ add_to_debug_log(splice_cmd, "calc_in_ppm_and_fee");
+
+ plugin_log(cmd->plugin, LOG_DBG, "calc_in_ppm_and_fee starting"
+ " calculations%s", final_pass ? " FINALIZING PASS" : "");
+
+ out_sats = splice_cmd->initial_funds;
+ sub_fee_from_general = true;
+
+ in_wallet = input_wallet(splice_cmd, NULL);
+ out_wallet = output_wallet(splice_cmd);
/* First add all sats going into general fund */
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
action = splice_cmd->actions[i];
- if (action->pays_fee)
- is_any_paying_fee = true;
+ if (action->pays_fee) {
+ sub_fee_from_general = false;
+ /* Has the onchain fee been finalized? */
+ if (action->onchain_wallet) {
+ if (!amount_sat_is_zero(action->out_sat)
+ || !amount_sat_is_zero(action->in_sat))
+ sub_fee_from_general = true;
+ }
+ /* If we're the input wallet -- the fee may be finalized
+ * on the output wallet instead. Check there. */
+ if (action == in_wallet && out_wallet) {
+ if (!amount_sat_is_zero(out_wallet->out_sat)
+ || !amount_sat_is_zero(out_wallet->in_sat)) {
+ sub_fee_from_general = true;
+ }
+
+ }
+ }
+ plugin_log(cmd->plugin, LOG_DBG, " plus %s (pays_fee %s, "
+ "out_ppm %u, out_sat %s, in_ppm %u, in_sat %s)",
+ fmt_amount_sat(tmpctx, action->out_sat),
+ action->pays_fee ? "yes" : "no",
+ action->out_ppm,
+ fmt_amount_sat(tmpctx, action->out_sat),
+ action->in_ppm,
+ fmt_amount_sat(tmpctx, action->in_sat));
if (!amount_sat_add(&out_sats, out_sats, action->out_sat))
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
"Unable to add out_sats");
- if (action->out_ppm)
+ if (action->out_ppm && !action->onchain_wallet)
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
"Unable to resolve out_ppm");
}
+ *non_wallet_demand = AMOUNT_SAT(0);
+ *missing_funds = AMOUNT_SAT(0);
+
/* Now take away all sats being spent by general fund */
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
action = splice_cmd->actions[i];
+ plugin_log(cmd->plugin, LOG_DBG, " minus %s",
+ fmt_amount_sat(tmpctx, action->in_sat));
+ /* Subtract used funds from out_sats */
if (!amount_sat_sub(&out_sats, out_sats, action->in_sat))
+ NOTICE_MISSING(missing_funds, action->in_sat,
+ &out_sats);
+ if (action->onchain_wallet)
+ continue;
+ /* Add up non_wallet_demand (needed for wallet out_ppm) */
+ if (!amount_sat_add(non_wallet_demand, *non_wallet_demand,
+ action->in_sat))
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
- "Unable to sub out_sats");
+ "Unable to add to non_wallet_demand");
+ }
+
+ /* Reduce non_wallet_demand by sats added from channels */
+ for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
+ action = splice_cmd->actions[i];
+ if (!action->channel_id)
+ continue;
+ if (!amount_sat_sub(non_wallet_demand, *non_wallet_demand,
+ action->out_sat))
+ *non_wallet_demand = AMOUNT_SAT(0);
}
/* If no one voulenteers to pay the fee, we take it out of the general
* fund. */
- if (!is_any_paying_fee) {
+ if (sub_fee_from_general) {
+
+ plugin_log(cmd->plugin, LOG_DBG, " remove %s fee from general"
+ " fund %s",
+ fmt_amount_sat(tmpctx, onchain_fee),
+ fmt_amount_sat(tmpctx, out_sats));
+
if (!amount_sat_sub(&out_sats, out_sats, onchain_fee))
+ NOTICE_MISSING(missing_funds, onchain_fee, &out_sats);
+
+ if (!amount_sat_add(non_wallet_demand, *non_wallet_demand,
+ onchain_fee))
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
- tal_fmt(tmpctx,
- "Unable to take onchain fee %s"
- " fromm general funds of %s",
- fmt_amount_sat(tmpctx, onchain_fee),
- fmt_amount_sat(tmpctx, out_sats)));
+ "Unable to add to non_wallet_demand");
}
+ *extra_funds = out_sats;
+
+ plugin_log(cmd->plugin, LOG_DBG, " general fund is %s",
+ fmt_amount_sat(tmpctx, out_sats));
+
+ ppm_actions = 0;
+
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
+ struct amount_sat sat;
action = splice_cmd->actions[i];
if (action->in_ppm) {
/* ppm percentage calculation:
* action->in_sat = out_sats * in_ppm / 1000000 */
- assert(amount_sat_is_zero(action->in_sat));
- if (!amount_sat_mul(&action->in_sat, out_sats, action->in_ppm))
- return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
- "Unable to mul sats & in_ppm");
- action->in_sat = amount_sat_div(action->in_sat, 1000000);
- action->in_ppm = 0;
+ if (!amount_sat_mul(&sat, out_sats, action->in_ppm))
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ "Unable to mul sats & in_ppm");
+
+ sat = amount_sat_div(sat, 1000000);
+
+ if (!amount_sat_add(&action->in_sat, action->in_sat, sat))
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ "Unable to add sats & in_sat");
+ if (!amount_sat_is_zero(sat) || final_pass) {
+ plugin_log(cmd->plugin, LOG_DBG,
+ " resolving percentage, in_ppm calc"
+ " %u of %s = %s",
+ action->in_ppm,
+ fmt_amount_sat(tmpctx, out_sats),
+ fmt_amount_sat(tmpctx, sat));
+ action->in_ppm = 0;
+ }
+
+ /* Remove used sats from extra_funds */
+ if (!amount_sat_sub(extra_funds, *extra_funds, sat)) {
+ /* If we can't do that then add to missing */
+ NOTICE_MISSING(missing_funds, sat,
+ extra_funds);
+ }
+
+ ppm_actions++;
+ last_ppm_action = action;
+ }
+
+ if (final_pass && action->pays_fee
+ && !amount_sat_is_zero(action->in_sat)) {
+ plugin_log(cmd->plugin, LOG_DBG,
+ " subtracting fee of %s from %s",
+ fmt_amount_sat(tmpctx, onchain_fee),
+ fmt_amount_sat(tmpctx, action->in_sat));
+ if (!amount_sat_sub(&action->in_sat, action->in_sat,
+ onchain_fee))
+ NOTICE_MISSING(missing_funds, onchain_fee,
+ &action->in_sat);
+ action->pays_fee = false;
}
+ if (action->pays_fee && !sub_fee_from_general) {
+ plugin_log(cmd->plugin, LOG_DBG,
+ " action pays fee, so removing fee %s from"
+ " extra_funds %s",
+ fmt_amount_sat(tmpctx, onchain_fee),
+ fmt_amount_sat(tmpctx, *extra_funds));
+ if (!amount_sat_sub(extra_funds, *extra_funds,
+ onchain_fee))
+ NOTICE_MISSING(missing_funds, onchain_fee,
+ extra_funds);
+ }
+
+ /* Onchain wallet fees are handled seperately */
+ if (action->onchain_wallet)
+ continue;
+
+ if (!final_pass)
+ continue;
+
/* If this item pays the fee, subtract it from either their
* in_sats or add it to out_sats. */
if (action->pays_fee && !amount_sat_is_zero(action->in_sat)) {
+ plugin_log(cmd->plugin, LOG_DBG, " sub fee %s",
+ fmt_amount_sat(tmpctx, onchain_fee));
if (!amount_sat_sub(&action->in_sat, action->in_sat,
onchain_fee))
return do_fail(cmd, splice_cmd,
@@ -378,6 +537,8 @@ static struct command_result *calc_in_ppm_and_fee(struct command *cmd,
" item in_sat");
}
if (action->pays_fee && !amount_sat_is_zero(action->out_sat)) {
+ plugin_log(cmd->plugin, LOG_DBG, "add fee %s",
+ fmt_amount_sat(tmpctx, onchain_fee));
if (!amount_sat_add(&action->out_sat, action->out_sat,
onchain_fee))
return do_fail(cmd, splice_cmd,
@@ -387,6 +548,30 @@ static struct command_result *calc_in_ppm_and_fee(struct command *cmd,
}
}
+ /* Because of percentage based rounding, we can lose ~1 sat per
+ * percentage amount receiver. If extra sats is at or below 1 per
+ * receiver, we simply dump it in the last ppm receiver. */
+ if (ppm_actions && !amount_sat_is_zero(*extra_funds)
+ && amount_sat_less_eq(*extra_funds, amount_sat(ppm_actions))) {
+ plugin_log(cmd->plugin, LOG_DBG,
+ " placing %s lost during rounding",
+ fmt_amount_sat(tmpctx, *extra_funds));
+ if (!amount_sat_add(&last_ppm_action->in_sat,
+ last_ppm_action->in_sat,
+ *extra_funds))
+ return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
+ "Failed to add extra sats");
+ *extra_funds = AMOUNT_SAT(0);
+ }
+
+ plugin_log(cmd->plugin, LOG_DBG, "calc_in_ppm_and_fee finished."
+ " out_sats: %s, extra_funds: %s, missing_funds: %s,"
+ " non_wallet_demand: %s",
+ fmt_amount_sat(tmpctx, out_sats),
+ fmt_amount_sat(tmpctx, *extra_funds),
+ fmt_amount_sat(tmpctx, *missing_funds),
+ fmt_amount_sat(tmpctx, *non_wallet_demand));
+
/* validate result */
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
action = splice_cmd->actions[i];
@@ -469,10 +654,28 @@ static struct command_result *addpsbt_get_result(struct command *cmd,
JSONRPC2_INVALID_PARAMS,
"Unable to add excess sats");
+ plugin_log(cmd->plugin, LOG_DBG,
+ "Received input(s) with %s",
+ fmt_amount_sat(tmpctx, action->out_sat));
+
out_wallet = output_wallet(splice_cmd);
+ plugin_log(cmd->plugin, LOG_DBG,
+ "Adding excess sats back into out wallet %s"
+ " which already has %s",
+ fmt_amount_sat(tmpctx, excess_sat),
+ out_wallet
+ ? fmt_amount_sat(tmpctx,
+ out_wallet->in_sat)
+ : "(NO WALLET)");
+
+ if (!out_wallet) {
+ plugin_log(cmd->plugin, LOG_DBG, "Generating"
+ " output wallet.");
+ out_wallet = make_wallet(splice_cmd);
+ }
+
if (out_wallet) {
- if (!out_wallet->in_ppm
- && !amount_sat_add(&out_wallet->in_sat,
+ if (!amount_sat_add(&out_wallet->in_sat,
out_wallet->in_sat,
excess_sat))
return do_fail(cmd, splice_cmd,
@@ -507,12 +710,14 @@ static struct command_result *addpsbt_get_result(struct command *cmd,
static struct command_result *onchain_wallet_fund(struct command *cmd,
struct splice_cmd *splice_cmd,
- size_t index)
+ size_t index,
+ struct amount_sat already_funded)
{
struct splice_script_result *action = splice_cmd->actions[index];
struct splice_cmd_action_state *state = splice_cmd->states[index];
struct out_req *req;
struct splice_index_pkg *pkg;
+ struct amount_sat sats;
const char *command;
bool addinginputs = !amount_sat_is_zero(action->out_sat);
@@ -524,21 +729,34 @@ static struct command_result *onchain_wallet_fund(struct command *cmd,
if (addinginputs) {
command = "addpsbtinput";
splice_cmd->wallet_inputs_to_signed++;
- /* DTODO track which specific inputs are added and only sign
- * those */
}
req = jsonrpc_request_start(cmd, command,
addpsbt_get_result,
splice_error_pkg, pkg);
- if (!amount_sat_is_zero(action->out_sat)) {
- json_add_sats(req->js, "satoshi", action->out_sat);
+ if (addinginputs) {
+ sats = action->out_sat;
+ if (!amount_sat_sub(&sats, sats, already_funded))
+ return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
+ tal_fmt(tmpctx,
+ "Internal error; unable to sub"
+ " already_funded %s sats from out_stats"
+ " %s onchain_wallet_fund",
+ fmt_amount_sat(tmpctx, already_funded),
+ fmt_amount_sat(tmpctx, sats)));
+ json_add_sats(req->js, "satoshi", sats);
assert(splice_cmd->feerate_per_kw);
json_add_u32(req->js, "min_feerate", splice_cmd->feerate_per_kw);
+
+ plugin_log(cmd->plugin, LOG_DBG, "Adding input of at least %s",
+ fmt_amount_sat(tmpctx, sats));
}
else {
json_add_sats(req->js, "satoshi", action->in_sat);
+
+ plugin_log(cmd->plugin, LOG_DBG, "Adding output of %s",
+ fmt_amount_sat(tmpctx, action->in_sat));
}
json_add_psbt(req->js, "initialpsbt", splice_cmd->psbt);
@@ -546,7 +764,14 @@ static struct command_result *onchain_wallet_fund(struct command *cmd,
if (addinginputs)
json_add_bool(req->js, "mark_our_inputs", true);
- state->state = SPLICE_CMD_DONE;
+ if (state->state == SPLICE_CMD_PENDING) {
+ plugin_log(cmd->plugin, LOG_DBG, "Not marking index %d done"
+ " because it is pending", (int)index);
+ } else {
+ plugin_log(cmd->plugin, LOG_DBG, "Marking index %d done",
+ (int)index);
+ state->state = SPLICE_CMD_DONE;
+ }
return send_outreq(req);
}
@@ -1055,6 +1280,346 @@ static struct command_result *handle_wetrun(struct command *cmd,
return send_outreq(req);
}
+/* Before calling, ensure `onchain_fee` is accounted for in `needed_funds`.
+ *
+ * If we need to fund from the onchain wallet this requires another pass so
+ * `onchain_fee` will be subtracted out of `needed_funds` and an
+ * `onchain_wallet_fund` command is returned.
+ *
+ * If we are finished funding or we can take the needed funds out of a wallet
+ * output, we return NULL for success and leave `needed_funds` unmolested.
+ */
+static struct command_result *handle_wallet_fund(struct command *cmd,
+ struct splice_cmd *splice_cmd,
+ struct amount_sat onchain_fee)
+{
+ size_t index;
+ struct splice_script_result *input, *output;
+ struct amount_sat already_funded, wallet_funding, missing_funds;
+
+ wallet_funding = wallet_funding_amnt(splice_cmd);
+
+ if (!amount_sat_sub(&missing_funds, splice_cmd->needed_funds,
+ wallet_funding))
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ "Failed to calculate missing funds");
+
+ plugin_log(cmd->plugin, LOG_INFORM, "handle_wallet_fund needed_funds"
+ " %s, current_funds %s, missing_funds %s",
+ fmt_amount_sat(tmpctx, splice_cmd->needed_funds),
+ fmt_amount_sat(tmpctx, wallet_funding),
+ fmt_amount_sat(tmpctx, missing_funds));
+
+ input = input_wallet(splice_cmd, &index);
+ output = output_wallet(splice_cmd);
+
+ if (!input)
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ "Can't fund wallet with no input wallet");
+
+ /* Can we fund the input by just subtracting from the output? */
+ if (output && amount_sat_greater(output->in_sat, missing_funds)) {
+
+ plugin_log(cmd->plugin, LOG_INFORM, "Taking %s"
+ " from output wallet %s to cover fee",
+ fmt_amount_sat(tmpctx, missing_funds),
+ fmt_amount_sat(tmpctx, output->in_sat));
+
+ if (!amount_sat_sub(&output->in_sat, output->in_sat,
+ missing_funds))
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ tal_fmt(tmpctx,
+ "Failed to subtract fee amount %s"
+ " from output %s",
+ fmt_amount_sat(tmpctx, missing_funds),
+ fmt_amount_sat(tmpctx, output->in_sat)));
+
+ return NULL;
+ }
+
+ already_funded = input->out_sat;
+
+ if (!amount_sat_add(&input->out_sat, input->out_sat, missing_funds))
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ "Unable to add missing funds to wallet input");
+
+ /* Retruning `onchain_wallet_fund` means we will need to go around
+ * again and the caller will added the new `onchain_fee` to
+ * `needed_funds`, so we must take the now old amount out. */
+ if (!amount_sat_sub(&splice_cmd->needed_funds,
+ splice_cmd->needed_funds,
+ onchain_fee))
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ "Internal error; unable"
+ " to subtract fee from"
+ " needed_funds");
+
+ plugin_log(cmd->plugin, LOG_INFORM, "Requesting funding"
+ " amount + %s fee wallet inputs for %s"
+ " with %s already_funded",
+ fmt_amount_sat(tmpctx, onchain_fee),
+ fmt_amount_sat(tmpctx, input->out_sat),
+ fmt_amount_sat(tmpctx, already_funded));
+
+ return onchain_wallet_fund(cmd, splice_cmd,
+ index,
+ already_funded);
+}
+
+static struct command_result *handle_fee_and_ppm(struct command *cmd,
+ struct splice_cmd *splice_cmd)
+{
+ struct command_result *result;
+ struct amount_sat onchain_fee;
+ size_t weight;
+ struct amount_sat extra_funds, missing_funds, non_wallet_demand, sat;
+ struct splice_script_result *funding_wallet_action = NULL;
+ size_t funding_wallet_index;
+
+ funding_wallet_action = input_wallet(splice_cmd, &funding_wallet_index);
+
+ /* We calculate the weight with simulated wallet */
+ weight = calc_weight(splice_cmd, true);
+ onchain_fee = amount_tx_fee(splice_cmd->feerate_per_kw, weight);
+
+ plugin_log(cmd->plugin, LOG_INFORM,
+ "Splice fee is %s at %"PRIu32" perkw (%.02f sat/vB) "
+ "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);
+
+ /* If the wallet pays the fee, we need to add input(s) to cover
+ * it. This can potentially need to be done mulitple times since
+ * adding an input increases the needed fee. */
+ if (funding_wallet_action && funding_wallet_action->pays_fee
+ && !funding_wallet_action->out_ppm
+ && splice_cmd->states[funding_wallet_index]->state != SPLICE_CMD_PENDING) {
+
+ result = calc_in_ppm_and_fee(cmd, splice_cmd,
+ onchain_fee,
+ false,
+ &extra_funds,
+ &missing_funds,
+ &non_wallet_demand);
+ if (result)
+ return result;
+
+ if (!amount_sat_add(&splice_cmd->needed_funds,
+ splice_cmd->needed_funds,
+ onchain_fee))
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ "Internal error; unable to add"
+ " fee to needed_funds");
+
+ /* We need to add wallet funds (again?) */
+ if (unresolved_wallet_inputs(splice_cmd)) {
+ result = handle_wallet_fund(cmd, splice_cmd,
+ onchain_fee);
+ if (result)
+ return result;
+ }
+ }
+
+ /* Now we're ready to calculate wallet funding in_ppm. This is
+ * a special case where we take a percentage of the
+ * non_wallet_demand. */
+ if (funding_wallet_action && funding_wallet_action->out_ppm) {
+
+ result = calc_in_ppm_and_fee(cmd, splice_cmd,
+ onchain_fee,
+ false,
+ &extra_funds,
+ &missing_funds,
+ &non_wallet_demand);
+ if (result)
+ return result;
+
+ if (!amount_sat_mul(&sat, non_wallet_demand,
+ funding_wallet_action->out_ppm))
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ "Unable to mul sats & out_ppm");
+ sat = amount_sat_div(sat, 1000000);
+
+ plugin_log(cmd->plugin, LOG_DBG,
+ "Processing wallet percentage,"
+ " non_wallet_demand %s * %uppm = %s",
+ fmt_amount_sat(tmpctx, non_wallet_demand),
+ funding_wallet_action->out_ppm,
+ fmt_amount_sat(tmpctx, sat));
+
+ /* Add onchain fee to `sat` if wallet pays the fee */
+ if (funding_wallet_action->pays_fee) {
+ if (!amount_sat_add(&sat, sat, onchain_fee))
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ "Failed to add onchain fee to"
+ " ppm funding wallet");
+
+ plugin_log(cmd->plugin, LOG_DBG,
+ "Adding onchain_fee %s = %s",
+ fmt_amount_sat(tmpctx, onchain_fee),
+ fmt_amount_sat(tmpctx, sat));
+ }
+
+ if (!amount_sat_is_zero(extra_funds)) {
+ plugin_log(cmd->plugin, LOG_DBG,
+ "Extra funds %s",
+ fmt_amount_sat(tmpctx, extra_funds));
+ }
+
+ /* Marking `out_ppm` as resolved allows the next pass
+ * here to drop down past this block */
+ funding_wallet_action->out_ppm = 0;
+
+ /* If sat resolves to real number, add it to `needed_funds` and
+ * fund it */
+ if (!amount_sat_is_zero(sat)) {
+
+ /* PENDING is a special case that funds the wallet but
+ * keeps it from being marked DONE by the funder */
+ splice_cmd->states[funding_wallet_index]->state = SPLICE_CMD_PENDING;
+
+ if (!amount_sat_add(&splice_cmd->needed_funds,
+ splice_cmd->needed_funds,
+ sat))
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ "Internal error; unable"
+ " to add fee to"
+ " needed_funds (wallet"
+ " ppm)");
+
+ plugin_log(cmd->plugin, LOG_DBG,
+ "Wallet funding pass for sats"
+ " %s, total %s",
+ fmt_amount_sat(tmpctx, sat),
+ fmt_amount_sat(tmpctx, splice_cmd->needed_funds));
+
+ result = handle_wallet_fund(cmd, splice_cmd,
+ AMOUNT_SAT(0));
+ if (result)
+ return result;
+ }
+ }
+
+ /* Here we know `funding_wallet_action->out_ppm` is resolved
+ * but we need to check for repeat funding needs */
+
+ /* If adding funds required more funds to pay for fees, we must repeat
+ * the funding operation started by the `out_ppm` block */
+ if (funding_wallet_action
+ && splice_cmd->states[funding_wallet_index]->state == SPLICE_CMD_PENDING) {
+
+ result = calc_in_ppm_and_fee(cmd, splice_cmd,
+ onchain_fee,
+ false,
+ &extra_funds,
+ &missing_funds,
+ &non_wallet_demand);
+ if (result)
+ return result;
+
+ /* Are we done? */
+ if (amount_sat_is_zero(missing_funds)) {
+ plugin_log(cmd->plugin, LOG_DBG,
+ "Wallet percentage processing done because"
+ " we have no missing funds");
+ splice_cmd->states[funding_wallet_index]->state = SPLICE_CMD_NONE;
+ } else if (funding_wallet_action->pays_fee
+ || !fee_action(splice_cmd)) {
+ /* We only do extra rounds if our wallet pays the fee
+ * or if no one is paying fee (ie fee is paid from)
+ * general funds */
+
+ if (amount_sat_greater(missing_funds, onchain_fee))
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ tal_fmt(tmpctx,
+ "Internal error; should never"
+ " need an extra pass on ppm"
+ " wallet funding that is larger"
+ " than onchain_fee."
+ " missing_funds %s,"
+ " onchain_fee %s",
+ fmt_amount_sat(tmpctx, missing_funds),
+ fmt_amount_sat(tmpctx, onchain_fee)));
+
+ if (!amount_sat_add(&splice_cmd->needed_funds,
+ splice_cmd->needed_funds,
+ missing_funds))
+ return do_fail(cmd, splice_cmd,
+ JSONRPC2_INVALID_PARAMS,
+ "Internal error; unable"
+ " to add fee to"
+ " needed_funds (wallet"
+ " ppm)");
+
+ plugin_log(cmd->plugin, LOG_DBG,
+ "Extra wallet funding pass for missing sats"
+ " %s, total %s",
+ fmt_amount_sat(tmpctx, missing_funds),
+ fmt_amount_sat(tmpctx, splice_cmd->needed_funds));
+
+ result = handle_wallet_fund(cmd, splice_cmd,
+ AMOUNT_SAT(0));
+ if (result)
+ return result;
+ else
+ splice_cmd->states[funding_wallet_index]->state = SPLICE_CMD_NONE;
+ } else {
+ splice_cmd->states[funding_wallet_index]->state = SPLICE_CMD_NONE;
+ }
+ }
+
+ /* Success! */
+ plugin_log(cmd->plugin, LOG_INFORM, "Wallet funding done");
+
+ /* Do a final pass to update values */
+ result = calc_in_ppm_and_fee(cmd, splice_cmd,
+ onchain_fee,
+ true,
+ &extra_funds,
+ &missing_funds,
+ &non_wallet_demand);
+ if (result)
+ return result;
+
+ /* One more pass to check for missing or extra funds */
+ result = calc_in_ppm_and_fee(cmd, splice_cmd,
+ onchain_fee,
+ false,
+ &extra_funds,
+ &missing_funds,
+ &non_wallet_demand);
+ if (result)
+ return result;
+
+ if (!amount_sat_is_zero(extra_funds))
+ return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
+ tal_fmt(tmpctx,
+ "Script calculation ended with"
+ " unclaimed extra funds %s",
+ fmt_amount_sat(tmpctx,
+ extra_funds)));
+ if (!amount_sat_is_zero(missing_funds))
+ return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
+ tal_fmt(tmpctx,
+ "Script is missing %s funds",
+ fmt_amount_sat(tmpctx,
+ missing_funds)));
+
+ return NULL;
+}
+
static struct command_result *continue_splice(struct command *cmd,
struct splice_cmd *splice_cmd)
{
@@ -1062,46 +1627,38 @@ static struct command_result *continue_splice(struct command *cmd,
struct splice_cmd_action_state *state;
struct command_result *result;
size_t index;
- size_t weight;
- struct amount_sat onchain_fee;
bool multiple_require_sigs;
+ struct splice_script_result *funding_wallet_action = NULL;
+ size_t funding_wallet_index;
add_to_debug_log(splice_cmd, "continue_splice");
if (!splice_cmd->feerate_per_kw)
return load_feerate(cmd, splice_cmd);
- /* On first pass we add wallet actions that contribute funds */
- for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
- action = splice_cmd->actions[i];
- state = splice_cmd->states[i];
- if (state->state != SPLICE_CMD_NONE)
- continue;
- if (splice_cmd->actions[i]->onchain_wallet
- && !amount_sat_is_zero(splice_cmd->actions[i]->out_sat)) {
- state->state = SPLICE_CMD_DONE;
- return onchain_wallet_fund(cmd, splice_cmd, i);
- }
- }
+ funding_wallet_action = input_wallet(splice_cmd, &funding_wallet_index);
- if (!splice_cmd->fee_calculated) {
- splice_cmd->fee_calculated = true;
+ /* On first pass we add wallet actions that contribute funds but only
+ * if it is a static amount */
+ if (funding_wallet_action
+ && splice_cmd->states[funding_wallet_index]->state == SPLICE_CMD_NONE
+ && !funding_wallet_action->out_ppm && !funding_wallet_action->pays_fee) {
- /* We calculate the weight simulator wallet outputs */
- weight = calc_weight(splice_cmd, true);
- onchain_fee = amount_tx_fee(splice_cmd->feerate_per_kw, weight);
+ funding_wallet_action->out_sat = splice_cmd->needed_funds;
+ plugin_log(cmd->plugin, LOG_INFORM, "funding static"
+ " wallet inputs for %s",
+ fmt_amount_sat(tmpctx, funding_wallet_action->out_sat));
+ return onchain_wallet_fund(cmd, splice_cmd,
+ funding_wallet_index, AMOUNT_SAT(0));
+ }
- plugin_log(cmd->plugin, LOG_INFORM,
- "Splice fee is %s at %"PRIu32" perkw (%.02f sat/vB) "
- "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);
+ if (!splice_cmd->fee_calculated) {
- result = calc_in_ppm_and_fee(cmd, splice_cmd, onchain_fee);
+ result = handle_fee_and_ppm(cmd, splice_cmd);
if (result)
return result;
+
+ splice_cmd->fee_calculated = true;
}
/* Only after fee calcualtion can we add wallet actions taking funds */
@@ -1110,11 +1667,21 @@ static struct command_result *continue_splice(struct command *cmd,
state = splice_cmd->states[i];
if (state->state != SPLICE_CMD_NONE)
continue;
- if (splice_cmd->actions[i]->onchain_wallet
- && !amount_sat_is_zero(splice_cmd->actions[i]->in_sat)) {
- state->state = SPLICE_CMD_DONE;
- return onchain_wallet_fund(cmd, splice_cmd, i);
+ if (!action->onchain_wallet)
+ continue;
+ /* Add output for wallet funds */
+ if (amount_sat_less(action->in_sat, chainparams->dust_limit)) {
+ plugin_log(cmd->plugin, LOG_INFORM, "Adding a"
+ " wallet output of %s is below"
+ " dust_limit of %s. Leaving dust as"
+ " contribution to fee",
+ fmt_amount_sat(tmpctx, action->in_sat),
+ fmt_amount_sat(tmpctx, chainparams->dust_limit));
+ } else {
+ return onchain_wallet_fund(cmd, splice_cmd, i,
+ AMOUNT_SAT(0));
}
+ state->state = SPLICE_CMD_DONE;
}
result = check_emergency_sat(cmd, splice_cmd);
@@ -1260,8 +1827,9 @@ static struct command_result *execute_splice(struct command *cmd,
state = splice_cmd->states[i];
char *bitcoin_address;
- /* Today UINT32_MAX just means 100%. In the future it might mean
- * something different. */
+ /* `out_ppm` is the percent to take out of the action.
+ * If it is set to '*' we get a value of UINT32_MAX.
+ * In this case we treat it as "take 100% out of the action." */
if (action->out_ppm == UINT32_MAX)
action->out_ppm = 1000000;
Why this scored 23/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.