diff --git a/bitcoin/test/run-tx-bitcoin_tx_2of2_input_witness_weight.c b/bitcoin/test/run-tx-bitcoin_tx_2of2_input_witness_weight.c index 582461423359..173ad73f3da5 100644 --- a/bitcoin/test/run-tx-bitcoin_tx_2of2_input_witness_weight.c +++ b/bitcoin/test/run-tx-bitcoin_tx_2of2_input_witness_weight.c @@ -100,6 +100,14 @@ bool psbt_finalize(struct wally_psbt *psbt UNNEEDED) struct amount_sat psbt_input_get_amount(const struct wally_psbt *psbt UNNEEDED, size_t in UNNEEDED) { fprintf(stderr, "psbt_input_get_amount called!\n"); abort(); } +/* Generated stub for psbt_input_get_weight */ +size_t psbt_input_get_weight(const struct wally_psbt *psbt UNNEEDED, + size_t in UNNEEDED) +{ fprintf(stderr, "psbt_input_get_weight called!\n"); abort(); } +/* Generated stub for psbt_output_get_weight */ +size_t psbt_output_get_weight(const struct wally_psbt *psbt UNNEEDED, + size_t out UNNEEDED) +{ fprintf(stderr, "psbt_output_get_weight called!\n"); abort(); } /* Generated stub for psbt_input_set_wit_utxo */ void psbt_input_set_wit_utxo(struct wally_psbt *psbt UNNEEDED, size_t in UNNEEDED, const u8 *scriptPubkey UNNEEDED, struct amount_sat amt UNNEEDED) diff --git a/bitcoin/tx.c b/bitcoin/tx.c index db179fb3371a..6e2672f7c58f 100644 --- a/bitcoin/tx.c +++ b/bitcoin/tx.c @@ -483,6 +483,20 @@ u8 *linearize_wtx(const tal_t *ctx, const struct wally_tx *wtx) return arr; } +size_t wally_psbt_weight(const struct wally_psbt *psbt) +{ + size_t weight = bitcoin_tx_core_weight(psbt->num_inputs, + psbt->num_outputs); + + 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); + + return weight; +} + size_t wally_tx_weight(const struct wally_tx *wtx) { size_t weight; diff --git a/bitcoin/tx.h b/bitcoin/tx.h index 74454a1da252..a08a40ab0388 100644 --- a/bitcoin/tx.h +++ b/bitcoin/tx.h @@ -61,6 +61,7 @@ u8 *linearize_wtx(const tal_t *ctx, const struct wally_tx *wtx); /* Get weight of tx in Sipa; assumes it will have witnesses! */ size_t bitcoin_tx_weight(const struct bitcoin_tx *tx); size_t wally_tx_weight(const struct wally_tx *wtx); +size_t wally_psbt_weight(const struct wally_psbt *psbt); /* Allocate a tx: you just need to fill in inputs and outputs (they're * zeroed with inputs' sequence_number set to FFFFFFFF) */ diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index 13da76ed6144..a01c2c5f3c75 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -1212,6 +1212,19 @@ def openchannel_abort(self, channel_id): } return self.call("openchannel_abort", payload) + def splice_out(self, chan_id, amount, feerate_per_kw=None, force_feerate=None, initialpsbt=None, locktime=None, sign_first=None): + """ Initiate a splice """ + payload = { + "channel_id": chan_id, + "amount": amount, + "feerate_per_kw": feerate_per_kw, + "force_feerate": force_feerate, + "initialpsbt": initialpsbt, + "locktime": locktime, + "sign_first": sign_first, + } + return self.call("splice_out", payload) + def splice_init(self, chan_id, amount, initialpsbt=None, feerate_per_kw=None): """ Initiate a splice """ payload = { diff --git a/doc/schemas/splice_out.request.json b/doc/schemas/splice_out.request.json new file mode 100644 index 000000000000..9110a8c517f3 --- /dev/null +++ b/doc/schemas/splice_out.request.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "channel_id", + "amount" + ], + "added": "v23.11", + "properties": { + "channel_id": { + "type": "string", + "description": "the channel id of the channel to take funds from" + }, + "amount": { + "type": "msat", + "description": "a positive amount of satoshis to be taken from the channel" + }, + "initialpsbt": { + "type": "string", + "description": "the (optional) base 64 encoded PSBT to begin with. If not specified, one will be generated automatically" + }, + "feerate_per_kw": { + "type": "u32", + "description": "the miner fee we will pay from our channel funds. It is calculated by `feerate_per_kw` * our_bytes_in_splice_tx / 1000" + }, + "force_feerate": { + "type": "boolean", + "description": "by default splices will fail if the fee provided looks too high. This is to protect against accidentally setting your fee higher than intended. Set `force_feerate` to true to skip this saftey check" + }, + "locktime": { + "type": "u32", + "description": "the locktime to use if initialpsbt is not specified." + }, + "sign_first": { + "type": "bool", + "description": "offer to our peer to sign the splice first." + } + } +} diff --git a/doc/schemas/splice_out.schema.json b/doc/schemas/splice_out.schema.json new file mode 100644 index 000000000000..8325ca53dd83 --- /dev/null +++ b/doc/schemas/splice_out.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "tx", + "txid" + ], + "added": "v23.11", + "properties": { + "tx": { + "type": "hex", + "description": "The hex representation of the final transaction that is published" + }, + "txid": { + "type": "txid", + "description": "The txid is of the final transaction" + } + } +} diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index 9f544524f34d..5d88511fbb33 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -1,4 +1,5 @@ #include "config.h" +#include #include #include #include @@ -27,6 +28,7 @@ #include #include #include +#include struct splice_command { /* Inside struct lightningd splice_commands. */ @@ -35,8 +37,38 @@ struct splice_command { struct command *cmd; /* Channel being spliced. */ struct channel *channel; + /* Should the command be completed automatically */ + bool auto_complete; + /* Should we sign first when completing automatically */ + bool sign_first; + /* When in auto_complete mode, sign_splice_cb will be called when + * signing is needed */ + struct wally_psbt *(*sign_splice_cb)(struct splice_command *cc, + struct wally_psbt *psbt); }; +static void destroy_splice_command(struct splice_command *cc) +{ + list_del(&cc->list); +} + +static struct splice_command *splice_command_new(struct command *cmd, + struct channel *channel) +{ + struct splice_command *cc = tal(cmd, struct splice_command); + + list_add_tail(&cmd->ld->splice_commands, &cc->list); + tal_add_destructor(cc, destroy_splice_command); + + cc->cmd = cmd; + cc->channel = channel; + cc->auto_complete = false; + cc->sign_first = false; + cc->sign_splice_cb = NULL; + + return cc; +} + void channel_update_feerates(struct lightningd *ld, const struct channel *channel) { u8 *msg; @@ -293,10 +325,16 @@ static void handle_splice_confirmed_init(struct lightningd *ld, return; } - struct json_stream *response = json_stream_success(cc->cmd); - json_add_string(response, "psbt", psbt_to_b64(tmpctx, psbt)); + if (cc->auto_complete) { + subd_send_msg(channel->owner, + take(towire_channeld_splice_update(NULL, psbt))); + } + else { + struct json_stream *response = json_stream_success(cc->cmd); + json_add_string(response, "psbt", psbt_to_b64(tmpctx, psbt)); - was_pending(command_success(cc->cmd, response)); + was_pending(command_success(cc->cmd, response)); + } } /* Channeld sends us this in response to a user's `splice_update` request */ @@ -307,6 +345,7 @@ static void handle_splice_confirmed_update(struct lightningd *ld, struct splice_command *cc; struct wally_psbt *psbt; bool commitments_secured; + u8 *outmsg; if (!fromwire_channeld_splice_confirmed_update(tmpctx, msg, @@ -326,11 +365,23 @@ static void handle_splice_confirmed_update(struct lightningd *ld, return; } - struct json_stream *response = json_stream_success(cc->cmd); - json_add_string(response, "psbt", psbt_to_b64(tmpctx, psbt)); - json_add_bool(response, "commitments_secured", commitments_secured); + if (cc->auto_complete) { + if (cc->sign_splice_cb) + psbt = cc->sign_splice_cb(cc, psbt); + + assert(commitments_secured); - was_pending(command_success(cc->cmd, response)); + outmsg = towire_channeld_splice_signed(NULL, psbt, + cc->sign_first); + subd_send_msg(channel->owner, take(outmsg)); + } + else { + struct json_stream *response = json_stream_success(cc->cmd); + json_add_string(response, "psbt", psbt_to_b64(tmpctx, psbt)); + json_add_bool(response, "commitments_secured", commitments_secured); + + was_pending(command_success(cc->cmd, response)); + } } /* Channeld uses this to request the funding transaction for help building the @@ -1901,18 +1952,12 @@ static struct command_result *param_channel_for_splice(struct command *cmd, return NULL; } -static void destroy_splice_command(struct splice_command *cc) -{ - list_del(&cc->list); -} - static struct command_result *json_splice_init(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, const jsmntok_t *params) { struct channel *channel; - struct splice_command *cc; struct wally_psbt *initialpsbt; s64 *relative_amount; u32 *feerate_per_kw; @@ -1949,13 +1994,7 @@ static struct command_result *json_splice_init(struct command *cmd, log_debug(cmd->ld->log, "splice_init input PSBT version %d", initialpsbt->version); - cc = tal(cmd, struct splice_command); - - list_add_tail(&cmd->ld->splice_commands, &cc->list); - tal_add_destructor(cc, destroy_splice_command); - - cc->cmd = cmd; - cc->channel = channel; + splice_command_new(cmd, channel); msg = towire_channeld_splice_init(NULL, initialpsbt, *relative_amount, *feerate_per_kw, *force_feerate); @@ -1970,7 +2009,6 @@ static struct command_result *json_splice_update(struct command *cmd, const jsmntok_t *params) { struct channel *channel; - struct splice_command *cc; struct wally_psbt *psbt; if (!param(cmd, buffer, params, @@ -1992,13 +2030,7 @@ static struct command_result *json_splice_update(struct command *cmd, log_debug(cmd->ld->log, "splice_update input PSBT version %d", psbt->version); - cc = tal(cmd, struct splice_command); - - list_add_tail(&cmd->ld->splice_commands, &cc->list); - tal_add_destructor(cc, destroy_splice_command); - - cc->cmd = cmd; - cc->channel = channel; + splice_command_new(cmd, channel); subd_send_msg(channel->owner, take(towire_channeld_splice_update(NULL, psbt))); @@ -2012,7 +2044,6 @@ static struct command_result *json_splice_signed(struct command *cmd, { u8 *msg; struct channel *channel; - struct splice_command *cc; struct wally_psbt *psbt; bool *sign_first; @@ -2036,13 +2067,7 @@ static struct command_result *json_splice_signed(struct command *cmd, log_debug(cmd->ld->log, "splice_signed input PSBT version %d", psbt->version); - cc = tal(cmd, struct splice_command); - - list_add_tail(&cmd->ld->splice_commands, &cc->list); - tal_add_destructor(cc, destroy_splice_command); - - cc->cmd = cmd; - cc->channel = channel; + splice_command_new(cmd, channel); msg = towire_channeld_splice_signed(tmpctx, psbt, *sign_first); subd_send_msg(channel->owner, take(msg)); @@ -2079,6 +2104,122 @@ static const struct json_command splice_signed_command = { }; AUTODATA(json_command, &splice_signed_command); +static struct command_result *json_splice_out(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + u8 *msg; + struct splice_command *cc; + struct channel *channel; + struct amount_sat *amount; + s64 relative_amount; + u32 *feerate_per_kw; + bool *force_feerate; + struct wally_psbt *psbt; + u32 *locktime; + struct pubkey pubkey; + s64 keyidx; + u8 *b32script; + size_t weight; + bool *sign_first; + + if (!param(cmd, buffer, params, + p_req("channel_id", param_channel_for_splice, &channel), + p_req("amount", param_sat_or_all, &amount), + p_opt("feerate_per_kw", param_feerate, &feerate_per_kw), + p_opt_def("force_feerate", param_bool, &force_feerate, false), + p_opt("initialpsbt", param_psbt, &psbt), + p_opt("locktime", param_number, &locktime), + p_opt_def("sign_first", param_bool, &sign_first, false), + NULL)) + return command_param_failed(); + + if (!feerate_per_kw) { + feerate_per_kw = tal(cmd, u32); + *feerate_per_kw = opening_feerate(cmd->ld->topology); + } + + if (!psbt) { + if (!locktime) { + locktime = tal(cmd, u32); + *locktime = default_locktime(cmd->ld->topology); + } + psbt = create_psbt(cmd, 0, 0, *locktime); + } + else if(locktime) { + return command_fail(cmd, FUNDING_PSBT_INVALID, + "Can't set locktime of an existing" + " {initialpsbt}"); + } + + if (!validate_psbt(psbt)) + return command_fail(cmd, + FUNDING_PSBT_INVALID, + "PSBT failed to validate."); + + if (amount_sat_less(*amount, chainparams->dust_limit)) + return command_fail(cmd, FUND_OUTPUT_IS_DUST, + "amount is below dust limit (%s)", + type_to_string(tmpctx, + struct amount_sat, + &chainparams->dust_limit)); + + if (splice_command_for_chan(cmd->ld, channel)) + return command_fail(cmd, + SPLICE_BUSY_ERROR, + "Currently waiting on previous splice" + " command to finish."); + + /* Get a change adddress */ + keyidx = wallet_get_newindex(cmd->ld); + if (keyidx < 0) + return command_fail(cmd, LIGHTNINGD, + "Failed to generate change address." + " Keys exhausted."); + + b32script = NULL; + if (chainparams->is_elements) { + bip32_pubkey(cmd->ld, &pubkey, keyidx); + b32script = scriptpubkey_p2wpkh(tmpctx, &pubkey); + } else { + b32script = p2tr_for_keyidx(tmpctx, cmd->ld, keyidx); + } + if (!b32script) { + return command_fail(cmd, LIGHTNINGD, + "Failed to generate change address." + " Keys generation failure"); + } + txfilter_add_scriptpubkey(cmd->ld->owned_txfilter, b32script); + + psbt_append_output(psbt, b32script, *amount); + + weight = wally_psbt_weight(psbt); + weight += bitcoin_tx_input_weight(true, bitcoin_tx_2of2_input_witness_weight()); + weight += bitcoin_tx_output_weight(BITCOIN_SCRIPTPUBKEY_P2WPKH_LEN); + + relative_amount = -amount_tx_fee(*feerate_per_kw, weight).satoshis; /* Raw: splice-out to splice_init */ + relative_amount -= amount->satoshis; /* Raw: splice-out to splice_init */ + + cc = splice_command_new(cmd, channel); + cc->auto_complete = true; + cc->sign_first = *sign_first; + + msg = towire_channeld_splice_init(NULL, psbt, relative_amount, + *feerate_per_kw, *force_feerate); + + subd_send_msg(channel->owner, take(msg)); + return command_still_pending(cmd); +} + +static const struct json_command splice_out_command = { + "splice_out", + "channels", + json_splice_out, + "Splice {amount} satoshis out of {channel_id} into the onchain wallet." +}; +AUTODATA(json_command, &splice_out_command); + static struct command_result *json_dev_feerate(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, diff --git a/tests/test_splicing.py b/tests/test_splicing.py index b2a085a00d52..fbcc64d13372 100644 --- a/tests/test_splicing.py +++ b/tests/test_splicing.py @@ -171,6 +171,30 @@ def test_splice_out(node_factory, bitcoind): assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0 +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +def test_splice_out_ez(node_factory, bitcoind): + l1, l2 = node_factory.line_graph(2, fundamount=1000000, wait_for_announce=True, opts={'experimental-splicing': None}) + + l1.rpc.splice_out(l1.get_channel_id(l2), 100000) + + l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') + l1.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') + + bitcoind.generate_block(6, wait_for_mempool=1) + + 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, '3', 'no_3') + l1.rpc.pay(inv['bolt11']) + + # Check that the splice doesn't generate a unilateral close transaction + time.sleep(5) + assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0 + + @pytest.mark.openchannel('v1') @pytest.mark.openchannel('v2') @unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need')