diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index bb80fb4d3..a88d866ac 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -1155,12 +1155,13 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("utxopsbt", payload) - def signpsbt(self, psbt): + 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/tests/test_wallet.py b/tests/test_wallet.py index 9588cf4f5..d2d0833df 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -738,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/walletrpc.c b/wallet/walletrpc.c index e47e09fcc..b1d18affd 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;