Browse Source

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
paymod-02
niftynei 5 years ago
committed by Christian Decker
parent
commit
9830c94778
  1. 18
      contrib/pyln-client/pyln/client/lightning.py
  2. 121
      tests/test_wallet.py
  3. 192
      wallet/walletrpc.c

18
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.

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

192
wallet/walletrpc.c

@ -34,6 +34,28 @@
#include <wally_bip32.h>
#include <wire/wire_sync.h>
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);

Loading…
Cancel
Save