diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index 208a0fb27..bb80fb4d3 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -1140,6 +1140,21 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("fundpsbt", payload) + 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): """ Add internal wallet's signatures to PSBT diff --git a/doc/lightning-utxopsbt.7 b/doc/lightning-utxopsbt.7 index 6523fa027..a2fd3b14f 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 b1279ccd7..e257cc6d8 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 f21f93bb1..9588cf4f5 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -559,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 diff --git a/wallet/reservation.c b/wallet/reservation.c index b647c0072..5b3a764b5 100644 --- a/wallet/reservation.c +++ b/wallet/reservation.c @@ -427,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), @@ -436,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(); @@ -479,7 +480,7 @@ static struct command_result *json_utxopsbt(struct command *cmd, } return finish_psbt(cmd, utxos, *feerate_per_kw, *weight, excess, - *reserve, NULL); + *reserve, locktime); } static const struct json_command utxopsbt_command = { "utxopsbt",