From 524de4a5236be4736306e0809cfda1b8d5864bc4 Mon Sep 17 00:00:00 2001 From: Dusty Daemon Date: Thu, 14 Sep 2023 14:29:20 -0400 Subject: [PATCH] recvpsbt: New onchain command for PSBT output ChangeLog-Added: New `recvpsbt` command for creating a PSBT that can receive funds to the on-chain wallet. --- contrib/pyln-client/pyln/client/lightning.py | 11 +++ doc/Makefile | 1 + doc/index.rst | 1 + doc/lightning-recvpsbt.7.md | 65 +++++++++++++++ doc/schemas/recvpsbt.request.json | 21 +++++ doc/schemas/recvpsbt.schema.json | 25 ++++++ tests/test_wallet.py | 27 ++++++ wallet/reservation.c | 88 ++++++++++++++++++++ 8 files changed, 239 insertions(+) create mode 100644 doc/lightning-recvpsbt.7.md create mode 100644 doc/schemas/recvpsbt.request.json create mode 100644 doc/schemas/recvpsbt.schema.json diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index b535f88e9bab..14fe59ecb35f 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -1523,6 +1523,17 @@ def fundpsbt(self, satoshi, feerate, startweight, minconf=None, reserve=None, lo } return self.call("fundpsbt", payload) + def recvpsbt(self, satoshi, initialpsbt=None, locktime=None): + """ + Create a PSBT with an output of amount satoshi leading to the on-chain wallet + """ + payload = { + "satoshi": satoshi, + "initialpsbt": initialpsbt, + "locktime": locktime, + } + return self.call("recvpsbt", payload) + def utxopsbt(self, satoshi, feerate, startweight, utxos, reserve=None, reservedok=False, locktime=None, min_witness_weight=None, excess_as_change=False): """ Create a PSBT with given inputs, to give an output of satoshi. diff --git a/doc/Makefile b/doc/Makefile index 130d9fdcf5fe..5430c98a3c47 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -50,6 +50,7 @@ MANPAGES := doc/lightning-cli.1 \ doc/lightning-fundchannel_complete.7 \ doc/lightning-fundchannel_cancel.7 \ doc/lightning-funderupdate.7 \ + doc/lightning-recvpsbt.7 \ doc/lightning-fundpsbt.7 \ doc/lightning-getroute.7 \ doc/lightning-hsmtool.8 \ diff --git a/doc/index.rst b/doc/index.rst index d59d7700cf9f..8c912bd6db81 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -99,6 +99,7 @@ Core Lightning Documentation lightning-preapproveinvoice lightning-preapprovekeysend lightning-recoverchannel + lightning-recvpsbt lightning-renepay lightning-renepaystatus lightning-reserveinputs diff --git a/doc/lightning-recvpsbt.7.md b/doc/lightning-recvpsbt.7.md new file mode 100644 index 000000000000..45cd7cddc392 --- /dev/null +++ b/doc/lightning-recvpsbt.7.md @@ -0,0 +1,65 @@ +lightning-recvpsbt -- Command to populate PSBT outputs from the wallet +================================================================ + +SYNOPSIS +-------- + +**recvpsbt** *satoshi* [*initialpsbt*] [*locktime*] + +DESCRIPTION +----------- + +`recvpsbt` is a low-level RPC command which creates or modifies a PSBT +by adding a single output of amount *satoshi*. + +This is used to receive funds into the on-chain wallet interactively +using PSBTs. + +*satoshi* is the satoshi value of the output. It can +be a whole number, a whole number ending in *sat*, a whole number +ending in *000msat*, or a number with 1 to 8 decimal places ending in +*btc*. + +*initialpsbt* is a PSBT to add the output to. If not speciifed, a PSBt +will be created automatically. + +*locktime* is an optional locktime: if not set, it is set to a recent +block height (if no initial psbt is specified). + +EXAMPLE USAGE +------------- + +Here is a command to make a PSBT with a 100,000 sat output that leads +to the on-chain wallet. +```shell +lightning-cli recvpsbt 100000sat +``` + +RETURN VALUE +------------ + +[comment]: # (GENERATE-FROM-SCHEMA-START) +On success, an object is returned, containing: + +- **psbt** (string): Unsigned PSBT which fulfills the parameters given +- **estimated\_added\_weight** (u32): The estimated weight of the added output +- **outnum** (u32): The 0-based number where the output was placed + +[comment]: # (GENERATE-FROM-SCHEMA-END) + +AUTHOR +------ + +@dusty\_daemon + +SEE ALSO +-------- + +lightning-fundpsbt(7), lightning-utxopsbt(7) + +RESOURCES +--------- + +Main web site: + +[comment]: # ( SHA256STAMP:fad04aa277b18392570cc1ba6302506dadc71d1e3844720cb5c1b74812d731f2) diff --git a/doc/schemas/recvpsbt.request.json b/doc/schemas/recvpsbt.request.json new file mode 100644 index 000000000000..5bda836672ad --- /dev/null +++ b/doc/schemas/recvpsbt.request.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "satoshi" + ], + "added": "v23.08.1", + "properties": { + "satoshi": { + "type": "msat" + }, + "locktime": { + "type": "u32" + }, + "initialpsbt": { + "type": "string", + "description": "the (optional) base 64 encoded PSBT to begin with. If not specified, one will be generated automatically" + } + } +} diff --git a/doc/schemas/recvpsbt.schema.json b/doc/schemas/recvpsbt.schema.json new file mode 100644 index 000000000000..6481688d231b --- /dev/null +++ b/doc/schemas/recvpsbt.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "psbt", + "estimated_added_weight", + "outnum" + ], + "added": "v23.08.1", + "properties": { + "psbt": { + "type": "string", + "description": "Unsigned PSBT which fulfills the parameters given" + }, + "estimated_added_weight": { + "type": "u32", + "description": "The estimated weight of the added output" + }, + "outnum": { + "type": "u32", + "description": "The 0-based number where the output was placed" + } + } +} diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 86bcf82891be..f89d8994b460 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -607,6 +607,33 @@ def test_fundpsbt(node_factory, bitcoind, chainparams): l1.rpc.fundpsbt(amount // 2, feerate, 0) +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +def test_recvpsbt(node_factory, bitcoind, chainparams): + amount1 = 1000000 + amount2 = 3333333 + locktime = 111 + l1 = node_factory.get_node() + + result = l1.rpc.recvpsbt(amount1, locktime=locktime) + assert result['outnum'] == 0 + + psbt_info = bitcoind.rpc.decodepsbt(l1.rpc.setpsbtversion(result['psbt'], 0)['psbt']) + + assert len(psbt_info['tx']['vout']) == 1 + assert psbt_info['tx']['vout'][0]['n'] == result['outnum'] + assert psbt_info['tx']['vout'][0]['value'] * 100000000 == amount1 + assert psbt_info['tx']['locktime'] == locktime + + result = l1.rpc.recvpsbt(amount2, result['psbt']) + n = result['outnum'] + + psbt_info = bitcoind.rpc.decodepsbt(l1.rpc.setpsbtversion(result['psbt'], 0)['psbt']) + + assert len(psbt_info['tx']['vout']) == 2 + assert psbt_info['tx']['vout'][n]['value'] * 100000000 == amount2 + assert psbt_info['tx']['vout'][n]['n'] == result['outnum'] + + def test_utxopsbt(node_factory, bitcoind, chainparams): amount = 1000000 l1 = node_factory.get_node() diff --git a/wallet/reservation.c b/wallet/reservation.c index 15e2045b1e5a..b634471f4cd8 100644 --- a/wallet/reservation.c +++ b/wallet/reservation.c @@ -654,6 +654,94 @@ static const struct json_command fundpsbt_command = { }; AUTODATA(json_command, &fundpsbt_command); +static struct command_result *json_recvpsbt(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct json_stream *response; + struct amount_sat *amount; + struct wally_psbt *psbt; + u32 *locktime; + ssize_t outnum; + u32 weight; + struct pubkey pubkey; + s64 keyidx; + u8 *b32script; + + if (!param(cmd, buffer, params, + p_req("satoshi", param_sat, &amount), + p_opt("initialpsbt", param_psbt, &psbt), + p_opt("locktime", param_number, &locktime), + NULL)) + return command_param_failed(); + + 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, + "Receive amount is below dust limit (%s)", + type_to_string(tmpctx, + struct amount_sat, + &chainparams->dust_limit)); + + /* 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."); + + 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); + + outnum = psbt->num_outputs; + psbt_append_output(psbt, b32script, *amount); + /* Add additional weight of output */ + weight = bitcoin_tx_output_weight( + chainparams->is_elements ? BITCOIN_SCRIPTPUBKEY_P2WPKH_LEN : BITCOIN_SCRIPTPUBKEY_P2TR_LEN); + + response = json_stream_success(cmd); + json_add_psbt(response, "psbt", psbt); + json_add_num(response, "estimated_added_weight", weight); + json_add_num(response, "outnum", outnum); + return command_success(cmd, response); +} + +static const struct json_command recvpsbt_command = { + "recvpsbt", + "bitcoin", + json_recvpsbt, + "Create a PSBT (or modify existing {initialpsbt}) with an output receiving {satoshi} amount.", + false +}; +AUTODATA(json_command, &recvpsbt_command); + static struct command_result *param_txout(struct command *cmd, const char *name, const char *buffer,