From 9830c947787dc613051712e80cd705fd4bd31d2c Mon Sep 17 00:00:00 2001 From: niftynei Date: Tue, 16 Jun 2020 13:47:07 -0500 Subject: [PATCH] rpc: new signpsbt + sendpsbt rpcs Changelog-Added: JSON-RPC: new call `signpsbt` which will add the wallet's signatures to a provided psbt Changelog-Added: JSON-RPC: new call `sendpsbt` which will finalize and send a signed PSBT --- contrib/pyln-client/pyln/client/lightning.py | 18 ++ tests/test_wallet.py | 121 +++++++++++- wallet/walletrpc.c | 192 +++++++++++++++++-- 3 files changed, 316 insertions(+), 15 deletions(-) diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index f374eafbc..abba57f23 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -1129,6 +1129,24 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("unreserveinputs", payload) + def signpsbt(self, psbt): + """ + Add internal wallet's signatures to PSBT + """ + payload = { + "psbt": psbt, + } + return self.call("signpsbt", payload) + + def sendpsbt(self, psbt): + """ + Finalize extract and broadcast a PSBT + """ + payload = { + "psbt": psbt, + } + return self.call("sendpsbt", payload) + def signmessage(self, message): """ Sign a message with this node's secret key. diff --git a/tests/test_wallet.py b/tests/test_wallet.py index a60677a64..56e9bc1ac 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -1,3 +1,4 @@ +from bitcoin.rpc import JSONRPCError from decimal import Decimal from fixtures import * # noqa: F401,F403 from fixtures import TEST_NETWORK @@ -518,7 +519,7 @@ def test_reserveinputs(node_factory, bitcoind, chainparams): unreserve_psbt = bitcoind.rpc.createpsbt(unreserve_utxos, []) unreserved = l1.rpc.unreserveinputs(unreserve_psbt) - assert unreserved['all_unreserved'] + assert all([x['unreserved'] for x in unreserved['outputs']]) outputs = l1.rpc.listfunds()['outputs'] assert len([x for x in outputs if not x['reserved']]) == len(unreserved['outputs']) for i in range(len(unreserved['outputs'])): @@ -531,7 +532,7 @@ def test_reserveinputs(node_factory, bitcoind, chainparams): unreserve_utxos.append({'txid': 'b' * 64, 'vout': 0, 'sequence': 0}) unreserve_psbt = bitcoind.rpc.createpsbt(unreserve_utxos, []) unreserved = l1.rpc.unreserveinputs(unreserve_psbt) - assert not unreserved['all_unreserved'] + assert not any([x['unreserved'] for x in unreserved['outputs']]) for un in unreserved['outputs']: assert not un['unreserved'] assert len([x for x in l1.rpc.listfunds()['outputs'] if not x['reserved']]) == 3 @@ -561,6 +562,122 @@ def test_reserveinputs(node_factory, bitcoind, chainparams): assert len(l1.rpc.listfunds()['outputs']) == 12 +def test_sign_and_send_psbt(node_factory, bitcoind, chainparams): + """ + Tests for the sign + send psbt RPCs + """ + amount = 1000000 + total_outs = 12 + l1 = node_factory.get_node(feerates=(7500, 7500, 7500, 7500)) + l2 = node_factory.get_node() + addr = chainparams['example_addr'] + + # Add a medley of funds to withdraw later, bech32 + p2sh-p2wpkh + for i in range(total_outs // 2): + bitcoind.rpc.sendtoaddress(l1.rpc.newaddr()['bech32'], + amount / 10**8) + bitcoind.rpc.sendtoaddress(l1.rpc.newaddr('p2sh-segwit')['p2sh-segwit'], + amount / 10**8) + bitcoind.generate_block(1) + wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == total_outs) + + # Make a PSBT out of our inputs + reserved = l1.rpc.reserveinputs(outputs=[{addr: Millisatoshi(3 * amount * 1000)}]) + assert len([x for x in l1.rpc.listfunds()['outputs'] if x['reserved']]) == 4 + psbt = bitcoind.rpc.decodepsbt(reserved['psbt']) + saved_input = psbt['tx']['vin'][0] + + # Go ahead and unreserve the UTXOs, we'll use a smaller + # set of them to create a second PSBT that we'll attempt to sign + # and broadcast (to disastrous results) + l1.rpc.unreserveinputs(reserved['psbt']) + + # Re-reserve one of the utxos we just unreserved + utxos = [] + utxos.append(saved_input['txid'] + ":" + str(saved_input['vout'])) + second_reservation = l1.rpc.reserveinputs([{addr: Millisatoshi(amount * 0.5 * 1000)}], feerate='253perkw', utxos=utxos) + + # 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(reserved['psbt'])['signed_psbt'] + + # Now we unreserve the singleton, so we can reserve it again + l1.rpc.unreserveinputs(second_reservation['psbt']) + + # We re-reserve the first set... + utxos = [] + for vin in psbt['tx']['vin']: + utxos.append(vin['txid'] + ':' + str(vin['vout'])) + reserved = l1.rpc.reserveinputs(outputs=[{addr: Millisatoshi(3 * amount * 1000)}], utxos=utxos) + # Sign + send the PSBT we've created + signed_psbt = l1.rpc.signpsbt(reserved['psbt'])['signed_psbt'] + broadcast_tx = l1.rpc.sendpsbt(signed_psbt) + + # Check that it was broadcast successfully + l1.daemon.wait_for_log(r'sendrawtx exit 0 .* sendrawtransaction {}'.format(broadcast_tx['tx'])) + bitcoind.generate_block(1) + + # We expect a change output to be added to the wallet + expected_outs = total_outs - 4 + 1 + wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == expected_outs) + + # Let's try *sending* a PSBT that can't be finalized (it's unsigned) + with pytest.raises(RpcError, match=r"PSBT not finalizeable"): + l1.rpc.sendpsbt(second_reservation['psbt']) + + # 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".format(utxos[0])): + l1.rpc.signpsbt(second_reservation['psbt']) + + # Queue up another node, to make some PSBTs for us + for i in range(total_outs // 2): + bitcoind.rpc.sendtoaddress(l2.rpc.newaddr()['bech32'], + amount / 10**8) + bitcoind.rpc.sendtoaddress(l2.rpc.newaddr('p2sh-segwit')['p2sh-segwit'], + amount / 10**8) + # Create a PSBT using L2 + bitcoind.generate_block(1) + wait_for(lambda: len(l2.rpc.listfunds()['outputs']) == total_outs) + l2_reserved = l2.rpc.reserveinputs(outputs=[{addr: Millisatoshi(3 * amount * 1000)}]) + + # Try to get L1 to sign it + with pytest.raises(RpcError, match=r"No wallet inputs to sign"): + l1.rpc.signpsbt(l2_reserved['psbt']) + + # Add some of our own PSBT inputs to it + l1_reserved = l1.rpc.reserveinputs(outputs=[{addr: Millisatoshi(3 * amount * 1000)}]) + joint_psbt = bitcoind.rpc.joinpsbts([l1_reserved['psbt'], l2_reserved['psbt']]) + + half_signed_psbt = l1.rpc.signpsbt(joint_psbt)['signed_psbt'] + totally_signed = l2.rpc.signpsbt(half_signed_psbt)['signed_psbt'] + + broadcast_tx = l1.rpc.sendpsbt(totally_signed) + l1.daemon.wait_for_log(r'sendrawtx exit 0 .* sendrawtransaction {}'.format(broadcast_tx['tx'])) + + # Send a PSBT that's not ours + l2_reserved = l2.rpc.reserveinputs(outputs=[{addr: Millisatoshi(3 * amount * 1000)}]) + l2_signed_psbt = l2.rpc.signpsbt(l2_reserved['psbt'])['signed_psbt'] + l1.rpc.sendpsbt(l2_signed_psbt) + + # Re-try sending the same tx? + bitcoind.generate_block(1) + sync_blockheight(bitcoind, [l1]) + # Expect an error here + with pytest.raises(JSONRPCError, match=r"Transaction already in block chain"): + bitcoind.rpc.sendrawtransaction(broadcast_tx['tx']) + + # Try an empty PSBT + with pytest.raises(RpcError, match=r"should be a PSBT, not"): + l1.rpc.signpsbt('') + with pytest.raises(RpcError, match=r"should be a PSBT, not"): + l1.rpc.sendpsbt('') + + # Try a modified (invalid) PSBT string + modded_psbt = l2_reserved['psbt'][:-3] + 'A' + l2_reserved['psbt'][-3:] + with pytest.raises(RpcError, match=r"should be a PSBT, not"): + l1.rpc.signpsbt(modded_psbt) + + def test_txsend(node_factory, bitcoind, chainparams): amount = 1000000 l1 = node_factory.get_node(random_hsm=True) diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c index a6fc4180c..ff40dd9c7 100644 --- a/wallet/walletrpc.c +++ b/wallet/walletrpc.c @@ -34,6 +34,28 @@ #include #include +struct tx_broadcast { + struct command *cmd; + const struct utxo **utxos; + const struct wally_tx *wtx; + struct amount_sat *expected_change; +}; + +static struct tx_broadcast *unreleased_tx_to_broadcast(const tal_t *ctx, + struct command *cmd, + struct unreleased_tx *utx) +{ + struct tx_broadcast *txb = tal(ctx, struct tx_broadcast); + struct amount_sat *change = tal(txb, struct amount_sat); + + txb->cmd = cmd; + txb->utxos = utx->wtx->utxos; + txb->wtx = utx->tx->wtx; + *change = utx->wtx->change; + txb->expected_change = change; + return txb; +} + /** * wallet_withdrawal_broadcast - The tx has been broadcast (or it failed) * @@ -45,29 +67,34 @@ */ static void wallet_withdrawal_broadcast(struct bitcoind *bitcoind UNUSED, bool success, const char *msg, - struct unreleased_tx *utx) + struct tx_broadcast *txb) { - struct command *cmd = utx->wtx->cmd; + struct command *cmd = txb->cmd; struct lightningd *ld = cmd->ld; - struct amount_sat change = AMOUNT_SAT(0); /* FIXME: This won't be necessary once we use ccan/json_out! */ /* Massage output into shape so it doesn't kill the JSON serialization */ char *output = tal_strjoin(cmd, tal_strsplit(cmd, msg, "\n", STR_NO_EMPTY), " ", STR_NO_TRAIL); if (success) { + struct bitcoin_txid txid; + struct amount_sat change = AMOUNT_SAT(0); + /* Mark used outputs as spent */ - wallet_confirm_utxos(ld->wallet, utx->wtx->utxos); + wallet_confirm_utxos(ld->wallet, txb->utxos); /* Extract the change output and add it to the DB */ - wallet_extract_owned_outputs(ld->wallet, utx->tx->wtx, NULL, &change); + wallet_extract_owned_outputs(ld->wallet, txb->wtx, NULL, &change); /* Note normally, change_satoshi == withdraw->wtx->change, but * not if we're actually making a payment to ourselves! */ - assert(amount_sat_greater_eq(change, utx->wtx->change)); + if (txb->expected_change) + assert(amount_sat_greater_eq(change, *txb->expected_change)); struct json_stream *response = json_stream_success(cmd); - json_add_tx(response, "tx", utx->tx); - json_add_txid(response, "txid", &utx->txid); + wally_txid(txb->wtx, &txid); + json_add_hex_talarr(response, "tx", + linearize_wtx(tmpctx, txb->wtx)); + json_add_txid(response, "txid", &txid); was_pending(command_success(cmd, response)); } else { was_pending(command_fail(cmd, LIGHTNINGD, @@ -127,7 +154,8 @@ static struct command_result *broadcast_and_wait(struct command *cmd, /* Now broadcast the transaction */ bitcoind_sendrawtx(cmd->ld->topology->bitcoind, tal_hex(tmpctx, linearize_tx(tmpctx, utx->tx)), - wallet_withdrawal_broadcast, utx); + wallet_withdrawal_broadcast, + unreleased_tx_to_broadcast(cmd, cmd, utx)); return command_still_pending(cmd); } @@ -1218,7 +1246,6 @@ static struct command_result *json_unreserveinputs(struct command *cmd, { struct json_stream *response; struct wally_psbt *psbt; - bool all_unreserved; /* for each input in the psbt, attempt to 'unreserve' it */ if (!param(cmd, buffer, params, @@ -1227,7 +1254,6 @@ static struct command_result *json_unreserveinputs(struct command *cmd, return command_param_failed(); response = json_stream_success(cmd); - all_unreserved = psbt->tx->num_inputs != 0; json_array_start(response, "outputs"); for (size_t i = 0; i < psbt->tx->num_inputs; i++) { struct wally_tx_input *in; @@ -1243,11 +1269,9 @@ static struct command_result *json_unreserveinputs(struct command *cmd, json_add_u64(response, "vout", in->index); json_add_bool(response, "unreserved", unreserved); json_object_end(response); - all_unreserved &= unreserved; } json_array_end(response); - json_add_bool(response, "all_unreserved", all_unreserved); return command_success(cmd, response); } static const struct json_command unreserveinputs_command = { @@ -1258,3 +1282,145 @@ static const struct json_command unreserveinputs_command = { false }; AUTODATA(json_command, &unreserveinputs_command); + +static struct command_result *match_psbt_inputs_to_utxos(struct command *cmd, + struct wally_psbt *psbt, + 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; + + 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) + continue; + + /* Oops we haven't reserved this utxo yet. + * Let's just go ahead and reserve it now. */ + if (utxo->status != output_state_reserved) + return command_fail(cmd, LIGHTNINGD, + "Aborting PSBT signing. UTXO %s:%u is not reserved", + type_to_string(tmpctx, struct bitcoin_txid, + &utxo->txid), + utxo->outnum); + tal_arr_expand(utxos, utxo); + } + + return NULL; +} + +static struct command_result *json_signpsbt(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct command_result *res; + struct json_stream *response; + struct wally_psbt *psbt, *signed_psbt; + struct utxo **utxos; + + if (!param(cmd, buffer, params, + p_req("psbt", param_psbt, &psbt), + NULL)) + return command_param_failed(); + + /* 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); + if (res) + return res; + + if (tal_count(utxos) == 0) + return command_fail(cmd, LIGHTNINGD, + "No wallet inputs to sign"); + + /* FIXME: hsm will sign almost anything, but it should really + * fail cleanly (not abort!) and let us report the error here. */ + u8 *msg = towire_hsm_sign_withdrawal(cmd, + cast_const2(const struct utxo **, utxos), + psbt); + + if (!wire_sync_write(cmd->ld->hsm_fd, take(msg))) + fatal("Could not write sign_withdrawal to HSM: %s", + strerror(errno)); + + msg = wire_sync_read(cmd, cmd->ld->hsm_fd); + + if (!fromwire_hsm_sign_withdrawal_reply(cmd, msg, &signed_psbt)) + fatal("HSM gave bad sign_withdrawal_reply %s", + tal_hex(tmpctx, msg)); + + response = json_stream_success(cmd); + json_add_psbt(response, "signed_psbt", signed_psbt); + return command_success(cmd, response); +} + +static const struct json_command signpsbt_command = { + "signpsbt", + "bitcoin", + json_signpsbt, + "Sign this wallet's inputs on a provided PSBT.", + false +}; + +AUTODATA(json_command, &signpsbt_command); + +static struct command_result *json_sendpsbt(struct command *cmd, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) +{ + struct command_result *res; + struct wally_psbt *psbt; + struct wally_tx *w_tx; + struct tx_broadcast *txb; + struct utxo **utxos; + + if (!param(cmd, buffer, params, + p_req("psbt", param_psbt, &psbt), + NULL)) + return command_param_failed(); + + w_tx = psbt_finalize(psbt, true); + if (!w_tx) + return command_fail(cmd, LIGHTNINGD, + "PSBT not finalizeable %s", + type_to_string(tmpctx, struct wally_psbt, + psbt)); + + /* 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); + if (res) + return res; + + txb = tal(cmd, struct tx_broadcast); + txb->utxos = cast_const2(const struct utxo **, + tal_steal(txb, utxos)); + txb->wtx = tal_steal(txb, w_tx); + txb->cmd = cmd; + txb->expected_change = NULL; + + /* Now broadcast the transaction */ + bitcoind_sendrawtx(cmd->ld->topology->bitcoind, + tal_hex(tmpctx, linearize_wtx(tmpctx, w_tx)), + wallet_withdrawal_broadcast, txb); + + return command_still_pending(cmd); +} + +static const struct json_command sendpsbt_command = { + "sendpsbt", + "bitcoin", + json_sendpsbt, + "Finalize, extract and send a PSBT.", + false +}; + +AUTODATA(json_command, &sendpsbt_command);