signpsbt: add signonly parameter to restrict/enforce what inputs to sign.

This is an extra safety check for dual funding, where we only want to sign
the inputs we provided!

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Changelog-Added: JSON-RPC: `signpsbt` takes an optional `signonly` array to limit what inputs to sign.
This commit is contained in:
Rusty Russell 2020-08-18 13:57:04 +09:30 committed by neil saitug
parent aab3808668
commit 7435d50970
3 changed files with 87 additions and 6 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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;