diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index 23c7583c457e..a88d866acfad 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -1126,7 +1126,7 @@ def unreserveinputs(self, psbt): } return self.call("unreserveinputs", payload) - def fundpsbt(self, satoshi, feerate, startweight, minconf=None, reserve=True): + def fundpsbt(self, satoshi, feerate, startweight, minconf=None, reserve=True, locktime=None): """ Create a PSBT with inputs sufficient to give an output of satoshi. """ @@ -1136,15 +1136,32 @@ def fundpsbt(self, satoshi, feerate, startweight, minconf=None, reserve=True): "startweight": startweight, "minconf": minconf, "reserve": reserve, + "locktime": locktime, } return self.call("fundpsbt", payload) - def signpsbt(self, psbt): + def utxopsbt(self, satoshi, feerate, startweight, utxos, reserve=True, reservedok=False, locktime=None): + """ + Create a PSBT with given inputs, to give an output of satoshi. + """ + payload = { + "satoshi": satoshi, + "feerate": feerate, + "startweight": startweight, + "utxos": utxos, + "reserve": reserve, + "reservedok": reservedok, + "locktime": locktime, + } + return self.call("utxopsbt", payload) + + def signpsbt(self, psbt, signonly=None): """ Add internal wallet's signatures to PSBT """ payload = { "psbt": psbt, + "signonly": signonly, } return self.call("signpsbt", payload) diff --git a/doc/lightning-fundpsbt.7 b/doc/lightning-fundpsbt.7 index 6b6bafc08b9b..349f0c542907 100644 --- a/doc/lightning-fundpsbt.7 +++ b/doc/lightning-fundpsbt.7 @@ -3,7 +3,7 @@ lightning-fundpsbt - Command to populate PSBT inputs from the wallet .SH SYNOPSIS -\fBfundpsbt\fR \fIsatoshi\fR \fIfeerate\fR \fIstartweight\fR [\fIminconf\fR] [\fIreserve\fR] +\fBfundpsbt\fR \fIsatoshi\fR \fIfeerate\fR \fIstartweight\fR [\fIminconf\fR] [\fIreserve\fR] [\fIlocktime\fR] .SH DESCRIPTION @@ -39,6 +39,10 @@ outputs should have\. Default is 1\. \fIreserve\fR is a boolean: if true (the default), then \fIreserveinputs\fR is called (successfully, with \fIexclusive\fR true) on the returned PSBT\. + +\fIlocktime\fR is an optional locktime: if not set, it is set to a recent +block height\. + .SH EXAMPLE USAGE Let's assume the caller is trying to produce a 100,000 satoshi output\. diff --git a/doc/lightning-fundpsbt.7.md b/doc/lightning-fundpsbt.7.md index 85e150f7283b..fc84e9a1ea7a 100644 --- a/doc/lightning-fundpsbt.7.md +++ b/doc/lightning-fundpsbt.7.md @@ -4,7 +4,7 @@ lightning-fundpsbt -- Command to populate PSBT inputs from the wallet SYNOPSIS -------- -**fundpsbt** *satoshi* *feerate* *startweight* \[*minconf*\] \[*reserve*\] +**fundpsbt** *satoshi* *feerate* *startweight* \[*minconf*\] \[*reserve*\] \[*locktime*\] DESCRIPTION ----------- @@ -36,6 +36,9 @@ outputs should have. Default is 1. *reserve* is a boolean: if true (the default), then *reserveinputs* is called (successfully, with *exclusive* true) on the returned PSBT. +*locktime* is an optional locktime: if not set, it is set to a recent +block height. + EXAMPLE USAGE ------------- diff --git a/doc/lightning-utxopsbt.7 b/doc/lightning-utxopsbt.7 index 6523fa027545..a2fd3b14f508 100644 --- a/doc/lightning-utxopsbt.7 +++ b/doc/lightning-utxopsbt.7 @@ -3,7 +3,7 @@ lightning-utxopsbt - Command to populate PSBT inputs from given UTXOs .SH SYNOPSIS -\fButxopsbt\fR \fIsatoshi\fR \fIfeerate\fR \fIstartweight\fR \fIutxos\fR [\fIreserve\fR] [\fIreservedok\fR] +\fButxopsbt\fR \fIsatoshi\fR \fIfeerate\fR \fIstartweight\fR \fIutxos\fR [\fIreserve\fR] [\fIreservedok\fR] [\fIlocktime\fR] .SH DESCRIPTION @@ -27,6 +27,10 @@ is equivalent to setting it to zero)\. Unless \fIreservedok\fR is set to true (default is false) it will also fail if any of the \fIutxos\fR are already reserved\. + +\fIlocktime\fR is an optional locktime: if not set, it is set to a recent +block height\. + .SH RETURN VALUE On success, returns the \fIpsbt\fR containing the inputs, \fIfeerate_per_kw\fR diff --git a/doc/lightning-utxopsbt.7.md b/doc/lightning-utxopsbt.7.md index b1279ccd7e19..e257cc6d894b 100644 --- a/doc/lightning-utxopsbt.7.md +++ b/doc/lightning-utxopsbt.7.md @@ -4,7 +4,7 @@ lightning-utxopsbt -- Command to populate PSBT inputs from given UTXOs SYNOPSIS -------- -**utxopsbt** *satoshi* *feerate* *startweight* *utxos* \[*reserve*\] \[*reservedok*\] +**utxopsbt** *satoshi* *feerate* *startweight* *utxos* \[*reserve*\] \[*reservedok*\] \[*locktime*\] DESCRIPTION ----------- @@ -26,6 +26,9 @@ is equivalent to setting it to zero). Unless *reservedok* is set to true (default is false) it will also fail if any of the *utxos* are already reserved. +*locktime* is an optional locktime: if not set, it is set to a recent +block height. + RETURN VALUE ------------ diff --git a/tests/test_wallet.py b/tests/test_wallet.py index e8567c2e4620..d2d0833df3aa 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -508,6 +508,9 @@ def test_fundpsbt(node_factory, bitcoind, chainparams): # Should get one input, plus some excess funding = l1.rpc.fundpsbt(amount // 2, feerate, 0, reserve=False) psbt = bitcoind.rpc.decodepsbt(funding['psbt']) + # We can fuzz this up to 99 blocks back. + assert psbt['tx']['locktime'] > bitcoind.rpc.getblockcount() - 100 + assert psbt['tx']['locktime'] <= bitcoind.rpc.getblockcount() assert len(psbt['tx']['vin']) == 1 assert funding['excess_msat'] > Millisatoshi(0) assert funding['excess_msat'] < Millisatoshi(amount // 2 * 1000) @@ -515,9 +518,10 @@ def test_fundpsbt(node_factory, bitcoind, chainparams): assert 'estimated_final_weight' in funding assert 'reservations' not in funding - # This should add 99 to the weight, but otherwise be identical (might choose different inputs though!) - funding2 = l1.rpc.fundpsbt(amount // 2, feerate, 99, reserve=False) + # This should add 99 to the weight, but otherwise be identical (might choose different inputs though!) except for locktime. + funding2 = l1.rpc.fundpsbt(amount // 2, feerate, 99, reserve=False, locktime=bitcoind.rpc.getblockcount() + 1) psbt2 = bitcoind.rpc.decodepsbt(funding2['psbt']) + assert psbt2['tx']['locktime'] == bitcoind.rpc.getblockcount() + 1 assert len(psbt2['tx']['vin']) == 1 assert funding2['excess_msat'] < funding['excess_msat'] assert funding2['feerate_per_kw'] == 7500 @@ -555,6 +559,92 @@ def test_fundpsbt(node_factory, bitcoind, chainparams): l1.rpc.fundpsbt(amount // 2, feerate, 0) +def test_utxopsbt(node_factory, bitcoind): + amount = 1000000 + l1 = node_factory.get_node() + + outputs = [] + # Add a medley of funds to withdraw later, bech32 + p2sh-p2wpkh + txid = bitcoind.rpc.sendtoaddress(l1.rpc.newaddr()['bech32'], + amount / 10**8) + outputs.append((txid, bitcoind.rpc.gettransaction(txid)['details'][0]['vout'])) + txid = bitcoind.rpc.sendtoaddress(l1.rpc.newaddr('p2sh-segwit')['p2sh-segwit'], + amount / 10**8) + outputs.append((txid, bitcoind.rpc.gettransaction(txid)['details'][0]['vout'])) + + bitcoind.generate_block(1) + wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == len(outputs)) + + feerate = '7500perkw' + + # Explicitly spend the first output above. + funding = l1.rpc.utxopsbt(amount // 2, feerate, 0, + ['{}:{}'.format(outputs[0][0], outputs[0][1])], + reserve=False) + psbt = bitcoind.rpc.decodepsbt(funding['psbt']) + # We can fuzz this up to 99 blocks back. + assert psbt['tx']['locktime'] > bitcoind.rpc.getblockcount() - 100 + assert psbt['tx']['locktime'] <= bitcoind.rpc.getblockcount() + assert len(psbt['tx']['vin']) == 1 + assert funding['excess_msat'] > Millisatoshi(0) + assert funding['excess_msat'] < Millisatoshi(amount // 2 * 1000) + assert funding['feerate_per_kw'] == 7500 + assert 'estimated_final_weight' in funding + assert 'reservations' not in funding + + # This should add 99 to the weight, but otherwise be identical except for locktime. + funding2 = l1.rpc.utxopsbt(amount // 2, feerate, 99, + ['{}:{}'.format(outputs[0][0], outputs[0][1])], + reserve=False, locktime=bitcoind.rpc.getblockcount() + 1) + psbt2 = bitcoind.rpc.decodepsbt(funding2['psbt']) + assert psbt2['tx']['locktime'] == bitcoind.rpc.getblockcount() + 1 + assert psbt2['tx']['vin'] == psbt['tx']['vin'] + assert psbt2['tx']['vout'] == psbt['tx']['vout'] + assert funding2['excess_msat'] < funding['excess_msat'] + assert funding2['feerate_per_kw'] == 7500 + assert funding2['estimated_final_weight'] == funding['estimated_final_weight'] + 99 + assert 'reservations' not in funding2 + + # Cannot afford this one (too much) + with pytest.raises(RpcError, match=r"not afford"): + l1.rpc.utxopsbt(amount, feerate, 0, + ['{}:{}'.format(outputs[0][0], outputs[0][1])]) + + # Nor this (even with both) + with pytest.raises(RpcError, match=r"not afford"): + l1.rpc.utxopsbt(amount * 2, feerate, 0, + ['{}:{}'.format(outputs[0][0], outputs[0][1]), + '{}:{}'.format(outputs[1][0], outputs[1][1])]) + + # Should get two inputs (and reserve!) + funding = l1.rpc.utxopsbt(amount, feerate, 0, + ['{}:{}'.format(outputs[0][0], outputs[0][1]), + '{}:{}'.format(outputs[1][0], outputs[1][1])]) + psbt = bitcoind.rpc.decodepsbt(funding['psbt']) + assert len(psbt['tx']['vin']) == 2 + assert len(funding['reservations']) == 2 + assert funding['reservations'][0]['txid'] == outputs[0][0] + assert funding['reservations'][0]['vout'] == outputs[0][1] + assert funding['reservations'][0]['was_reserved'] is False + assert funding['reservations'][0]['reserved'] is True + assert funding['reservations'][1]['txid'] == outputs[1][0] + assert funding['reservations'][1]['vout'] == outputs[1][1] + assert funding['reservations'][1]['was_reserved'] is False + assert funding['reservations'][1]['reserved'] is True + + # Should refuse to use reserved outputs. + with pytest.raises(RpcError, match=r"already reserved"): + l1.rpc.utxopsbt(amount, feerate, 0, + ['{}:{}'.format(outputs[0][0], outputs[0][1]), + '{}:{}'.format(outputs[1][0], outputs[1][1])]) + + # Unless we tell it that's ok. + l1.rpc.utxopsbt(amount, feerate, 0, + ['{}:{}'.format(outputs[0][0], outputs[0][1]), + '{}:{}'.format(outputs[1][0], outputs[1][1])], + reservedok=True) + + def test_sign_and_send_psbt(node_factory, bitcoind, chainparams): """ Tests for the sign + send psbt RPCs @@ -648,17 +738,42 @@ def test_sign_and_send_psbt(node_factory, bitcoind, chainparams): with pytest.raises(RpcError, match=r"No wallet inputs to sign"): l1.rpc.signpsbt(l2_funding['psbt']) + # With signonly it will fail if it can't sign it. + with pytest.raises(RpcError, match=r"is unknown"): + l1.rpc.signpsbt(l2_funding['psbt'], signonly=[0]) + # Add some of our own PSBT inputs to it l1_funding = l1.rpc.fundpsbt(satoshi=Millisatoshi(3 * amount * 1000), feerate=7500, startweight=42, reserve=True) + l1_num_inputs = len(bitcoind.rpc.decodepsbt(l1_funding['psbt'])['tx']['vin']) + l2_num_inputs = len(bitcoind.rpc.decodepsbt(l2_funding['psbt'])['tx']['vin']) - # Join and add an output + # Join and add an output (reorders!) joint_psbt = bitcoind.rpc.joinpsbts([l1_funding['psbt'], l2_funding['psbt'], output_pbst]) - half_signed_psbt = l1.rpc.signpsbt(joint_psbt)['signed_psbt'] + # Ask it to sign inputs it doesn't know, it will fail. + with pytest.raises(RpcError, match=r"is unknown"): + l1.rpc.signpsbt(joint_psbt, + signonly=list(range(l1_num_inputs + 1))) + + # Similarly, it can't sign inputs it doesn't know. + sign_success = [] + for i in range(l1_num_inputs + l2_num_inputs): + try: + l1.rpc.signpsbt(joint_psbt, signonly=[i]) + except RpcError: + continue + sign_success.append(i) + assert len(sign_success) == l1_num_inputs + + # But it can sign all the valid ones at once. + half_signed_psbt = l1.rpc.signpsbt(joint_psbt, signonly=sign_success)['signed_psbt'] + for s in sign_success: + assert bitcoind.rpc.decodepsbt(half_signed_psbt)['inputs'][s]['partial_signatures'] is not None + totally_signed = l2.rpc.signpsbt(half_signed_psbt)['signed_psbt'] broadcast_tx = l1.rpc.sendpsbt(totally_signed) diff --git a/wallet/reservation.c b/wallet/reservation.c index 1ea8aa19447b..5b3a764b510f 100644 --- a/wallet/reservation.c +++ b/wallet/reservation.c @@ -213,11 +213,11 @@ static struct command_result *finish_psbt(struct command *cmd, u32 feerate_per_kw, size_t weight, struct amount_sat excess, - bool reserve) + bool reserve, + u32 *locktime) { struct json_stream *response; struct bitcoin_tx *tx; - u32 locktime; u32 current_height = get_block_height(cmd->ld->topology); /* Setting the locktime to the next block to be mined has multiple @@ -229,18 +229,21 @@ static struct command_result *finish_psbt(struct command *cmd, * 0xFFFFFFFD by default. Other wallets are likely to implement * this too). */ - locktime = current_height; + if (!locktime) { + locktime = tal(cmd, u32); + *locktime = current_height; - /* Eventually fuzz it too. */ - if (locktime > 100 && pseudorand(10) == 0) - locktime -= pseudorand(100); + /* Eventually fuzz it too. */ + if (*locktime > 100 && pseudorand(10) == 0) + *locktime -= pseudorand(100); + } /* FIXME: tx_spending_utxos does more than we need, but there * are other users right now. */ tx = tx_spending_utxos(cmd, chainparams, cast_const2(const struct utxo **, utxos), cmd->ld->wallet->bip32_base, - false, 0, locktime, + false, 0, *locktime, BITCOIN_TX_RBF_SEQUENCE); response = json_stream_success(cmd); @@ -264,7 +267,7 @@ static struct command_result *json_fundpsbt(struct command *cmd, u32 *minconf, *weight; struct amount_sat *amount, input, diff; bool all, *reserve; - u32 maxheight; + u32 *locktime, maxheight; if (!param(cmd, buffer, params, p_req("satoshi", param_sat_or_all, &amount), @@ -272,6 +275,7 @@ static struct command_result *json_fundpsbt(struct command *cmd, p_req("startweight", param_number, &weight), p_opt_def("minconf", param_number, &minconf, 1), p_opt_def("reserve", param_bool, &reserve, true), + p_opt("locktime", param_number, &locktime), NULL)) return command_param_failed(); @@ -336,7 +340,8 @@ static struct command_result *json_fundpsbt(struct command *cmd, tal_count(utxos)); } - return finish_psbt(cmd, utxos, *feerate_per_kw, *weight, diff, *reserve); + return finish_psbt(cmd, utxos, *feerate_per_kw, *weight, diff, *reserve, + locktime); } static const struct json_command fundpsbt_command = { @@ -422,7 +427,7 @@ static struct command_result *json_utxopsbt(struct command *cmd, u32 *feerate_per_kw, *weight; bool all, *reserve, *reserved_ok; struct amount_sat *amount, input, excess; - u32 current_height; + u32 current_height, *locktime; if (!param(cmd, buffer, params, p_req("satoshi", param_sat_or_all, &amount), @@ -431,6 +436,7 @@ static struct command_result *json_utxopsbt(struct command *cmd, p_req("utxos", param_txout, &utxos), p_opt_def("reserve", param_bool, &reserve, true), p_opt_def("reservedok", param_bool, &reserved_ok, false), + p_opt("locktime", param_number, &locktime), NULL)) return command_param_failed(); @@ -474,7 +480,7 @@ static struct command_result *json_utxopsbt(struct command *cmd, } return finish_psbt(cmd, utxos, *feerate_per_kw, *weight, excess, - *reserve); + *reserve, locktime); } static const struct json_command utxopsbt_command = { "utxopsbt", diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c index e47e09fcca03..b1d18affd813 100644 --- a/wallet/walletrpc.c +++ b/wallet/walletrpc.c @@ -1221,8 +1221,17 @@ struct command_result *param_psbt(struct command *cmd, json_tok_full(buffer, tok)); } +static bool in_only_inputs(const u32 *only_inputs, u32 this) +{ + for (size_t i = 0; i < tal_count(only_inputs); i++) + if (only_inputs[i] == this) + return true; + return false; +} + static struct command_result *match_psbt_inputs_to_utxos(struct command *cmd, struct wally_psbt *psbt, + const u32 *only_inputs, struct utxo ***utxos) { *utxos = tal_arr(cmd, struct utxo *, 0); @@ -1230,11 +1239,21 @@ static struct command_result *match_psbt_inputs_to_utxos(struct command *cmd, struct utxo *utxo; struct bitcoin_txid txid; + if (only_inputs && !in_only_inputs(only_inputs, i)) + continue; + wally_tx_input_get_txid(&psbt->tx->inputs[i], &txid); utxo = wallet_utxo_get(*utxos, cmd->ld->wallet, &txid, psbt->tx->inputs[i].index); - if (!utxo) + if (!utxo) { + if (only_inputs) + return command_fail(cmd, LIGHTNINGD, + "Aborting PSBT signing. UTXO %s:%u is unknown (and specified by signonly)", + type_to_string(tmpctx, struct bitcoin_txid, + &txid), + psbt->tx->inputs[i].index); continue; + } /* Oops we haven't reserved this utxo yet! */ if (!is_reserved(utxo, get_block_height(cmd->ld->topology))) @@ -1249,6 +1268,32 @@ static struct command_result *match_psbt_inputs_to_utxos(struct command *cmd, return NULL; } +static struct command_result *param_input_numbers(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *tok, + u32 **input_nums) +{ + struct command_result *res; + const jsmntok_t *arr, *t; + size_t i; + + res = param_array(cmd, name, buffer, tok, &arr); + if (res) + return res; + + *input_nums = tal_arr(cmd, u32, arr->size); + json_for_each_arr(i, t, arr) { + u32 *num; + res = param_number(cmd, name, buffer, t, &num); + if (res) + return res; + (*input_nums)[i] = *num; + tal_free(num); + } + return NULL; +} + static struct command_result *json_signpsbt(struct command *cmd, const char *buffer, const jsmntok_t *obj UNNEEDED, @@ -1258,17 +1303,27 @@ static struct command_result *json_signpsbt(struct command *cmd, struct json_stream *response; struct wally_psbt *psbt, *signed_psbt; struct utxo **utxos; + u32 *input_nums; if (!param(cmd, buffer, params, p_req("psbt", param_psbt, &psbt), + p_opt("signonly", param_input_numbers, &input_nums), NULL)) return command_param_failed(); + /* Sanity check! */ + for (size_t i = 0; i < tal_count(input_nums); i++) { + if (input_nums[i] >= psbt->num_inputs) + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "signonly[%zu]: %u out of range", + i, input_nums[i]); + } + /* We have to find/locate the utxos that are ours on this PSBT, * so that the HSM knows how/what to sign for (it's possible some of * our utxos require more complicated data to sign for e.g. * closeinfo outputs */ - res = match_psbt_inputs_to_utxos(cmd, psbt, &utxos); + res = match_psbt_inputs_to_utxos(cmd, psbt, input_nums, &utxos); if (res) return res; @@ -1334,7 +1389,7 @@ static struct command_result *json_sendpsbt(struct command *cmd, /* We have to find/locate the utxos that are ours on this PSBT, * so that we know who to mark as used. */ - res = match_psbt_inputs_to_utxos(cmd, psbt, &utxos); + res = match_psbt_inputs_to_utxos(cmd, psbt, NULL, &utxos); if (res) return res;