Browse Source

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.
bump-pyln-proto
Rusty Russell 5 years ago
committed by neil saitug
parent
commit
7435d50970
  1. 3
      contrib/pyln-client/pyln/client/lightning.py
  2. 29
      tests/test_wallet.py
  3. 61
      wallet/walletrpc.c

3
contrib/pyln-client/pyln/client/lightning.py

@ -1155,12 +1155,13 @@ class LightningRpc(UnixDomainSocketRpc):
} }
return self.call("utxopsbt", payload) return self.call("utxopsbt", payload)
def signpsbt(self, psbt): def signpsbt(self, psbt, signonly=None):
""" """
Add internal wallet's signatures to PSBT Add internal wallet's signatures to PSBT
""" """
payload = { payload = {
"psbt": psbt, "psbt": psbt,
"signonly": signonly,
} }
return self.call("signpsbt", payload) return self.call("signpsbt", payload)

29
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"): with pytest.raises(RpcError, match=r"No wallet inputs to sign"):
l1.rpc.signpsbt(l2_funding['psbt']) 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 # Add some of our own PSBT inputs to it
l1_funding = l1.rpc.fundpsbt(satoshi=Millisatoshi(3 * amount * 1000), l1_funding = l1.rpc.fundpsbt(satoshi=Millisatoshi(3 * amount * 1000),
feerate=7500, feerate=7500,
startweight=42, startweight=42,
reserve=True) 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'], joint_psbt = bitcoind.rpc.joinpsbts([l1_funding['psbt'], l2_funding['psbt'],
output_pbst]) 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'] totally_signed = l2.rpc.signpsbt(half_signed_psbt)['signed_psbt']
broadcast_tx = l1.rpc.sendpsbt(totally_signed) broadcast_tx = l1.rpc.sendpsbt(totally_signed)

61
wallet/walletrpc.c

@ -1221,8 +1221,17 @@ struct command_result *param_psbt(struct command *cmd,
json_tok_full(buffer, tok)); 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, static struct command_result *match_psbt_inputs_to_utxos(struct command *cmd,
struct wally_psbt *psbt, struct wally_psbt *psbt,
const u32 *only_inputs,
struct utxo ***utxos) struct utxo ***utxos)
{ {
*utxos = tal_arr(cmd, struct utxo *, 0); *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 utxo *utxo;
struct bitcoin_txid txid; struct bitcoin_txid txid;
if (only_inputs && !in_only_inputs(only_inputs, i))
continue;
wally_tx_input_get_txid(&psbt->tx->inputs[i], &txid); wally_tx_input_get_txid(&psbt->tx->inputs[i], &txid);
utxo = wallet_utxo_get(*utxos, cmd->ld->wallet, utxo = wallet_utxo_get(*utxos, cmd->ld->wallet,
&txid, psbt->tx->inputs[i].index); &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; continue;
}
/* Oops we haven't reserved this utxo yet! */ /* Oops we haven't reserved this utxo yet! */
if (!is_reserved(utxo, get_block_height(cmd->ld->topology))) 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; 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, static struct command_result *json_signpsbt(struct command *cmd,
const char *buffer, const char *buffer,
const jsmntok_t *obj UNNEEDED, const jsmntok_t *obj UNNEEDED,
@ -1258,17 +1303,27 @@ static struct command_result *json_signpsbt(struct command *cmd,
struct json_stream *response; struct json_stream *response;
struct wally_psbt *psbt, *signed_psbt; struct wally_psbt *psbt, *signed_psbt;
struct utxo **utxos; struct utxo **utxos;
u32 *input_nums;
if (!param(cmd, buffer, params, if (!param(cmd, buffer, params,
p_req("psbt", param_psbt, &psbt), p_req("psbt", param_psbt, &psbt),
p_opt("signonly", param_input_numbers, &input_nums),
NULL)) NULL))
return command_param_failed(); 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, /* 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 * 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. * our utxos require more complicated data to sign for e.g.
* closeinfo outputs */ * closeinfo outputs */
res = match_psbt_inputs_to_utxos(cmd, psbt, &utxos); res = match_psbt_inputs_to_utxos(cmd, psbt, input_nums, &utxos);
if (res) if (res)
return 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, /* We have to find/locate the utxos that are ours on this PSBT,
* so that we know who to mark as used. * 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) if (res)
return res; return res;

Loading…
Cancel
Save