diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index aa1c2b590..394a27052 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -1319,7 +1319,7 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("unreserveinputs", payload) - def fundpsbt(self, satoshi, feerate, startweight, minconf=None, reserve=True, locktime=None, min_witness_weight=None): + def fundpsbt(self, satoshi, feerate, startweight, minconf=None, reserve=True, locktime=None, min_witness_weight=None, excess_as_change=False): """ Create a PSBT with inputs sufficient to give an output of satoshi. """ @@ -1331,10 +1331,11 @@ class LightningRpc(UnixDomainSocketRpc): "reserve": reserve, "locktime": locktime, "min_witness_weight": min_witness_weight, + "excess_as_change": excess_as_change, } return self.call("fundpsbt", payload) - def utxopsbt(self, satoshi, feerate, startweight, utxos, reserve=True, reservedok=False, locktime=None, min_witness_weight=None): + def utxopsbt(self, satoshi, feerate, startweight, utxos, reserve=True, reservedok=False, locktime=None, min_witness_weight=None, excess_as_change=False): """ Create a PSBT with given inputs, to give an output of satoshi. """ @@ -1347,6 +1348,7 @@ class LightningRpc(UnixDomainSocketRpc): "reservedok": reservedok, "locktime": locktime, "min_witness_weight": min_witness_weight, + "excess_as_change": excess_as_change, } return self.call("utxopsbt", payload) diff --git a/doc/lightning-fundpsbt.7 b/doc/lightning-fundpsbt.7 index 633d32c4b..ad051da10 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] [\fIlocktime\fR] [\fImin_witness_weight\fR] +\fBfundpsbt\fR \fIsatoshi\fR \fIfeerate\fR \fIstartweight\fR [\fIminconf\fR] [\fIreserve\fR] [\fIlocktime\fR] [\fImin_witness_weight\fR] [\fIexcess_as_change\fR] .SH DESCRIPTION @@ -48,6 +48,10 @@ block height\. witness\. If the actual witness weight is greater than the provided minimum, the actual witness weight will be used\. + +\fIexcess_as_change\fR is an optional boolean to flag to add a change output +for the excess sats\. + .SH EXAMPLE USAGE Let's assume the caller is trying to produce a 100,000 satoshi output\. @@ -85,6 +89,13 @@ If \fIreserve\fR was true, then a \fIreservations\fR array is returned, exactly like \fIreserveinputs\fR\. +If \fIexcess_as_change\fR is true and the excess is enough to cover +an additional output above the \fBdust_limit\fR, then an output is +added to the PSBT for the excess amount\. The \fIexcess_msat\fR will +be zero\. A \fIchange_outnum\fR will be returned with the index of +the change output\. + + On error the returned object will contain \fBcode\fR and \fBmessage\fR properties, with \fBcode\fR being one of the following: @@ -109,4 +120,4 @@ Rusty Russell \fI is mainly responsible\. Main web site: \fIhttps://github.com/ElementsProject/lightning\fR -\" SHA256STAMP:a8b9705274638127c2f5ec4e97ed94e6d7f6b6b10a76c2248e8bc8b36dd804ff +\" SHA256STAMP:b9ecd1408f0e5d8424e530ab44ab21b0e773c537c3512b68b31f197851d9abce diff --git a/doc/lightning-fundpsbt.7.md b/doc/lightning-fundpsbt.7.md index 1b7cd3939..bf7f211c6 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*\] \[*locktime*\] \[*min_witness_weight*\] +**fundpsbt** *satoshi* *feerate* *startweight* \[*minconf*\] \[*reserve*\] \[*locktime*\] \[*min_witness_weight*\] \[*excess_as_change*\] DESCRIPTION ----------- @@ -43,6 +43,9 @@ block height. witness. If the actual witness weight is greater than the provided minimum, the actual witness weight will be used. +*excess_as_change* is an optional boolean to flag to add a change output +for the excess sats. + EXAMPLE USAGE ------------- @@ -77,6 +80,12 @@ for the weights of the inputs and startweight. If *reserve* was true, then a *reservations* array is returned, exactly like *reserveinputs*. +If *excess_as_change* is true and the excess is enough to cover +an additional output above the `dust_limit`, then an output is +added to the PSBT for the excess amount. The *excess_msat* will +be zero. A *change_outnum* will be returned with the index of +the change output. + On error the returned object will contain `code` and `message` properties, with `code` being one of the following: diff --git a/doc/lightning-utxopsbt.7 b/doc/lightning-utxopsbt.7 index 50354766b..77fa6eb48 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] [\fIlocktime\fR] [\fImin_witness_weight\fR] +\fButxopsbt\fR \fIsatoshi\fR \fIfeerate\fR \fIstartweight\fR \fIutxos\fR [\fIreserve\fR] [\fIreservedok\fR] [\fIlocktime\fR] [\fImin_witness_weight\fR] [\fIexcess_as_change\fR] .SH DESCRIPTION @@ -36,6 +36,10 @@ block height\. witness\. If the actual witness weight is greater than the provided minimum, the actual witness weight will be used\. + +\fIexcess_as_change\fR is an optional boolean to flag to add a change output +for the excess sats\. + .SH RETURN VALUE On success, returns the \fIpsbt\fR containing the inputs, \fIfeerate_per_kw\fR @@ -51,6 +55,13 @@ If \fIreserve\fR was true, then a \fIreservations\fR array is returned, exactly like \fIreserveinputs\fR\. +If \fIexcess_as_change\fR is true and the excess is enough to cover +an additional output above the \fBdust_limit\fR, then an output is +added to the PSBT for the excess amount\. The \fIexcess_msat\fR will +be zero\. A \fIchange_outnum\fR will be returned with the index of +the change output\. + + On error the returned object will contain \fBcode\fR and \fBmessage\fR properties, with \fBcode\fR being one of the following: @@ -75,4 +86,4 @@ Rusty Russell \fI is mainly responsible\. Main web site: \fIhttps://github.com/ElementsProject/lightning\fR -\" SHA256STAMP:777710bb963f435193e92a55344c740c123d7aa4d54bf573c99a616f59eeee54 +\" SHA256STAMP:a56057522ed576f232d7d96794f39cc67a5a1b1bb7b6f7912e42c3769555e007 diff --git a/doc/lightning-utxopsbt.7.md b/doc/lightning-utxopsbt.7.md index f4706ff89..6fa930bc8 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*\] \[*locktime*\] \[*min_witness_weight*\] +**utxopsbt** *satoshi* *feerate* *startweight* *utxos* \[*reserve*\] \[*reservedok*\] \[*locktime*\] \[*min_witness_weight*\] \[*excess_as_change*\] DESCRIPTION ----------- @@ -33,6 +33,9 @@ block height. witness. If the actual witness weight is greater than the provided minimum, the actual witness weight will be used. +*excess_as_change* is an optional boolean to flag to add a change output +for the excess sats. + RETURN VALUE ------------ @@ -47,6 +50,12 @@ for the weights of the inputs and *startweight*. If *reserve* was true, then a *reservations* array is returned, exactly like *reserveinputs*. +If *excess_as_change* is true and the excess is enough to cover +an additional output above the `dust_limit`, then an output is +added to the PSBT for the excess amount. The *excess_msat* will +be zero. A *change_outnum* will be returned with the index of +the change output. + On error the returned object will contain `code` and `message` properties, with `code` being one of the following: diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 1bab3ddf1..4e02b2bb7 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -515,6 +515,18 @@ def test_fundpsbt(node_factory, bitcoind, chainparams): with pytest.raises(RpcError, match=r"not afford"): l1.rpc.fundpsbt(amount // 2, feerate, 0, minconf=2) + funding3 = l1.rpc.fundpsbt(amount // 2, feerate, 0, reserve=False, excess_as_change=True) + assert funding3['excess_msat'] == Millisatoshi(0) + # Should have the excess msat as the output value (minus fee for change) + psbt = bitcoind.rpc.decodepsbt(funding3['psbt']) + change = Millisatoshi("{}btc".format(psbt['tx']['vout'][funding3['change_outnum']]['value'])) + # The weight should be greater (now includes change output) + change_weight = funding3['estimated_final_weight'] - funding['estimated_final_weight'] + assert change_weight > 0 + # Check that the amount is ok (equal to excess minus change fee) + change_fee = Millisatoshi(7500 * change_weight) + assert funding['excess_msat'] == change + change_fee + # Should get two inputs. psbt = bitcoind.rpc.decodepsbt(l1.rpc.fundpsbt(amount, feerate, 0, reserve=False)['psbt']) assert len(psbt['tx']['vin']) == 2 @@ -602,6 +614,29 @@ def test_utxopsbt(node_factory, bitcoind, chainparams): ['{}:{}'.format(outputs[0][0], outputs[0][1]), '{}:{}'.format(outputs[1][0], outputs[1][1])]) + funding3 = l1.rpc.utxopsbt(amount // 2, feerate, 0, + ['{}:{}'.format(outputs[0][0], outputs[0][1])], + reserve=False, + excess_as_change=True) + assert funding3['excess_msat'] == Millisatoshi(0) + # Should have the excess msat as the output value (minus fee for change) + psbt = bitcoind.rpc.decodepsbt(funding3['psbt']) + change = Millisatoshi("{}btc".format(psbt['tx']['vout'][funding3['change_outnum']]['value'])) + # The weight should be greater (now includes change output) + change_weight = funding3['estimated_final_weight'] - funding['estimated_final_weight'] + assert change_weight > 0 + # Check that the amount is ok (equal to excess minus change fee) + change_fee = Millisatoshi(fee_val * change_weight // 1000 * 1000) + assert funding['excess_msat'] == change + change_fee + + # Do it again, but without enough for change! + funding4 = l1.rpc.utxopsbt(amount - 3500, + feerate, 0, + ['{}:{}'.format(outputs[0][0], outputs[0][1])], + reserve=False, + excess_as_change=True) + assert 'change_outnum' not in funding4 + # Should get two inputs (and reserve!) funding = l1.rpc.utxopsbt(amount, feerate, 0, ['{}:{}'.format(outputs[0][0], outputs[0][1]), diff --git a/wallet/reservation.c b/wallet/reservation.c index 53c7a60cc..1e678608e 100644 --- a/wallet/reservation.c +++ b/wallet/reservation.c @@ -1,6 +1,7 @@ /* Dealing with reserving UTXOs */ #include #include +#include #include #include #include @@ -319,10 +320,12 @@ static struct command_result *finish_psbt(struct command *cmd, size_t weight, struct amount_sat excess, bool reserve, - u32 *locktime) + u32 *locktime, + bool excess_as_change) { struct json_stream *response; struct wally_psbt *psbt; + size_t change_outnum; u32 current_height = get_block_height(cmd->ld->topology); /* Setting the locktime to the next block to be mined has multiple @@ -347,6 +350,44 @@ static struct command_result *finish_psbt(struct command *cmd, cmd->ld->wallet->bip32_base, *locktime, BITCOIN_TX_RBF_SEQUENCE); + /* Should we add a change output for the excess? */ + if (excess_as_change) { + struct amount_sat change; + struct pubkey pubkey; + s64 keyidx; + u8 *b32script; + + /* Checks for dust, returns 0sat if below dust */ + change = change_amount(excess, feerate_per_kw); + if (!amount_sat_greater(change, AMOUNT_SAT(0))) { + excess_as_change = false; + goto fee_calc; + } + + /* 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 (!bip32_pubkey(cmd->ld->wallet->bip32_base, &pubkey, keyidx)) + return command_fail(cmd, LIGHTNINGD, + "Failed to generate change address." + " Keys generation failure"); + b32script = scriptpubkey_p2wpkh(tmpctx, &pubkey); + txfilter_add_scriptpubkey(cmd->ld->owned_txfilter, b32script); + + change_outnum = psbt->num_outputs; + psbt_append_output(psbt, b32script, change); + /* Set excess to zero */ + excess = AMOUNT_SAT(0); + /* Add additional weight of output */ + weight += bitcoin_tx_output_weight( + BITCOIN_SCRIPTPUBKEY_P2WPKH_LEN); + } + +fee_calc: /* Add a fee output if this is elements */ if (is_elements(chainparams)) { struct amount_sat est_fee = @@ -358,6 +399,8 @@ static struct command_result *finish_psbt(struct command *cmd, json_add_num(response, "feerate_per_kw", feerate_per_kw); json_add_num(response, "estimated_final_weight", weight); json_add_amount_sat_only(response, "excess_msat", excess); + if (excess_as_change) + json_add_num(response, "change_outnum", change_outnum); if (reserve) reserve_and_report(response, cmd->ld->wallet, current_height, utxos); @@ -388,7 +431,7 @@ static struct command_result *json_fundpsbt(struct command *cmd, u32 *feerate_per_kw; u32 *minconf, *weight, *min_witness_weight; struct amount_sat *amount, input, diff; - bool all, *reserve; + bool all, *reserve, *excess_as_change; u32 *locktime, maxheight; if (!param(cmd, buffer, params, @@ -400,6 +443,8 @@ static struct command_result *json_fundpsbt(struct command *cmd, p_opt("locktime", param_number, &locktime), p_opt_def("min_witness_weight", param_number, &min_witness_weight, 0), + p_opt_def("excess_as_change", param_bool, + &excess_as_change, false), NULL)) return command_param_failed(); @@ -474,7 +519,7 @@ static struct command_result *json_fundpsbt(struct command *cmd, } return finish_psbt(cmd, utxos, *feerate_per_kw, *weight, diff, *reserve, - locktime); + locktime, *excess_as_change); } static const struct json_command fundpsbt_command = { @@ -558,7 +603,7 @@ static struct command_result *json_utxopsbt(struct command *cmd, { struct utxo **utxos; u32 *feerate_per_kw, *weight, *min_witness_weight; - bool all, *reserve, *reserved_ok; + bool all, *reserve, *reserved_ok, *excess_as_change; struct amount_sat *amount, input, excess; u32 current_height, *locktime; @@ -572,6 +617,8 @@ static struct command_result *json_utxopsbt(struct command *cmd, p_opt("locktime", param_number, &locktime), p_opt_def("min_witness_weight", param_number, &min_witness_weight, 0), + p_opt_def("excess_as_change", param_bool, + &excess_as_change, false), NULL)) return command_param_failed(); @@ -615,7 +662,7 @@ static struct command_result *json_utxopsbt(struct command *cmd, } return finish_psbt(cmd, utxos, *feerate_per_kw, *weight, excess, - *reserve, locktime); + *reserve, locktime, *excess_as_change); } static const struct json_command utxopsbt_command = { "utxopsbt",