core-lightning/plugins/spender/splice.c
Dusty Daemon 7fd16dc493 splice: Add plugin for magic “splice all” command
The command called “splice” can take a json payload or a ‘splice script’, process it into a list of ‘actions’ and then execute those actions.

These actions include or will include everything you would want to do with a splice:
* Splice into a channel
* Splice out of a channel
* Fund from wallet
* Deposit to wallet
* Send funds to bitcoin address

Changelog-Added: A new magic “splice” command is added that can take a ‘splice script’ or json payload and perform any complex splice across multiple channels merging the result into a single transaction. Some features are disabled and will be added in time.
2024-11-12 06:42:52 +10:30

1449 lines
43 KiB
C

#include "config.h"
#include <bitcoin/psbt.h>
#include <bitcoin/script.h>
#include <ccan/array_size/array_size.h>
#include <ccan/err/err.h>
#include <ccan/tal/str/str.h>
#include <ccan/tal/tal.h>
#include <common/addr.h>
#include <common/json_param.h>
#include <common/json_parse.h>
#include <common/json_stream.h>
#include <common/psbt_open.h>
#include <common/splice_script.h>
#include <plugins/spender/splice.h>
struct abort_pkg {
struct splice_cmd *splice_cmd;
enum jsonrpc_errcode code;
char *str;
};
static void debug_log_to_json(struct json_stream *response,
const char *debug_log)
{
char **lines = tal_strsplit(tmpctx, debug_log, "\n", STR_NO_EMPTY);
for (size_t i = 0; lines[i]; i++)
json_add_string(response, NULL, lines[i]);
}
static struct command_result *make_error(struct command *cmd,
struct abort_pkg *abort_pkg,
const char *phase)
{
struct splice_cmd *splice_cmd = abort_pkg->splice_cmd;
char *str = abort_pkg->str;
struct json_stream *response = jsonrpc_stream_fail(cmd,
abort_pkg->code,
str ?: phase);
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);
}
static struct command_result *unreserve_get_result(struct command *cmd,
const char *methodname,
const char *buf,
const jsmntok_t *result,
struct abort_pkg *abort_pkg)
{
struct splice_cmd *splice_cmd = abort_pkg->splice_cmd;
struct json_stream *response;
struct bitcoin_tx *tx;
u8 *tx_bytes;
if (splice_cmd->wetrun) {
response = jsonrpc_stream_success(cmd);
if (splice_cmd->psbt) {
json_add_psbt(response, "psbt", splice_cmd->psbt);
tx = bitcoin_tx_with_psbt(tmpctx, splice_cmd->psbt);
tx_bytes = linearize_tx(tmpctx, tx);
json_add_hex(response, "tx", tx_bytes,
tal_bytelen(tx_bytes));
json_add_txid(response, "txid",
&splice_cmd->final_txid);
}
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);
}
return make_error(cmd, abort_pkg, "unreserve_get_result");
}
static struct command_result *abort_get_result(struct command *cmd,
const char *methodname,
const char *buf,
const jsmntok_t *result,
struct abort_pkg *abort_pkg)
{
struct out_req *req;
struct splice_cmd *splice_cmd = abort_pkg->splice_cmd;
plugin_log(cmd->plugin, LOG_DBG,
"unreserveinputs(psbt:%p)", splice_cmd->psbt);
if (!splice_cmd->psbt)
return make_error(cmd, abort_pkg, "abort_get_result");
req = jsonrpc_request_start(cmd, "unreserveinputs",
unreserve_get_result, forward_error,
abort_pkg);
json_add_psbt(req->js, "psbt", splice_cmd->psbt);
return send_outreq(req);
}
static struct command_result *do_fail(struct command *cmd,
struct splice_cmd *splice_cmd,
enum jsonrpc_errcode code,
const char *str TAKES)
{
struct out_req *req;
struct abort_pkg *abort_pkg;
size_t added;
/* If we encounter an error, wetrun is canceled */
splice_cmd->wetrun = false;
plugin_log(cmd->plugin, LOG_DBG,
"splice_error(psbt:%p, splice_cmd_stat:%p)",
splice_cmd->psbt, splice_cmd);
abort_pkg = tal(cmd->plugin, struct abort_pkg);
abort_pkg->splice_cmd = tal_steal(abort_pkg, splice_cmd);
abort_pkg->str = tal_strdup(abort_pkg, str);
abort_pkg->code = code;
req = jsonrpc_request_start(cmd, "abort_channels",
abort_get_result, forward_error, abort_pkg);
added = 0;
json_array_start(req->js, "channel_ids");
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
if (splice_cmd->actions[i]->channel_id) {
added++;
json_add_channel_id(req->js, NULL,
splice_cmd->actions[i]->channel_id);
}
}
json_array_end(req->js);
if (!added) {
plugin_log(cmd->plugin, LOG_DBG,
"No channels were stfu'ed, skipping to unreserve"
" (psbt:%p)", splice_cmd->psbt);
return abort_get_result(cmd, NULL, NULL, NULL, abort_pkg);
}
return send_outreq(req);
}
static struct command_result *splice_error(struct command *cmd,
const char *methodname,
const char *buf,
const jsmntok_t *error,
struct splice_cmd *splice_cmd)
{
char *str = tal_fmt(NULL, "%s: %.*s",
methodname,
error->end - error->start,
buf + error->start);
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS, take(str));
}
struct splice_index_pkg {
struct splice_cmd *splice_cmd;
size_t index;
};
static struct command_result *splice_error_pkg(struct command *cmd,
const char *methodname,
const char *buf,
const jsmntok_t *error,
struct splice_index_pkg *pkg)
{
return splice_error(cmd, methodname, buf, error, pkg->splice_cmd);
}
static struct command_result *calc_in_ppm_and_fee(struct command *cmd,
struct splice_cmd *splice_cmd,
struct amount_sat onchain_fee)
{
struct splice_script_result *action;
struct amount_sat out_sats = splice_cmd->initial_funds;
bool is_any_paying_fee = false;
/* 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 (!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)
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
"Unable to resolve out_ppm");
}
/* 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];
if (!amount_sat_sub(&out_sats, out_sats, action->in_sat))
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
"Unable to sub out_sats");
}
/* If no one voulenteers to pay the fee, we take it out of the general
* fund. */
if (!is_any_paying_fee) {
if (!amount_sat_sub(&out_sats, out_sats, 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)));
}
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
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 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)) {
if (!amount_sat_sub(&action->in_sat, action->in_sat,
onchain_fee))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Unable to sub fee from"
" item in_sat");
}
if (action->pays_fee && !amount_sat_is_zero(action->out_sat)) {
if (!amount_sat_add(&action->out_sat, action->out_sat,
onchain_fee))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Unable to add fee to"
" item out_sat");
}
}
/* validate result */
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_is_zero(action->in_sat))
continue;
if (!amount_sat_is_zero(action->out_sat))
continue;
if (!amount_sat_is_zero(action->lease_sat))
continue;
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
"Each channel action must include non-zero"
" in sats, out sats, or lease sats.");
}
return NULL;
}
static struct command_result *continue_splice(struct command *cmd,
struct splice_cmd *splice_cmd);
static bool json_to_msat_to_sat(const char *buffer, const jsmntok_t *tok,
struct amount_sat *sat)
{
struct amount_msat msat;
if (!json_to_msat(buffer, tok, &msat))
return false;
return amount_msat_to_sat(sat, msat);
}
static struct splice_script_result *output_wallet(struct splice_cmd *splice_cmd)
{
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
struct splice_script_result *action = splice_cmd->actions[i];
if (!action->onchain_wallet)
continue;
if (action->in_ppm || !amount_sat_is_zero(action->in_sat))
return action;
}
return NULL;
}
static struct command_result *addpsbt_get_result(struct command *cmd,
const char *methodname,
const char *buf,
const jsmntok_t *result,
struct splice_index_pkg *pkg)
{
struct splice_cmd *splice_cmd = pkg->splice_cmd;
size_t index = pkg->index;
struct splice_script_result *action = splice_cmd->actions[index];
const jsmntok_t *tok;
struct amount_sat excess_sat;
struct splice_script_result *out_wallet;
tal_free(pkg);
tok = json_get_member(buf, result, "psbt");
tal_free(splice_cmd->psbt);
splice_cmd->psbt = json_to_psbt(splice_cmd, buf, tok);
assert(splice_cmd->psbt);
tok = json_get_member(buf, result, "excess_msat");
if (tok) {
if (!json_to_msat_to_sat(buf, tok, &excess_sat))
return command_fail_badparam(cmd, "addpsbt", buf, tok,
"invalid excess_msat");
if (!amount_sat_is_zero(excess_sat)) {
if (!amount_sat_add(&action->out_sat, action->out_sat,
excess_sat))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Unable to add excess sats");
out_wallet = output_wallet(splice_cmd);
if (out_wallet) {
if (!out_wallet->in_ppm
&& !amount_sat_add(&out_wallet->in_sat,
out_wallet->in_sat,
excess_sat))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Unable to add excess"
" sats to existing"
" wallet output");
}
else {
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Putting change back into same"
" wallet outpoint not yet"
" supported");
}
}
}
tok = json_get_member(buf, result, "emergency_sat");
if (tok) {
if (!amount_sat_is_zero(splice_cmd->emergency_sat))
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
"Internal error: two"
" emergency_sat");
if (!json_to_msat_to_sat(buf, tok, &splice_cmd->emergency_sat))
return command_fail_badparam(cmd, "addpsbt", buf, tok,
"invalid emergency_sat");
}
return continue_splice(splice_cmd->cmd, splice_cmd);
}
static struct command_result *onchain_wallet_fund(struct command *cmd,
struct splice_cmd *splice_cmd,
size_t index)
{
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;
const char *command;
bool addinginputs = !amount_sat_is_zero(action->out_sat);
pkg = tal(cmd->plugin, struct splice_index_pkg);
pkg->splice_cmd = splice_cmd;
pkg->index = index;
command = "addpsbtoutput";
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);
assert(splice_cmd->feerate_per_kw);
json_add_u32(req->js, "min_feerate", splice_cmd->feerate_per_kw);
}
else {
json_add_sats(req->js, "satoshi", action->in_sat);
}
json_add_psbt(req->js, "initialpsbt", splice_cmd->psbt);
json_add_bool(req->js, "add_initiator_serial_ids", true);
if (addinginputs)
json_add_bool(req->js, "mark_our_inputs", true);
state->state = SPLICE_CMD_DONE;
return send_outreq(req);
}
static struct command_result *feerate_get_result(struct command *cmd,
const char *method,
const char *buf,
const jsmntok_t *result,
struct splice_cmd *splice_cmd)
{
const jsmntok_t *tok = json_get_member(buf, result, "perkw");
tok = json_get_member(buf, tok, "opening");
if (!json_to_u32(buf, tok, &splice_cmd->feerate_per_kw))
return command_fail_badparam(cmd, "opening", buf,
tok, "invalid u32");
if (!splice_cmd->feerate_per_kw)
return command_fail(splice_cmd->cmd,
JSONRPC2_INVALID_PARAMS,
"Failed to load a default feerate");
plugin_log(cmd->plugin, LOG_DBG,
"got feerate %"PRIu32" perkw", splice_cmd->feerate_per_kw);
return continue_splice(splice_cmd->cmd, splice_cmd);
}
static struct command_result *load_feerate(struct command *cmd,
struct splice_cmd *splice_cmd)
{
struct out_req *req;
req = jsonrpc_request_start(cmd, "feerates",
feerate_get_result, splice_error,
splice_cmd);
json_add_string(req->js, "style", "perkw");
return send_outreq(req);
}
static size_t calc_weight(struct splice_cmd *splice_cmd,
bool simulate_wallet_outputs)
{
struct splice_script_result *action;
struct wally_psbt *psbt = splice_cmd->psbt;
size_t weight = 0;
size_t extra_inputs = 0;
size_t extra_outputs = 0;
/* 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_outputs; i++)
weight += psbt_output_get_weight(psbt, i);
/* Count the splice input & 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++;
}
} else 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++;
extra_outputs++;
}
}
/* DTODO make a test to confirm weight calculation is correct */
/* 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
*/
weight += bitcoin_tx_core_weight(psbt->num_inputs + extra_inputs,
psbt->num_outputs + extra_outputs);
return weight;
}
static struct command_result *splice_init_get_result(struct command *cmd,
const char *methodname,
const char *buf,
const jsmntok_t *result,
struct splice_cmd *splice_cmd)
{
const jsmntok_t *tok = json_get_member(buf, result, "psbt");
tal_free(splice_cmd->psbt);
splice_cmd->psbt = json_to_psbt(splice_cmd, buf, tok);
return continue_splice(splice_cmd->cmd, splice_cmd);
}
static struct command_result *splice_init(struct command *cmd,
struct splice_cmd *splice_cmd,
size_t index)
{
struct splice_script_result *action = splice_cmd->actions[index];
struct splice_cmd_action_state *state = splice_cmd->states[index];
struct out_req *req;
req = jsonrpc_request_start(cmd, "splice_init",
splice_init_get_result, splice_error,
splice_cmd);
json_add_channel_id(req->js, "channel_id", action->channel_id);
if (!amount_sat_is_zero(action->in_sat)) {
json_add_u64(req->js, "relative_amount",
action->in_sat.satoshis); /* Raw: signed RPC */
} else if (!amount_sat_is_zero(action->out_sat)) {
json_add_string(req->js, "relative_amount",
tal_fmt(req->js, "-%"PRIu64,
action->out_sat.satoshis)); /* Raw: signed RPC */
} else {
json_add_sats(req->js, "relative_amount", amount_sat(0));
}
json_add_psbt(req->js, "initialpsbt", splice_cmd->psbt);
json_add_u32(req->js, "feerate_per_kw", splice_cmd->feerate_per_kw);
json_add_bool(req->js, "skip_stfu", true);
json_add_bool(req->js, "force_feerate", splice_cmd->force_feerate);
state->state = SPLICE_CMD_INIT;
return send_outreq(req);
}
static struct command_result *splice_update_get_result(struct command *cmd,
const char *methodname,
const char *buf,
const jsmntok_t *result,
struct splice_index_pkg *pkg)
{
size_t index = pkg->index;
struct splice_cmd *splice_cmd = pkg->splice_cmd;
struct splice_cmd_action_state *state = splice_cmd->states[index];
const jsmntok_t *tok;
struct wally_psbt *psbt;
enum splice_cmd_state old_state = state->state;
bool got_sigs;
tal_free(pkg);
/* DTODO: juggle serial ids correctly for cross-channel splice */
tok = json_get_member(buf, result, "psbt");
psbt = json_to_psbt(splice_cmd, buf, tok);
if (psbt_contribs_changed(splice_cmd->psbt, psbt))
for (size_t i = 0; i < tal_count(splice_cmd->states); i++)
if (splice_cmd->actions[i]->channel_id)
splice_cmd->states[i]->state = SPLICE_CMD_UPDATE_NEEDS_CHANGES;
assert(psbt);
tal_free(splice_cmd->psbt);
splice_cmd->psbt = tal_steal(splice_cmd, psbt);
tok = json_get_member(buf, result, "signatures_secured");
if (!json_to_bool(buf, tok, &got_sigs))
return command_fail_badparam(cmd, "signatures_secured", buf,
tok, "invalid bool");
if (old_state != SPLICE_CMD_UPDATE)
state->state = SPLICE_CMD_UPDATE;
else
state->state = got_sigs ? SPLICE_CMD_RECVED_SIGS : SPLICE_CMD_UPDATE_DONE;
return continue_splice(splice_cmd->cmd, splice_cmd);
}
static struct command_result *splice_update(struct command *cmd,
struct splice_cmd *splice_cmd,
size_t index)
{
struct splice_script_result *action = splice_cmd->actions[index];
struct out_req *req;
struct splice_index_pkg *pkg = tal(cmd->plugin, struct splice_index_pkg);
pkg->splice_cmd = splice_cmd;
pkg->index = index;
plugin_log(cmd->plugin, LOG_DBG,
"splice_update(channel_id:%s)",
fmt_channel_id(tmpctx, action->channel_id));
req = jsonrpc_request_start(cmd, "splice_update",
splice_update_get_result, splice_error_pkg,
pkg);
json_add_channel_id(req->js, "channel_id", action->channel_id);
json_add_psbt(req->js, "psbt", splice_cmd->psbt);
return send_outreq(req);
}
static struct command_result *signpsbt_get_result(struct command *cmd,
const char *methodname,
const char *buf,
const jsmntok_t *result,
struct splice_cmd *splice_cmd)
{
const jsmntok_t *tok = json_get_member(buf, result, "signed_psbt");
struct channel_id *channel_ids;
tal_free(splice_cmd->psbt);
splice_cmd->psbt = json_to_psbt(splice_cmd, buf, tok);
splice_cmd->wallet_inputs_to_signed = 0;
/* After signing we add channel_ids to the PSBT for splice_signed */
channel_ids = tal_arr(NULL, struct channel_id, 0);
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++)
if (splice_cmd->actions[i]->channel_id)
tal_arr_expand(&channel_ids,
*splice_cmd->actions[i]->channel_id);
psbt_set_channel_ids(splice_cmd->psbt, channel_ids);
tal_free(channel_ids);
return continue_splice(splice_cmd->cmd, splice_cmd);
}
static struct command_result *signpsbt(struct command *cmd,
struct splice_cmd *splice_cmd)
{
struct out_req *req;
size_t num_to_be_signed;
req = jsonrpc_request_start(cmd, "signpsbt",
signpsbt_get_result, splice_error,
splice_cmd);
/* Use input markers to identify which inputs
* are ours, only sign those */
json_array_start(req->js, "signonly");
num_to_be_signed = 0;
for (size_t i = 0; i < splice_cmd->psbt->num_inputs; i++) {
if (psbt_input_is_ours(&splice_cmd->psbt->inputs[i])) {
json_add_num(req->js, NULL, i);
num_to_be_signed++;
}
}
json_array_end(req->js);
json_add_psbt(req->js, "psbt", splice_cmd->psbt);
/* If we have no inputs to be signed, skip ahead */
if (!num_to_be_signed) {
splice_cmd->wallet_inputs_to_signed = 0;
return continue_splice(splice_cmd->cmd, splice_cmd);
}
return send_outreq(req);
}
static struct splice_script_result *requires_our_sigs(struct splice_cmd *splice_cmd,
size_t *index,
bool *multiple_require_sigs)
{
struct splice_script_result *action = NULL;
*index = UINT32_MAX;
*multiple_require_sigs = false;
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
if (splice_cmd->states[i]->state == SPLICE_CMD_UPDATE_DONE) {
/* There can only be one node that requires our sigs */
if (action) {
*multiple_require_sigs = true;
return NULL;
}
action = splice_cmd->actions[i];
*index = i;
}
}
return action;
}
static struct command_result *splice_signed_get_result(struct command *cmd,
const char *methodname,
const char *buf,
const jsmntok_t *result,
struct splice_index_pkg *pkg)
{
size_t index = pkg->index;
struct splice_cmd *splice_cmd = pkg->splice_cmd;
const jsmntok_t *tok;
tal_free(pkg);
tok = json_get_member(buf, result, "psbt");
tal_free(splice_cmd->psbt);
splice_cmd->psbt = json_to_psbt(splice_cmd, buf, tok);
tok = json_get_member(buf, result, "txid");
if (!json_to_txid(buf, tok, &splice_cmd->final_txid))
return command_fail_badparam(cmd, "txid", buf,
tok, "invalid txid");
splice_cmd->states[index]->state = SPLICE_CMD_DONE;
return continue_splice(splice_cmd->cmd, splice_cmd);
}
static struct command_result *splice_signed(struct command *cmd,
struct splice_cmd *splice_cmd,
size_t index)
{
struct splice_script_result *action = splice_cmd->actions[index];
struct out_req *req;
struct splice_index_pkg *pkg;
pkg = tal(cmd->plugin, struct splice_index_pkg);
pkg->splice_cmd = splice_cmd;
pkg->index = index;
req = jsonrpc_request_start(cmd, "splice_signed",
splice_signed_get_result, splice_error_pkg,
pkg);
json_add_channel_id(req->js, "channel_id", action->channel_id);
json_add_psbt(req->js, "psbt", splice_cmd->psbt);
return send_outreq(req);
}
static struct command_result *check_emergency_sat(struct command *cmd,
struct splice_cmd *splice_cmd)
{
struct amount_sat to_wallet = AMOUNT_SAT(0);
if (amount_sat_is_zero(splice_cmd->emergency_sat))
return NULL;
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
struct splice_script_result *action = splice_cmd->actions[i];
if (action->onchain_wallet)
if (!amount_sat_add(&to_wallet, to_wallet,
action->in_sat))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Unable to amount_sat_add"
" wallet amounts for"
" emergency_sat calc");
}
if (!amount_sat_greater_eq(to_wallet, splice_cmd->emergency_sat))
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
tal_fmt(tmpctx,
"Amount going to onchain wallet %s is"
" not enough to meet the emergency"
" minimum of %s",
fmt_amount_sat(tmpctx, to_wallet),
fmt_amount_sat(tmpctx, splice_cmd->emergency_sat)));
return NULL;
}
static const char *cmd_state_string(enum splice_cmd_state state)
{
switch (state) {
case SPLICE_CMD_NONE:
return " ";
case SPLICE_CMD_INIT:
return " INIT ";
case SPLICE_CMD_UPDATE:
return " UPDATE ";
case SPLICE_CMD_UPDATE_NEEDS_CHANGES:
return "UPDATE_NEEDS_CHANGES";
case SPLICE_CMD_UPDATE_DONE:
return " UPDATE_DONE ";
case SPLICE_CMD_RECVED_SIGS:
return " RECVED_SIGS ";
case SPLICE_CMD_DONE:
return " DONE ";
}
return NULL;
}
static void add_to_debug_log(struct splice_cmd *scmd, const char *phase)
{
char **log = &scmd->debug_log;
if (!*log)
return;
tal_append_fmt(log, "#%d: (%s)\n", ++scmd->debug_counter, phase);
for (size_t i = 0; i < tal_count(scmd->actions); i++) {
struct splice_script_result *action = scmd->actions[i];
struct splice_cmd_action_state *state = scmd->states[i];
tal_append_fmt(log, "[%s] %s\n",
cmd_state_string(state->state),
splice_to_string(tmpctx, action));
}
}
static struct command_result *handle_wetrun(struct command *cmd,
struct splice_cmd *splice_cmd)
{
struct out_req *req;
struct abort_pkg *abort_pkg;
size_t added;
abort_pkg = tal(cmd->plugin, struct abort_pkg);
abort_pkg->splice_cmd = tal_steal(abort_pkg, splice_cmd);
abort_pkg->str = NULL;
req = jsonrpc_request_start(cmd, "abort_channels",
abort_get_result, forward_error, abort_pkg);
added = 0;
json_array_start(req->js, "channel_ids");
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
if (splice_cmd->actions[i]->channel_id) {
added++;
json_add_channel_id(req->js, NULL,
splice_cmd->actions[i]->channel_id);
}
}
json_array_end(req->js);
if (!added)
return unreserve_get_result(cmd, NULL, NULL, NULL, abort_pkg);
return send_outreq(req);
}
static struct command_result *continue_splice(struct command *cmd,
struct splice_cmd *splice_cmd)
{
struct splice_script_result *action;
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;
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);
}
}
if (!splice_cmd->fee_calculated) {
splice_cmd->fee_calculated = true;
/* We calculate the weight simulator wallet outputs */
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 personal vbytes are %.02f",
fmt_amount_sat(tmpctx, onchain_fee),
splice_cmd->feerate_per_kw,
4 * splice_cmd->feerate_per_kw / 1000.0f,
weight / 4.0f);
result = calc_in_ppm_and_fee(cmd, splice_cmd, onchain_fee);
if (result)
return result;
}
/* Only after fee calcualtion can we add wallet actions taking 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]->in_sat)) {
state->state = SPLICE_CMD_DONE;
return onchain_wallet_fund(cmd, splice_cmd, i);
}
}
result = check_emergency_sat(cmd, splice_cmd);
if (result)
return result;
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 (!action->channel_id)
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
"Internal error; should not get"
" here with non-channels with state"
" NONE");
return splice_init(cmd, splice_cmd, i);
}
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_INIT
|| state->state == SPLICE_CMD_UPDATE_NEEDS_CHANGES)
return splice_update(cmd, splice_cmd, i);
}
/* It is possible to receive a signature when we do splice_update with
* no changes. Therefore wetrun must abort here to prevent any of our
* peers locking up funds */
if (splice_cmd->wetrun)
return handle_wetrun(cmd, splice_cmd);
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_UPDATE)
return splice_update(cmd, splice_cmd, i);
}
/* The signpsbt operation also adds channel_ids to psbt */
if (splice_cmd->wallet_inputs_to_signed)
return signpsbt(cmd, splice_cmd);
if (requires_our_sigs(splice_cmd, &index, &multiple_require_sigs))
return splice_signed(cmd, splice_cmd, index);
if (multiple_require_sigs)
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
"Requested splice is impossible because multiple"
" peers demand they do not sign first. Someone"
" must sign first.");
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
action = splice_cmd->actions[i];
state = splice_cmd->states[i];
if (i != index && state->state == SPLICE_CMD_RECVED_SIGS)
return splice_signed(cmd, splice_cmd, i);
}
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++)
assert(splice_cmd->states[i]->state == SPLICE_CMD_DONE);
add_to_debug_log(splice_cmd, "continue_splice-finished");
struct json_stream *response = jsonrpc_stream_success(cmd);
json_add_psbt(response, "psbt", splice_cmd->psbt);
json_add_txid(response, "txid", &splice_cmd->final_txid);
if (splice_cmd->debug_log) {
json_array_start(response, "log");
debug_log_to_json(response, splice_cmd->debug_log);
json_array_end(response);
}
return command_finished(cmd, response);
}
static struct command_result *execute_splice(struct command *cmd,
struct splice_cmd *splice_cmd)
{
struct splice_script_result *action;
struct splice_cmd_action_state *state;
struct wally_psbt_output *output;
u64 serial_id;
int pays_fee;
u8 *scriptpubkey;
/* Basic validation */
pays_fee = 0;
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
int dest_count = 0;
action = splice_cmd->actions[i];
state = splice_cmd->states[i];
if (splice_cmd->actions[i]->out_ppm)
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
"Should be no out_ppm on final");
if (splice_cmd->actions[i]->pays_fee) {
if (pays_fee)
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Only one item may pay fee");
pays_fee++;
}
if (splice_cmd->actions[i]->channel_id)
dest_count++;
if (splice_cmd->actions[i]->bitcoin_address)
dest_count++;
if (splice_cmd->actions[i]->onchain_wallet)
dest_count++;
if (dest_count < 1)
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
"Must specify 1 destination per");
if (dest_count > 1)
return do_fail(cmd, splice_cmd, JSONRPC2_INVALID_PARAMS,
"Too many destinations per");
/* If user specifies both sats in and out, we just use the
* larger of the two and subtract the smaller. */
if (amount_sat_greater(action->in_sat, action->out_sat)) {
if (!amount_sat_sub(&action->in_sat, action->in_sat,
action->out_sat))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Unable to sub out_sat from"
" in_sat");
action->out_sat = amount_sat(0);
} else {
if (!amount_sat_sub(&action->out_sat, action->out_sat,
action->in_sat))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Unable to sub in_sat from"
" out_sat");
action->in_sat = amount_sat(0);
}
}
add_to_debug_log(splice_cmd, "execute_splice");
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++) {
action = splice_cmd->actions[i];
state = splice_cmd->states[i];
char *bitcoin_address;
/* Load (only one) feerate if user provided one */
if (action->feerate_per_kw) {
if (splice_cmd->feerate_per_kw)
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Only one item may set"
" feerate");
splice_cmd->feerate_per_kw = action->feerate_per_kw;
}
/* Fund out to bitcoin address */
if (action->bitcoin_address) {
if (!amount_sat_is_zero(action->in_sat))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Cannot fund from bitcoin"
" address");
if (!decode_scriptpubkey_from_addr(cmd, chainparams,
action->bitcoin_address,
&scriptpubkey))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Bitcoin address"
" unrecognized");
/* Reencode scriptpubkey to addr for verification */
bitcoin_address = encode_scriptpubkey_to_addr(tmpctx,
chainparams,
scriptpubkey);
if (!bitcoin_address)
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Bitcoin scriptpubkey failed"
" reencoding for address");
if (!strcmp(bitcoin_address, action->bitcoin_address))
return do_fail(cmd, splice_cmd,
JSONRPC2_INVALID_PARAMS,
"Bitcoin scriptpubkey failed"
" validation for address");
output = psbt_append_output(splice_cmd->psbt,
scriptpubkey,
action->in_sat);
/* DTODO: support dynamic address payouts (percent) */
serial_id = psbt_new_output_serial(splice_cmd->psbt,
TX_INITIATOR);
psbt_output_set_serial_id(splice_cmd->psbt, output,
serial_id);
state->state = SPLICE_CMD_DONE;
add_to_debug_log(splice_cmd,
"execute_splice-load_btcaddress");
}
}
return continue_splice(cmd, splice_cmd);
}
static struct command_result *adjust_pending_out_ppm(struct splice_script_result **actions,
struct channel_id channel_id,
struct amount_sat available_funds,
struct splice_cmd *splice_cmd)
{
for (size_t i = 0; i < tal_count(actions); i++) {
if (!actions[i]->channel_id)
continue;
if (!channel_id_eq(actions[i]->channel_id, &channel_id))
continue;
/* Skip channels not using out_ppm */
if (!actions[i]->out_ppm)
continue;
/* For now max (asterisks) means 100% but that may change in the
* future */
if (actions[i]->out_ppm == UINT32_MAX)
actions[i]->out_ppm = 1000000;
/* ppm percentage calculation:
* action->out_sat = available_funds * out_ppm / 1000000 */
if (!amount_sat_mul(&actions[i]->out_sat, available_funds,
actions[i]->out_ppm))
return command_fail(splice_cmd->cmd, JSONRPC2_INVALID_PARAMS,
"Unable to mul sats(%s) &"
" out_ppm(%"PRIu32") for channel id"
" %s",
fmt_amount_sat(tmpctx, available_funds),
actions[i]->out_ppm,
fmt_channel_id(tmpctx, &channel_id));
actions[i]->out_sat = amount_sat_div(actions[i]->out_sat,
1000000);
actions[i]->out_ppm = 0;
}
return NULL;
}
static struct command_result *stfu_channels_get_result(struct command *cmd,
const char *methodname,
const char *buf,
const jsmntok_t *toks,
struct splice_cmd *splice_cmd)
{
const jsmntok_t *jchannels, *jchannel;
size_t i;
const char *err;
struct command_result *result;
jchannels = json_get_member(buf, toks, "channels");
json_for_each_arr(i, jchannel, jchannels) {
struct channel_id channel_id;
struct amount_sat sat;
memset(&channel_id, 0, sizeof(channel_id));
memset(&sat, 0, sizeof(sat));
err = json_scan(tmpctx, buf, jchannel,
"{channel_id?:%,available_msat?:%}",
JSON_SCAN(json_to_channel_id, &channel_id),
JSON_SCAN(json_to_msat_to_sat, &sat));
if (err)
errx(1, "Bad stfu_channels.channels %zu: %s",
i, err);
result = adjust_pending_out_ppm(splice_cmd->actions,
channel_id, sat, splice_cmd);
if (result)
return result;
}
return execute_splice(splice_cmd->cmd, splice_cmd);
}
static struct command_result *splice_dryrun(struct command *cmd,
struct splice_cmd *splice_cmd)
{
char **lines;
unsigned int i;
struct json_stream *response;
const char *str;
response = jsonrpc_stream_success(cmd);
json_array_start(response, "dryrun");
str = splicearr_to_string(response, splice_cmd->actions);
lines = tal_strsplit(response, take(str), "\n", STR_NO_EMPTY);
for (i = 0; lines[i] != NULL; i++)
json_add_string(response, NULL, lines[i]);
json_array_end(response);
return command_finished(cmd, response);
}
static struct command_result *handle_splice_cmd(struct command *cmd,
struct splice_cmd *splice_cmd)
{
struct out_req *req;
if (splice_cmd->dryrun)
return splice_dryrun(cmd, splice_cmd);
req = jsonrpc_request_start(cmd, "stfu_channels",
stfu_channels_get_result,
splice_error, splice_cmd);
json_array_start(req->js, "channel_ids");
/* We begin by stfu'ing and getting available balance on all MAX reqs */
for (size_t i = 0; i < tal_count(splice_cmd->actions); i++)
if (splice_cmd->actions[i]->channel_id)
json_add_channel_id(req->js, NULL,
splice_cmd->actions[i]->channel_id);
json_array_end(req->js);
return send_outreq(req);
}
static struct command_result *
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
* since adding more funds adds more input bytes. We don't
* support it for now. */
if (action->pays_fee && action->onchain_wallet
&& action->out_ppm)
return command_fail(splice_cmd->cmd,
JSONRPC2_INVALID_PARAMS,
"Don't support dynamic fee being"
" added to onchain wallet");
if (action->onchain_wallet && action->out_ppm)
return command_fail(splice_cmd->cmd,
JSONRPC2_INVALID_PARAMS,
"Don't support dynamic wallet"
" funding amounts for now");
if (action->pays_fee && action->onchain_wallet
&& !amount_sat_is_zero(action->out_sat))
return command_fail(splice_cmd->cmd,
JSONRPC2_INVALID_PARAMS,
"Don't support wallet funding"
" being used for fee");
if (action->pays_fee) {
if (paying_fee_count)
return command_fail(splice_cmd->cmd,
JSONRPC2_INVALID_PARAMS,
"Only one item may pay the"
" fee");
paying_fee_count++;
}
if (action->bitcoin_address && action->in_ppm)
return command_fail(splice_cmd->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,
"Paying out to bitcoin addresses"
" not supported for now.");
}
return NULL;
}
static struct command_result *listpeerchannels_get_result(struct command *cmd,
const char *methodname,
const char *buf,
const jsmntok_t *toks,
struct splice_cmd *splice_cmd)
{
struct splice_script_error *error;
struct splice_script_chan **channels;
struct command_result *result;
const jsmntok_t *jchannels, *jchannel;
char **lines;
struct json_stream *response;
const char *str;
size_t i;
const char *err;
channels = tal_arr(tmpctx, struct splice_script_chan*, 0);
jchannels = json_get_member(buf, toks, "channels");
json_for_each_arr(i, jchannel, jchannels) {
tal_arr_expand(&channels, tal(channels,
struct splice_script_chan));
err = json_scan(tmpctx, buf, jchannel,
"{peer_id?:%,channel_id?:%}",
JSON_SCAN(json_to_node_id,
&channels[i]->node_id),
JSON_SCAN(json_to_channel_id,
&channels[i]->chan_id));
if (err)
errx(1, "Bad listpeerchannels.channels %zu: %s",
i, err);
}
if (splice_cmd->script) {
error = parse_splice_script(splice_cmd, splice_cmd->script,
channels, &splice_cmd->actions);
if (error) {
response = jsonrpc_stream_fail(cmd,
JSONRPC2_INVALID_PARAMS,
"Splice script compile"
" failed");
json_array_start(response, "compiler_error");
str = fmt_splice_script_compiler_error(response,
splice_cmd->script,
error);
lines = tal_strsplit(response, take(str), "\n",
STR_NO_EMPTY);
for (i = 0; lines[i] != NULL; i++)
json_add_string(response, NULL, lines[i]);
json_array_end(response);
return command_finished(cmd, response);
}
splice_cmd->states = tal_arr(splice_cmd,
struct splice_cmd_action_state*,
tal_count(splice_cmd->actions));
for (i = 0; i < tal_count(splice_cmd->states); i++) {
splice_cmd->states[i] = tal(splice_cmd->states,
struct splice_cmd_action_state);
splice_cmd->states[i]->state = SPLICE_CMD_NONE;
}
}
assert(splice_cmd->actions);
result = validate_splice_cmd(splice_cmd);
if (result)
return result;
return handle_splice_cmd(splice_cmd->cmd, splice_cmd);
}
static struct command_result *
json_splice(struct command *cmd, const char *buf, const jsmntok_t *params)
{
struct out_req *req;
const char *script;
const jsmntok_t *json;
struct wally_psbt *psbt;
bool *dryrun, *force_feerate, *debug_log, *wetrun;
struct str_or_arr *str_or_arr;
if (!param(cmd, buf, params,
p_opt("script_or_json", param_string_or_array, &str_or_arr),
p_opt_def("dryrun", param_bool, &dryrun, false),
p_opt_def("force_feerate", param_bool, &force_feerate,
false),
p_opt_def("debug_log", param_bool, &debug_log, false),
p_opt_dev("dev-wetrun", param_bool, &wetrun, false),
NULL))
return command_param_failed();
if (!str_or_arr)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Must pass 'script_or_json'");
script = str_or_arr->str;
json = str_or_arr->arr;
psbt = create_psbt(cmd, 0, 0, 0);
struct splice_cmd *splice_cmd = tal(cmd, struct splice_cmd);
splice_cmd->cmd = cmd;
splice_cmd->script = tal_steal(splice_cmd, script);
splice_cmd->psbt = tal_steal(splice_cmd, psbt);
splice_cmd->dryrun = *dryrun;
splice_cmd->wetrun = *wetrun;
splice_cmd->feerate_per_kw = 0;
splice_cmd->force_feerate = *force_feerate;
splice_cmd->wallet_inputs_to_signed = 0;
splice_cmd->fee_calculated = false;
splice_cmd->initial_funds = AMOUNT_SAT(0);
splice_cmd->emergency_sat = AMOUNT_SAT(0);
splice_cmd->debug_log = *debug_log ? tal_strdup(splice_cmd, "") : NULL;
splice_cmd->debug_counter = 0;
memset(&splice_cmd->final_txid, 0, sizeof(splice_cmd->final_txid));
/* If script validates as json, parse it as json instead */
if (json) {
if (!json_to_splice(splice_cmd, buf, json,
&splice_cmd->actions))
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"splice json failed validation");
splice_cmd->states = tal_arr(splice_cmd,
struct splice_cmd_action_state*,
tal_count(splice_cmd->actions));
for (size_t i = 0; i < tal_count(splice_cmd->states); i++) {
splice_cmd->states[i] = tal(splice_cmd->states,
struct splice_cmd_action_state);
splice_cmd->states[i]->state = SPLICE_CMD_NONE;
}
}
req = jsonrpc_request_start(cmd, "listpeerchannels",
listpeerchannels_get_result,
splice_error, splice_cmd);
return send_outreq(req);
}
const struct plugin_command splice_commands[] = {
{
"dev-splice",
json_splice
},
};
const size_t num_splice_commands = ARRAY_SIZE(splice_commands);