Skip to content

Commit

Permalink
signpsbt: add signonly parameter to restrict/enforce what inputs to s…
Browse files Browse the repository at this point in the history
…ign.

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

Signed-off-by: Rusty Russell <[email protected]>
Changelog-Added: JSON-RPC: `signpsbt` takes an optional `signonly` array to limit what inputs to sign.
  • Loading branch information
rustyrussell committed Aug 18, 2020
1 parent cf1f505 commit e01da0b
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 6 deletions.
3 changes: 2 additions & 1 deletion contrib/pyln-client/pyln/client/lightning.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,12 +1155,13 @@ def utxopsbt(self, satoshi, feerate, startweight, utxos, reserve=True, reservedo
}
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)

Expand Down
29 changes: 27 additions & 2 deletions tests/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
61 changes: 58 additions & 3 deletions wallet/walletrpc.c
Original file line number Diff line number Diff line change
Expand Up @@ -1221,20 +1221,39 @@ 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);
for (size_t i = 0; i < psbt->tx->num_inputs; i++) {
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)))
Expand All @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -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;

Expand Down

0 comments on commit e01da0b

Please sign in to comment.