diff --git a/.msggen.json b/.msggen.json index 4b19ed69ca93..a2c02d736877 100644 --- a/.msggen.json +++ b/.msggen.json @@ -745,7 +745,8 @@ "FundPsbt.excess_msat": 4, "FundPsbt.feerate_per_kw": 2, "FundPsbt.psbt": 1, - "FundPsbt.reservations[]": 6 + "FundPsbt.reservations[]": 6, + "FundPsbt.utxo_string": 7 }, "GetinfoAddress": { "Getinfo.address[].address": 3, @@ -1541,7 +1542,8 @@ }, "SignpsbtRequest": { "SignPsbt.psbt": 1, - "SignPsbt.signonly[]": 2 + "SignPsbt.signonly[]": 2, + "SignPsbt.utxo_string": 3 }, "SignpsbtResponse": { "SignPsbt.signed_psbt": 1 @@ -3009,6 +3011,10 @@ "added": "pre-v0.10.1", "deprecated": false }, + "FundPsbt.utxo_string": { + "added": "v23.11", + "deprecated": false + }, "GetRoute": { "added": "pre-v0.10.1", "deprecated": null @@ -5453,6 +5459,10 @@ "added": "pre-v0.10.1", "deprecated": false }, + "SignPsbt.utxo_string": { + "added": "v23.11", + "deprecated": false + }, "StaticBackup": { "added": "pre-v0.10.1", "deprecated": null diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index 70b693b514b7..50c9cf348330 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -1570,13 +1570,15 @@ def utxopsbt(self, satoshi, feerate, startweight, utxos, reserve=None, reservedo } return self.call("utxopsbt", payload) - def signpsbt(self, psbt, signonly=None): + def signpsbt(self, psbt, signonly=None, utxo_string=None, unsafe_sign_all=None): """ Add internal wallet's signatures to PSBT """ payload = { "psbt": psbt, "signonly": signonly, + "utxo_string": utxo_string, + "unsafe_sign_all": unsafe_sign_all, } return self.call("signpsbt", payload) diff --git a/doc/lightning-fundpsbt.7.md b/doc/lightning-fundpsbt.7.md index eaac0fb05e3c..6223abcf759f 100644 --- a/doc/lightning-fundpsbt.7.md +++ b/doc/lightning-fundpsbt.7.md @@ -81,6 +81,7 @@ On success, an object is returned, containing: - **was\_reserved** (boolean): Whether this output was previously reserved (always *false*) - **reserved** (boolean): Whether this output is now reserved (always *true*) - **reserved\_to\_block** (u32): The blockheight the reservation will expire +- **utxo\_string** (string, optional): A command seperated list of utxos used to fund the psbt *(added v24.02)* [comment]: # (GENERATE-FROM-SCHEMA-END) @@ -112,4 +113,4 @@ RESOURCES Main web site: -[comment]: # ( SHA256STAMP:13e35920ba8810db082e3cca62d1141a67498a2756da2479a24eaa62567ff4fe) +[comment]: # ( SHA256STAMP:8c942b9a2303a05e3cda8e3190b4c299ede6020849573ef4c4ca7ee4c4363fae) diff --git a/doc/lightning-signpsbt.7.md b/doc/lightning-signpsbt.7.md index f0809ccd15a7..56238864be03 100644 --- a/doc/lightning-signpsbt.7.md +++ b/doc/lightning-signpsbt.7.md @@ -13,11 +13,18 @@ DESCRIPTION BIP-174. - *psbt*: A string that represents the PSBT value. -- *signonly*: An optional array of input numbers to sign. +- *signonly*: An an array of input numbers to sign. +- *utxo\_string*: An an array of outputs to sign. +- *unsafe\_sign\_all*: A flag to recklessly sign all reserved inputs. -By default, all known inputs are signed, and others ignored: with -*signonly*, only those inputs are signed, and an error is returned if -one of them cannot be signed. +Specify which inputs to sign by either an array of input indices to sign with +*sigonly* or a *utxo\_string*. + +*utxo\_string* is in the format of a txid and outnum seperated by a colon. +Multiple utxos can be added by putting a comma inbetween entries. As such: +txid:outnum,txid:outnum... This is the same format returned by fundpsbt. + +An error is returned if one of them cannot be signed. Note that the command will fail if there are no inputs to sign, or if the inputs to be signed were not previously reserved. @@ -30,7 +37,8 @@ EXAMPLE JSON REQUEST "id": 82, "method": "signpsbt", "params": { - "psbt": "some_psbt" + "psbt": "some_psbt", + "utxo_string": "txid:outnum,txid:outnum" } } ``` diff --git a/doc/lightning-splice_init.7.md b/doc/lightning-splice_init.7.md index 5c7a4c3e2b88..0d22e0e95375 100644 --- a/doc/lightning-splice_init.7.md +++ b/doc/lightning-splice_init.7.md @@ -46,6 +46,7 @@ echo $RESULT; RESULT=$(lightning-cli fundpsbt -k satoshi=100000sat feerate=urgent startweight=800 excess_as_change=true); INITIALPSBT=$(echo $RESULT | jq -r ".psbt"); +UTXO_STRING=$(echo $RESULT | jq -r ".utxo_string"); echo $RESULT; RESULT=$(lightning-cli splice_init $CHANNEL_ID 100000 $INITIALPSBT); @@ -56,7 +57,7 @@ RESULT=$(lightning-cli splice_update $CHANNEL_ID $PSBT); PSBT=$(echo $RESULT | jq -r ".psbt"); echo $RESULT; -RESULT=$(lightning-cli signpsbt -k psbt="$PSBT"); +RESULT=$(lightning-cli signpsbt -k psbt="$PSBT" utxo_string="$UTXO_STRING"); PSBT=$(echo $RESULT | jq -r ".signed_psbt"); echo $RESULT; diff --git a/doc/schemas/fundpsbt.schema.json b/doc/schemas/fundpsbt.schema.json index 84f0969e77f9..59b0cdacb983 100644 --- a/doc/schemas/fundpsbt.schema.json +++ b/doc/schemas/fundpsbt.schema.json @@ -71,6 +71,11 @@ } } } + }, + "utxo_string": { + "type": "string", + "description": "A command seperated list of utxos used to fund the psbt", + "added": "v23.11" } } } diff --git a/doc/schemas/signpsbt.request.json b/doc/schemas/signpsbt.request.json index c8a2bbf85555..8b8b79d19800 100644 --- a/doc/schemas/signpsbt.request.json +++ b/doc/schemas/signpsbt.request.json @@ -14,6 +14,14 @@ "items": { "type": "u32" } + }, + "utxo_string": { + "type": "string", + "added": "v23.11" + }, + "unsafe_sign_all": { + "type": "bool", + "added": "v23.11" } } } diff --git a/external/lnprototest b/external/lnprototest index a8d4dcf6b859..e58d9e338c80 160000 --- a/external/lnprototest +++ b/external/lnprototest @@ -1 +1 @@ -Subproject commit a8d4dcf6b859f3c11c92a917f7c48b2051dbb4c4 +Subproject commit e58d9e338c80dea679b27455cf3f59e7ba5fddb5 diff --git a/tests/test_closing.py b/tests/test_closing.py index 28e40fac0944..80d363c01f86 100644 --- a/tests/test_closing.py +++ b/tests/test_closing.py @@ -3717,13 +3717,15 @@ def test_closing_anchorspend_htlc_tx_rbf(node_factory, bitcoind): # We reduce l1's UTXOs so it's forced to use more than one UTXO to push. fundsats = int(only_one(l1.rpc.listfunds()['outputs'])['amount_msat'].to_satoshi()) - psbt = l1.rpc.fundpsbt("all", "1000perkw", 1000)['psbt'] + result = l1.rpc.fundpsbt("all", "1000perkw", 1000) + psbt = result['psbt'] + utxo_string = result['utxo_string'] # Pay 5k sats in fees, send most to l2 psbt = l1.rpc.addpsbtoutput(fundsats - 20000 - 5000, psbt, destination=l2.rpc.newaddr()['bech32'])['psbt'] # 10x2000 sat outputs for l1 to use. for i in range(10): psbt = l1.rpc.addpsbtoutput(2000, psbt)['psbt'] - l1.rpc.sendpsbt(l1.rpc.signpsbt(psbt)['signed_psbt']) + l1.rpc.sendpsbt(l1.rpc.signpsbt(psbt, utxo_string=utxo_string)['signed_psbt']) bitcoind.generate_block(1, wait_for_mempool=1) sync_blockheight(bitcoind, [l1]) @@ -3912,12 +3914,14 @@ def test_peer_anchor_push(node_factory, bitcoind, executor, chainparams): fundsats = int(only_one(l2.rpc.listfunds()['outputs'])['amount_msat'].to_satoshi()) OUTPUT_SAT = 10000 NUM_OUTPUTS = 10 - psbt = l2.rpc.fundpsbt("all", "1000perkw", 1000)['psbt'] + result = l2.rpc.fundpsbt("all", "1000perkw", 1000) + psbt = result['psbt'] + utxo_string = result['utxo_string'] # Pay 5k sats in fees. psbt = l2.rpc.addpsbtoutput(fundsats - OUTPUT_SAT * NUM_OUTPUTS - 5000, psbt, destination=l3.rpc.newaddr()['bech32'])['psbt'] for _ in range(NUM_OUTPUTS): psbt = l2.rpc.addpsbtoutput(OUTPUT_SAT, psbt)['psbt'] - l2.rpc.sendpsbt(l2.rpc.signpsbt(psbt)['signed_psbt']) + l2.rpc.sendpsbt(l2.rpc.signpsbt(psbt, utxo_string=utxo_string)['signed_psbt']) bitcoind.generate_block(1, wait_for_mempool=1) sync_blockheight(bitcoind, [l2]) diff --git a/tests/test_opening.py b/tests/test_opening.py index 25ff00fd1eb7..0911715741fe 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -386,7 +386,7 @@ def test_v2_rbf_single(node_factory, bitcoind, chainparams): assert int(info_2['next_feerate'][:-5]) == rate * 65 // 64 # Sign our inputs, and continue - signed_psbt = l1.rpc.signpsbt(update['psbt'])['signed_psbt'] + signed_psbt = l1.rpc.signpsbt(update['psbt'], unsafe_sign_all=True)['signed_psbt'] # Fails because we didn't put enough feerate in. with pytest.raises(RpcError, match=r'insufficient fee'): @@ -412,7 +412,7 @@ def test_v2_rbf_single(node_factory, bitcoind, chainparams): funding_feerate=next_rate) update = l1.rpc.openchannel_update(chan_id, bump['psbt']) assert update['commitments_secured'] - signed_psbt = l1.rpc.signpsbt(update['psbt'])['signed_psbt'] + signed_psbt = l1.rpc.signpsbt(update['psbt'], unsafe_sign_all=True)['signed_psbt'] l1.rpc.openchannel_signed(chan_id, signed_psbt) bitcoind.generate_block(1) @@ -505,7 +505,7 @@ def test_v2_rbf_abort_retry(node_factory, bitcoind, chainparams): funding_feerate=next_rate) update = l1.rpc.openchannel_update(chan_id, bump['psbt']) - signed_psbt = l1.rpc.signpsbt(update['psbt'])['signed_psbt'] + signed_psbt = l1.rpc.signpsbt(update['psbt'], unsafe_sign_all=True)['signed_psbt'] l1.rpc.openchannel_signed(chan_id, signed_psbt) bitcoind.generate_block(1) @@ -638,7 +638,7 @@ def test_v2_rbf_liquidity_ad(node_factory, bitcoind, chainparams): assert update['commitments_secured'] # Sign our inputs, and continue - signed_psbt = l1.rpc.signpsbt(update['psbt'])['signed_psbt'] + signed_psbt = l1.rpc.signpsbt(update['psbt'], unsafe_sign_all=True)['signed_psbt'] l1.rpc.openchannel_signed(chan_id, signed_psbt) # There's data in the datastore now (l2 only) @@ -731,7 +731,7 @@ def test_v2_rbf_multi(node_factory, bitcoind, chainparams): assert update['commitments_secured'] # Sign our inputs, and continue - signed_psbt = l1.rpc.signpsbt(update['psbt'])['signed_psbt'] + signed_psbt = l1.rpc.signpsbt(update['psbt'], unsafe_sign_all=True)['signed_psbt'] l1.rpc.openchannel_signed(chan_id, signed_psbt) # We 2x the feerate to beat the min-relay fee @@ -754,7 +754,7 @@ def test_v2_rbf_multi(node_factory, bitcoind, chainparams): assert update['commitments_secured'] # Sign our inputs, and continue - signed_psbt = l1.rpc.signpsbt(update['psbt'])['signed_psbt'] + signed_psbt = l1.rpc.signpsbt(update['psbt'], unsafe_sign_all=True)['signed_psbt'] l1.rpc.openchannel_signed(chan_id, signed_psbt) bitcoind.generate_block(1) @@ -969,7 +969,7 @@ def test_rbf_reconnect_tx_construct(node_factory, bitcoind, chainparams): # We can call update again! It should short-circuit this time :) update = l1.rpc.openchannel_update(chan_id, bump['psbt']) assert update['commitments_secured'] - signed_psbt = l1.rpc.signpsbt(update['psbt'])['signed_psbt'] + signed_psbt = l1.rpc.signpsbt(update['psbt'], unsafe_sign_all=True)['signed_psbt'] l1.rpc.openchannel_signed(chan_id, signed_psbt) l2.daemon.wait_for_log('Broadcasting funding tx') @@ -1038,7 +1038,7 @@ def test_rbf_reconnect_tx_sigs(node_factory, bitcoind, chainparams): update = l1.rpc.openchannel_update(chan_id, bump['psbt']) # Sign our inputs, and continue - signed_psbt = l1.rpc.signpsbt(update['psbt'])['signed_psbt'] + signed_psbt = l1.rpc.signpsbt(update['psbt'], unsafe_sign_all=True)['signed_psbt'] # First time we error when we send our sigs with pytest.raises(RpcError): @@ -1210,7 +1210,7 @@ def run_retry(): update = l1.rpc.openchannel_update(chan_id, bump['psbt']) assert update['commitments_secured'] - return l1.rpc.signpsbt(update['psbt'])['signed_psbt'] + return l1.rpc.signpsbt(update['psbt'], unsafe_sign_all=True)['signed_psbt'] signed_psbt = run_retry() l1.rpc.openchannel_signed(chan_id, signed_psbt) @@ -1295,7 +1295,7 @@ def run_retry(): update = l1.rpc.openchannel_update(chan_id, bump['psbt']) assert update['commitments_secured'] - return l1.rpc.signpsbt(update['psbt'])['signed_psbt'] + return l1.rpc.signpsbt(update['psbt'], unsafe_sign_all=True)['signed_psbt'] signed_psbt = run_retry() l1.rpc.openchannel_signed(chan_id, signed_psbt) @@ -1363,7 +1363,7 @@ def run_retry(): update = l1.rpc.openchannel_update(chan_id, bump['psbt']) assert update['commitments_secured'] - return l1.rpc.signpsbt(update['psbt'])['signed_psbt'] + return l1.rpc.signpsbt(update['psbt'], unsafe_sign_all=True)['signed_psbt'] # Make a second inflight signed_psbt = run_retry() diff --git a/tests/test_splicing.py b/tests/test_splicing.py index 424b49c00f4b..7f0f7ce4b5ed 100644 --- a/tests/test_splicing.py +++ b/tests/test_splicing.py @@ -23,7 +23,7 @@ def test_splice(node_factory, bitcoind): result = l1.rpc.splice_init(chan_id, 100000, funds_result['psbt']) result = l1.rpc.splice_update(chan_id, result['psbt']) - result = l1.rpc.signpsbt(result['psbt']) + result = l1.rpc.signpsbt(result['psbt'], utxo_string=funds_result['utxo_string']) result = l1.rpc.splice_signed(chan_id, result['signed_psbt']) l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') @@ -61,7 +61,7 @@ def test_splice_gossip(node_factory, bitcoind): result = l1.rpc.splice_init(chan_id, 100000, funds_result['psbt']) result = l1.rpc.splice_update(chan_id, result['psbt']) - result = l1.rpc.signpsbt(result['psbt']) + result = l1.rpc.signpsbt(result['psbt'], utxo_string=funds_result['utxo_string']) result = l1.rpc.splice_signed(chan_id, result['signed_psbt']) wait_for(lambda: only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels'])['state'] == 'CHANNELD_AWAITING_SPLICE') @@ -119,7 +119,7 @@ def test_splice_listnodes(node_factory, bitcoind): result = l1.rpc.splice_init(chan_id, 100000, funds_result['psbt']) result = l1.rpc.splice_update(chan_id, result['psbt']) - result = l1.rpc.signpsbt(result['psbt']) + result = l1.rpc.signpsbt(result['psbt'], utxo_string=funds_result['utxo_string']) result = l1.rpc.splice_signed(chan_id, result['signed_psbt']) l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') @@ -208,7 +208,7 @@ def test_invalid_splice(node_factory, bitcoind): result = l1.rpc.splice_init(chan_id, 100000, funds_result['psbt']) result = l1.rpc.splice_update(chan_id, result['psbt']) - result = l1.rpc.signpsbt(result['psbt']) + result = l1.rpc.signpsbt(result['psbt'], utxo_string=funds_result['utxo_string']) result = l1.rpc.splice_signed(chan_id, result['signed_psbt']) l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') @@ -307,7 +307,7 @@ def test_splice_stuck_htlc(node_factory, bitcoind, executor): result = l1.rpc.splice_init(chan_id, 100000, funds_result['psbt']) result = l1.rpc.splice_update(chan_id, result['psbt']) - result = l1.rpc.signpsbt(result['psbt']) + result = l1.rpc.signpsbt(result['psbt'], utxo_string=funds_result['utxo_string']) result = l1.rpc.splice_signed(chan_id, result['signed_psbt']) l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 40d078cd338d..06603167cd0d 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -807,7 +807,7 @@ def test_sign_external_psbt(node_factory, bitcoind, chainparams): psbt = bitcoind.rpc.createpsbt(inputs, [{addr: (amount * 3) / 10**8}]) l1.rpc.reserveinputs(psbt) - l1.rpc.signpsbt(psbt) + l1.rpc.signpsbt(psbt, unsafe_sign_all=True) def test_psbt_version(node_factory, bitcoind, chainparams): @@ -913,7 +913,7 @@ def test_sign_and_send_psbt(node_factory, bitcoind, chainparams): # We require the utxos be reserved before signing them with pytest.raises(RpcError, match=r"Aborting PSBT signing. UTXO .* is not reserved"): - l1.rpc.signpsbt(funding['psbt'])['signed_psbt'] + l1.rpc.signpsbt(funding['psbt'], utxo_string=funding['utxo_string'])['signed_psbt'] # Now we unreserve the singleton, so we can reserve it again l1.rpc.unreserveinputs(psbt) @@ -930,7 +930,7 @@ def test_sign_and_send_psbt(node_factory, bitcoind, chainparams): l1.rpc.reserveinputs(fullpsbt) # Sign + send the PSBT we've created - signed_psbt = l1.rpc.signpsbt(fullpsbt)['signed_psbt'] + signed_psbt = l1.rpc.signpsbt(fullpsbt, utxo_string=funding['utxo_string'])['signed_psbt'] broadcast_tx = l1.rpc.sendpsbt(signed_psbt) # Check that it was broadcast successfully @@ -947,7 +947,7 @@ def test_sign_and_send_psbt(node_factory, bitcoind, chainparams): # Now we try signing a PSBT with an output that's already been spent with pytest.raises(RpcError, match=r"Aborting PSBT signing. UTXO .* is not reserved"): - l1.rpc.signpsbt(fullpsbt) + l1.rpc.signpsbt(fullpsbt, utxo_string=funding['utxo_string']) # Queue up another node, to make some PSBTs for us for i in range(total_outs): @@ -962,7 +962,7 @@ def test_sign_and_send_psbt(node_factory, bitcoind, chainparams): # Try to get L1 to sign it with pytest.raises(RpcError, match=r"No wallet inputs to sign"): - l1.rpc.signpsbt(l2_funding['psbt']) + l1.rpc.signpsbt(l2_funding['psbt'], utxo_string=funding['utxo_string']) # With signonly it will fail if it can't sign it. with pytest.raises(RpcError, match=r"is unknown"): @@ -1007,7 +1007,7 @@ def test_sign_and_send_psbt(node_factory, bitcoind, chainparams): 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'] + totally_signed = l2.rpc.signpsbt(half_signed_psbt, unsafe_sign_all=True)['signed_psbt'] broadcast_tx = l1.rpc.sendpsbt(totally_signed) l1.daemon.wait_for_log(r'sendrawtx exit 0 .* sendrawtransaction {}'.format(broadcast_tx['tx'])) @@ -1020,7 +1020,7 @@ def test_sign_and_send_psbt(node_factory, bitcoind, chainparams): output_psbt = bitcoind.rpc.createpsbt([], [{addr: float((out_total + out_amt).to_btc())}]) psbt = bitcoind.rpc.joinpsbts([l2_funding['psbt'], output_psbt]) - l2_signed_psbt = l2.rpc.signpsbt(psbt)['signed_psbt'] + l2_signed_psbt = l2.rpc.signpsbt(psbt, unsafe_sign_all=True)['signed_psbt'] l1.rpc.sendpsbt(l2_signed_psbt) # Re-try sending the same tx? diff --git a/wallet/reservation.c b/wallet/reservation.c index fe092fc916cc..524c0ff08615 100644 --- a/wallet/reservation.c +++ b/wallet/reservation.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,7 @@ static void reserve_and_report(struct json_stream *response, u32 reserve, struct utxo **utxos) { + char *utxo_string = tal_strdup(NULL, ""); json_array_start(response, "reservations"); for (size_t i = 0; i < tal_count(utxos); i++) { enum output_status oldstatus; @@ -75,8 +77,15 @@ static void reserve_and_report(struct json_stream *response, } json_add_reservestatus(response, utxos[i], oldstatus, old_res, current_height); + tal_append_fmt(&utxo_string, "%s%s", + type_to_string(utxo_string, + struct bitcoin_outpoint, + &utxos[i]->outpoint), + strlen(utxo_string) ? "," : ""); } json_array_end(response); + json_add_string(response, "utxo_string", utxo_string); + tal_free(utxo_string); } static struct command_result *json_reserveinputs(struct command *cmd, diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c index 71723b3599e3..53ce4372f859 100644 --- a/wallet/walletrpc.c +++ b/wallet/walletrpc.c @@ -740,14 +740,63 @@ static struct command_result *json_signpsbt(struct command *cmd, struct wally_psbt *psbt, *signed_psbt; struct utxo **utxos; u32 *input_nums; + const char *utxo_string; u32 psbt_version; + bool *sign_all; if (!param_check(cmd, buffer, params, p_req("psbt", param_psbt, &psbt), p_opt("signonly", param_input_numbers, &input_nums), + p_opt("utxo_string", param_string, &utxo_string), + p_opt_def("unsafe_sign_all", param_bool, &sign_all, false), NULL)) return command_param_failed(); + if (!input_nums && !utxo_string && !sign_all) + return command_fail(cmd, LIGHTNINGD, "Must specify which inputs" + " to sign with signonly or utxo_string"); + + if (input_nums && utxo_string) + return command_fail(cmd, LIGHTNINGD, "Must specify which inputs" + " to sign with either signonly or" + " utxo_string, not both."); + + if (sign_all) { + input_nums = tal_free(input_nums); + utxo_string = tal_free(utxo_string); + } + + if (utxo_string) + input_nums = tal_arr(tmpctx, u32, 0); + + while (utxo_string && strlen(utxo_string)) { + struct bitcoin_outpoint usr_outpoint; + + if (strlen(utxo_string) < 66) + return command_fail(cmd, LIGHTNINGD, "utxo_string encoding"); + + if (!bitcoin_txid_from_hex(utxo_string, 64, &usr_outpoint.txid)) + return command_fail(cmd, LIGHTNINGD, "utxo_string encoding"); + + if (utxo_string[64] != ':') + return command_fail(cmd, LIGHTNINGD, "utxo_string encoding"); + + errno = 0; + usr_outpoint.n = strtoul(utxo_string + 65, NULL, 10); + + if (errno != 0) + return command_fail(cmd, LIGHTNINGD, "utxo_string encoding"); + + utxo_string = strchr(utxo_string, ','); + + for (size_t i = 0; i < psbt->num_inputs; i++) { + struct bitcoin_outpoint outpoint; + wally_psbt_input_get_outpoint(&psbt->inputs[i], &outpoint); + if (bitcoin_outpoint_eq(&outpoint, &usr_outpoint)) + tal_arr_expand(&input_nums, i); + } + } + /* We internally deal with v2 only but we want to return V2 if given */ psbt_version = psbt->version; if (!psbt_set_version(psbt, 2)) { @@ -829,7 +878,10 @@ static const struct json_command signpsbt_command = { "signpsbt", "bitcoin", json_signpsbt, - "Sign this wallet's inputs on a provided PSBT.", + "Sign this wallet's inputs specified by {signonly} or {utxo_string} on" + " a provided {psbt}. {signonly} is an array of input indices to sign" + " while {utxo_string} is in the format returned by fundpsbt" + " \"txid:ounum,txid:outnum\".", false };