mirror of
https://github.com/aljazceru/lightning.git
synced 2025-12-19 23:24:27 +01:00
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
This commit is contained in:
committed by
Christian Decker
parent
fd8a716695
commit
9830c94778
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user