Browse Source

reserve/unreserve input: new RPC commands for reserving inputs/outputs

Reserve and unreserve wallet UTXOs using a PSBT which includes those
inputs.

Note that currently we unreserve inputs everytime the node restarts.
This will be addressed in a future commit.

Changelog-Added: JSON-RPC: Adds two new rpc methods, `reserveinputs` and `unreserveinputs`, which allow for reserving or unreserving wallet UTXOs
paymod-02
niftynei 4 years ago
committed by Christian Decker
parent
commit
103dce63ef
  1. 22
      contrib/pyln-client/pyln/client/lightning.py
  2. 125
      tests/test_wallet.py
  3. 14
      wallet/wallet.c
  4. 14
      wallet/wallet.h
  5. 100
      wallet/walletrpc.c

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

@ -1107,6 +1107,28 @@ class LightningRpc(UnixDomainSocketRpc):
}
return self.call("txsend", payload)
def reserveinputs(self, outputs, feerate=None, minconf=None, utxos=None):
"""
Reserve UTXOs and return a psbt for a 'stub' transaction that
spends the reserved UTXOs.
"""
payload = {
"outputs": outputs,
"feerate": feerate,
"minconf": minconf,
"utxos": utxos,
}
return self.call("reserveinputs", payload)
def unreserveinputs(self, psbt):
"""
Unreserve UTXOs that were previously reserved.
"""
payload = {
"psbt": psbt,
}
return self.call("unreserveinputs", payload)
def signmessage(self, message):
"""
Sign a message with this node's secret key.

125
tests/test_wallet.py

@ -436,6 +436,131 @@ def test_txprepare(node_factory, bitcoind, chainparams):
assert decode['vout'][changenum]['scriptPubKey']['type'] == 'witness_v0_keyhash'
def test_reserveinputs(node_factory, bitcoind, chainparams):
"""
Reserve inputs is basically the same as txprepare, with the
slight exception that 'reserveinputs' doesn't keep the
unsent transaction around
"""
amount = 1000000
total_outs = 12
l1 = node_factory.get_node(feerates=(7500, 7500, 7500, 7500))
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)
utxo_count = 8
sent = Decimal('0.01') * (utxo_count - 1)
reserved = l1.rpc.reserveinputs(outputs=[{addr: Millisatoshi(amount * (utxo_count - 1) * 1000)}])
assert reserved['feerate_per_kw'] == 7500
psbt = bitcoind.rpc.decodepsbt(reserved['psbt'])
out_found = False
assert len(psbt['inputs']) == utxo_count
outputs = l1.rpc.listfunds()['outputs']
assert len([x for x in outputs if not x['reserved']]) == total_outs - utxo_count
assert len([x for x in outputs if x['reserved']]) == utxo_count
total_outs -= utxo_count
saved_input = psbt['tx']['vin'][0]
# We should have two outputs
for vout in psbt['tx']['vout']:
if vout['scriptPubKey']['addresses'][0] == addr:
assert vout['value'] == sent
out_found = True
assert out_found
# Do it again, but for too many inputs
utxo_count = 12 - utxo_count + 1
sent = Decimal('0.01') * (utxo_count - 1)
with pytest.raises(RpcError, match=r"Cannot afford transaction"):
reserved = l1.rpc.reserveinputs(outputs=[{addr: Millisatoshi(amount * (utxo_count - 1) * 1000)}])
utxo_count -= 1
sent = Decimal('0.01') * (utxo_count - 1)
reserved = l1.rpc.reserveinputs(outputs=[{addr: Millisatoshi(amount * (utxo_count - 1) * 1000)}], feerate='10000perkw')
assert reserved['feerate_per_kw'] == 10000
psbt = bitcoind.rpc.decodepsbt(reserved['psbt'])
assert len(psbt['inputs']) == utxo_count
outputs = l1.rpc.listfunds()['outputs']
assert len([x for x in outputs if not x['reserved']]) == total_outs - utxo_count == 0
assert len([x for x in outputs if x['reserved']]) == 12
# No more available
with pytest.raises(RpcError, match=r"Cannot afford transaction"):
reserved = l1.rpc.reserveinputs(outputs=[{addr: Millisatoshi(amount * 1)}], feerate='253perkw')
# Unreserve three, from different psbts
unreserve_utxos = [
{
'txid': saved_input['txid'],
'vout': saved_input['vout'],
'sequence': saved_input['sequence']
}, {
'txid': psbt['tx']['vin'][0]['txid'],
'vout': psbt['tx']['vin'][0]['vout'],
'sequence': psbt['tx']['vin'][0]['sequence']
}, {
'txid': psbt['tx']['vin'][1]['txid'],
'vout': psbt['tx']['vin'][1]['vout'],
'sequence': psbt['tx']['vin'][1]['sequence']
}]
unreserve_psbt = bitcoind.rpc.createpsbt(unreserve_utxos, [])
unreserved = l1.rpc.unreserveinputs(unreserve_psbt)
assert unreserved['all_unreserved']
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'])):
un = unreserved['outputs'][i]
u_utxo = unreserve_utxos[i]
assert un['txid'] == u_utxo['txid'] and un['vout'] == u_utxo['vout'] and un['unreserved']
# Try unreserving the same utxos again, plus one that's not included
# We expect this to be a no-op.
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']
for un in unreserved['outputs']:
assert not un['unreserved']
assert len([x for x in l1.rpc.listfunds()['outputs'] if not x['reserved']]) == 3
# passing in an empty string should fail
with pytest.raises(RpcError, match=r"should be a PSBT, not "):
l1.rpc.unreserveinputs('')
# reserve one of the utxos that we just unreserved
utxos = []
utxos.append(saved_input['txid'] + ":" + str(saved_input['vout']))
reserved = l1.rpc.reserveinputs([{addr: Millisatoshi(amount * 0.5 * 1000)}], feerate='253perkw', utxos=utxos)
assert len([x for x in l1.rpc.listfunds()['outputs'] if not x['reserved']]) == 2
psbt = bitcoind.rpc.decodepsbt(reserved['psbt'])
assert len(psbt['inputs']) == 1
vin = psbt['tx']['vin'][0]
assert vin['txid'] == saved_input['txid'] and vin['vout'] == saved_input['vout']
# reserve them all!
reserved = l1.rpc.reserveinputs([{addr: 'all'}])
outputs = l1.rpc.listfunds()['outputs']
assert len([x for x in outputs if not x['reserved']]) == 0
assert len([x for x in outputs if x['reserved']]) == 12
# FIXME: restart the node, nothing will remain reserved
l1.restart()
assert len(l1.rpc.listfunds()['outputs']) == 12
def test_txsend(node_factory, bitcoind, chainparams):
amount = 1000000
l1 = node_factory.get_node(random_hsm=True)

14
wallet/wallet.c

@ -355,6 +355,15 @@ struct utxo *wallet_utxo_get(const tal_t *ctx, struct wallet *w,
return utxo;
}
bool wallet_unreserve_output(struct wallet *w,
const struct bitcoin_txid *txid,
const u32 outnum)
{
return wallet_update_output_status(w, txid, outnum,
output_state_reserved,
output_state_available);
}
/**
* unreserve_utxo - Mark a reserved UTXO as available again
*/
@ -376,6 +385,11 @@ static void destroy_utxos(const struct utxo **utxos, struct wallet *w)
unreserve_utxo(w, utxos[i]);
}
void wallet_persist_utxo_reservation(struct wallet *w, const struct utxo **utxos)
{
tal_del_destructor2(utxos, destroy_utxos, w);
}
void wallet_confirm_utxos(struct wallet *w, const struct utxo **utxos)
{
tal_del_destructor2(utxos, destroy_utxos, w);

14
wallet/wallet.h

@ -1246,6 +1246,20 @@ void add_unreleased_tx(struct wallet *w, struct unreleased_tx *utx);
/* These will touch the db, so need to be explicitly freed. */
void free_unreleased_txs(struct wallet *w);
/* wallet_persist_utxo_reservation - Removes destructor
*
* Persists the reservation in the database (until a restart)
* instead of clearing the reservation when the utxo object
* is destroyed */
void wallet_persist_utxo_reservation(struct wallet *w, const struct utxo **utxos);
/* wallet_unreserve_output - Unreserve a utxo
*
* We unreserve utxos so that they can be spent elsewhere.
* */
bool wallet_unreserve_output(struct wallet *w,
const struct bitcoin_txid *txid,
const u32 outnum);
/**
* Get a list of transactions that we track in the wallet.
*

100
wallet/walletrpc.c

@ -1158,3 +1158,103 @@ static const struct json_command listtransactions_command = {
"it closes the channel and returns funds to the wallet."
};
AUTODATA(json_command, &listtransactions_command);
static struct command_result *json_reserveinputs(struct command *cmd,
const char *buffer,
const jsmntok_t *obj UNNEEDED,
const jsmntok_t *params)
{
struct command_result *res;
struct json_stream *response;
struct unreleased_tx *utx;
u32 feerate;
res = json_prepare_tx(cmd, buffer, params, false, &utx, &feerate);
if (res)
return res;
/* Unlike json_txprepare, we don't keep the utx object
* around, so we remove the auto-cleanup that happens
* when the utxo objects are free'd */
wallet_persist_utxo_reservation(cmd->ld->wallet, utx->wtx->utxos);
response = json_stream_success(cmd);
json_add_psbt(response, "psbt", utx->tx->psbt);
json_add_u32(response, "feerate_per_kw", feerate);
return command_success(cmd, response);
}
static const struct json_command reserveinputs_command = {
"reserveinputs",
"bitcoin",
json_reserveinputs,
"Reserve inputs and pass back the resulting psbt",
false
};
AUTODATA(json_command, &reserveinputs_command);
static struct command_result *param_psbt(struct command *cmd,
const char *name,
const char *buffer,
const jsmntok_t *tok,
struct wally_psbt **psbt)
{
/* Pull out the token into a string, then pass to
* the PSBT parser; PSBT parser can't handle streaming
* atm as it doesn't accept a len value */
char *psbt_buff = json_strdup(cmd, buffer, tok);
if (psbt_from_b64(psbt_buff, psbt))
return NULL;
return command_fail(cmd, LIGHTNINGD, "'%s' should be a PSBT, not '%.*s'",
name, json_tok_full_len(tok),
json_tok_full(buffer, tok));
}
static struct command_result *json_unreserveinputs(struct command *cmd,
const char *buffer,
const jsmntok_t *obj UNNEEDED,
const jsmntok_t *params)
{
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,
p_req("psbt", param_psbt, &psbt),
NULL))
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;
struct bitcoin_txid txid;
bool unreserved;
in = &psbt->tx->inputs[i];
wally_tx_input_get_txid(in, &txid);
unreserved = wallet_unreserve_output(cmd->ld->wallet,
&txid, in->index);
json_object_start(response, NULL);
json_add_txid(response, "txid", &txid);
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 = {
"unreserveinputs",
"bitcoin",
json_unreserveinputs,
"Unreserve inputs, freeing them up to be reused",
false
};
AUTODATA(json_command, &unreserveinputs_command);

Loading…
Cancel
Save