Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fundpsbt/utxopsbt: let caller specify locktime, signpsbt: signing restrictions. #3954

Merged
merged 3 commits into from
Aug 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions contrib/pyln-client/pyln/client/lightning.py
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,7 @@ def unreserveinputs(self, psbt):
}
return self.call("unreserveinputs", payload)

def fundpsbt(self, satoshi, feerate, startweight, minconf=None, reserve=True):
def fundpsbt(self, satoshi, feerate, startweight, minconf=None, reserve=True, locktime=None):
"""
Create a PSBT with inputs sufficient to give an output of satoshi.
"""
Expand All @@ -1136,15 +1136,32 @@ def fundpsbt(self, satoshi, feerate, startweight, minconf=None, reserve=True):
"startweight": startweight,
"minconf": minconf,
"reserve": reserve,
"locktime": locktime,
}
return self.call("fundpsbt", payload)

def signpsbt(self, psbt):
def utxopsbt(self, satoshi, feerate, startweight, utxos, reserve=True, reservedok=False, locktime=None):
"""
Create a PSBT with given inputs, to give an output of satoshi.
"""
payload = {
"satoshi": satoshi,
"feerate": feerate,
"startweight": startweight,
"utxos": utxos,
"reserve": reserve,
"reservedok": reservedok,
"locktime": locktime,
}
return self.call("utxopsbt", payload)

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
6 changes: 5 additions & 1 deletion doc/lightning-fundpsbt.7

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion doc/lightning-fundpsbt.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ lightning-fundpsbt -- Command to populate PSBT inputs from the wallet
SYNOPSIS
--------

**fundpsbt** *satoshi* *feerate* *startweight* \[*minconf*\] \[*reserve*\]
**fundpsbt** *satoshi* *feerate* *startweight* \[*minconf*\] \[*reserve*\] \[*locktime*\]

DESCRIPTION
-----------
Expand Down Expand Up @@ -36,6 +36,9 @@ outputs should have. Default is 1.
*reserve* is a boolean: if true (the default), then *reserveinputs* is
called (successfully, with *exclusive* true) on the returned PSBT.

*locktime* is an optional locktime: if not set, it is set to a recent
block height.

EXAMPLE USAGE
-------------

Expand Down
6 changes: 5 additions & 1 deletion doc/lightning-utxopsbt.7

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion doc/lightning-utxopsbt.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ lightning-utxopsbt -- Command to populate PSBT inputs from given UTXOs
SYNOPSIS
--------

**utxopsbt** *satoshi* *feerate* *startweight* *utxos* \[*reserve*\] \[*reservedok*\]
**utxopsbt** *satoshi* *feerate* *startweight* *utxos* \[*reserve*\] \[*reservedok*\] \[*locktime*\]

DESCRIPTION
-----------
Expand All @@ -26,6 +26,9 @@ is equivalent to setting it to zero).
Unless *reservedok* is set to true (default is false) it will also fail
if any of the *utxos* are already reserved.

*locktime* is an optional locktime: if not set, it is set to a recent
block height.

RETURN VALUE
------------

Expand Down
123 changes: 119 additions & 4 deletions tests/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,16 +508,20 @@ def test_fundpsbt(node_factory, bitcoind, chainparams):
# Should get one input, plus some excess
funding = l1.rpc.fundpsbt(amount // 2, feerate, 0, reserve=False)
psbt = bitcoind.rpc.decodepsbt(funding['psbt'])
# We can fuzz this up to 99 blocks back.
assert psbt['tx']['locktime'] > bitcoind.rpc.getblockcount() - 100
assert psbt['tx']['locktime'] <= bitcoind.rpc.getblockcount()
assert len(psbt['tx']['vin']) == 1
assert funding['excess_msat'] > Millisatoshi(0)
assert funding['excess_msat'] < Millisatoshi(amount // 2 * 1000)
assert funding['feerate_per_kw'] == 7500
assert 'estimated_final_weight' in funding
assert 'reservations' not in funding

# This should add 99 to the weight, but otherwise be identical (might choose different inputs though!)
funding2 = l1.rpc.fundpsbt(amount // 2, feerate, 99, reserve=False)
# This should add 99 to the weight, but otherwise be identical (might choose different inputs though!) except for locktime.
funding2 = l1.rpc.fundpsbt(amount // 2, feerate, 99, reserve=False, locktime=bitcoind.rpc.getblockcount() + 1)
psbt2 = bitcoind.rpc.decodepsbt(funding2['psbt'])
assert psbt2['tx']['locktime'] == bitcoind.rpc.getblockcount() + 1
assert len(psbt2['tx']['vin']) == 1
assert funding2['excess_msat'] < funding['excess_msat']
assert funding2['feerate_per_kw'] == 7500
Expand Down Expand Up @@ -555,6 +559,92 @@ def test_fundpsbt(node_factory, bitcoind, chainparams):
l1.rpc.fundpsbt(amount // 2, feerate, 0)


def test_utxopsbt(node_factory, bitcoind):
amount = 1000000
l1 = node_factory.get_node()

outputs = []
# Add a medley of funds to withdraw later, bech32 + p2sh-p2wpkh
txid = bitcoind.rpc.sendtoaddress(l1.rpc.newaddr()['bech32'],
amount / 10**8)
outputs.append((txid, bitcoind.rpc.gettransaction(txid)['details'][0]['vout']))
txid = bitcoind.rpc.sendtoaddress(l1.rpc.newaddr('p2sh-segwit')['p2sh-segwit'],
amount / 10**8)
outputs.append((txid, bitcoind.rpc.gettransaction(txid)['details'][0]['vout']))

bitcoind.generate_block(1)
wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == len(outputs))

feerate = '7500perkw'

# Explicitly spend the first output above.
funding = l1.rpc.utxopsbt(amount // 2, feerate, 0,
['{}:{}'.format(outputs[0][0], outputs[0][1])],
reserve=False)
psbt = bitcoind.rpc.decodepsbt(funding['psbt'])
# We can fuzz this up to 99 blocks back.
assert psbt['tx']['locktime'] > bitcoind.rpc.getblockcount() - 100
assert psbt['tx']['locktime'] <= bitcoind.rpc.getblockcount()
assert len(psbt['tx']['vin']) == 1
assert funding['excess_msat'] > Millisatoshi(0)
assert funding['excess_msat'] < Millisatoshi(amount // 2 * 1000)
assert funding['feerate_per_kw'] == 7500
assert 'estimated_final_weight' in funding
assert 'reservations' not in funding

# This should add 99 to the weight, but otherwise be identical except for locktime.
funding2 = l1.rpc.utxopsbt(amount // 2, feerate, 99,
['{}:{}'.format(outputs[0][0], outputs[0][1])],
reserve=False, locktime=bitcoind.rpc.getblockcount() + 1)
psbt2 = bitcoind.rpc.decodepsbt(funding2['psbt'])
assert psbt2['tx']['locktime'] == bitcoind.rpc.getblockcount() + 1
assert psbt2['tx']['vin'] == psbt['tx']['vin']
assert psbt2['tx']['vout'] == psbt['tx']['vout']
assert funding2['excess_msat'] < funding['excess_msat']
assert funding2['feerate_per_kw'] == 7500
assert funding2['estimated_final_weight'] == funding['estimated_final_weight'] + 99
assert 'reservations' not in funding2

# Cannot afford this one (too much)
with pytest.raises(RpcError, match=r"not afford"):
l1.rpc.utxopsbt(amount, feerate, 0,
['{}:{}'.format(outputs[0][0], outputs[0][1])])

# Nor this (even with both)
with pytest.raises(RpcError, match=r"not afford"):
l1.rpc.utxopsbt(amount * 2, feerate, 0,
['{}:{}'.format(outputs[0][0], outputs[0][1]),
'{}:{}'.format(outputs[1][0], outputs[1][1])])

# Should get two inputs (and reserve!)
funding = l1.rpc.utxopsbt(amount, feerate, 0,
['{}:{}'.format(outputs[0][0], outputs[0][1]),
'{}:{}'.format(outputs[1][0], outputs[1][1])])
psbt = bitcoind.rpc.decodepsbt(funding['psbt'])
assert len(psbt['tx']['vin']) == 2
assert len(funding['reservations']) == 2
assert funding['reservations'][0]['txid'] == outputs[0][0]
assert funding['reservations'][0]['vout'] == outputs[0][1]
assert funding['reservations'][0]['was_reserved'] is False
assert funding['reservations'][0]['reserved'] is True
assert funding['reservations'][1]['txid'] == outputs[1][0]
assert funding['reservations'][1]['vout'] == outputs[1][1]
assert funding['reservations'][1]['was_reserved'] is False
assert funding['reservations'][1]['reserved'] is True

# Should refuse to use reserved outputs.
with pytest.raises(RpcError, match=r"already reserved"):
l1.rpc.utxopsbt(amount, feerate, 0,
['{}:{}'.format(outputs[0][0], outputs[0][1]),
'{}:{}'.format(outputs[1][0], outputs[1][1])])

# Unless we tell it that's ok.
l1.rpc.utxopsbt(amount, feerate, 0,
['{}:{}'.format(outputs[0][0], outputs[0][1]),
'{}:{}'.format(outputs[1][0], outputs[1][1])],
reservedok=True)


def test_sign_and_send_psbt(node_factory, bitcoind, chainparams):
"""
Tests for the sign + send psbt RPCs
Expand Down Expand Up @@ -648,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
28 changes: 17 additions & 11 deletions wallet/reservation.c
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,11 @@ static struct command_result *finish_psbt(struct command *cmd,
u32 feerate_per_kw,
size_t weight,
struct amount_sat excess,
bool reserve)
bool reserve,
u32 *locktime)
{
struct json_stream *response;
struct bitcoin_tx *tx;
u32 locktime;
u32 current_height = get_block_height(cmd->ld->topology);

/* Setting the locktime to the next block to be mined has multiple
Expand All @@ -229,18 +229,21 @@ static struct command_result *finish_psbt(struct command *cmd,
* 0xFFFFFFFD by default. Other wallets are likely to implement
* this too).
*/
locktime = current_height;
if (!locktime) {
locktime = tal(cmd, u32);
*locktime = current_height;

/* Eventually fuzz it too. */
if (locktime > 100 && pseudorand(10) == 0)
locktime -= pseudorand(100);
/* Eventually fuzz it too. */
if (*locktime > 100 && pseudorand(10) == 0)
*locktime -= pseudorand(100);
}

/* FIXME: tx_spending_utxos does more than we need, but there
* are other users right now. */
tx = tx_spending_utxos(cmd, chainparams,
cast_const2(const struct utxo **, utxos),
cmd->ld->wallet->bip32_base,
false, 0, locktime,
false, 0, *locktime,
BITCOIN_TX_RBF_SEQUENCE);

response = json_stream_success(cmd);
Expand All @@ -264,14 +267,15 @@ static struct command_result *json_fundpsbt(struct command *cmd,
u32 *minconf, *weight;
struct amount_sat *amount, input, diff;
bool all, *reserve;
u32 maxheight;
u32 *locktime, maxheight;

if (!param(cmd, buffer, params,
p_req("satoshi", param_sat_or_all, &amount),
p_req("feerate", param_feerate, &feerate_per_kw),
p_req("startweight", param_number, &weight),
p_opt_def("minconf", param_number, &minconf, 1),
p_opt_def("reserve", param_bool, &reserve, true),
p_opt("locktime", param_number, &locktime),
NULL))
return command_param_failed();

Expand Down Expand Up @@ -336,7 +340,8 @@ static struct command_result *json_fundpsbt(struct command *cmd,
tal_count(utxos));
}

return finish_psbt(cmd, utxos, *feerate_per_kw, *weight, diff, *reserve);
return finish_psbt(cmd, utxos, *feerate_per_kw, *weight, diff, *reserve,
locktime);
}

static const struct json_command fundpsbt_command = {
Expand Down Expand Up @@ -422,7 +427,7 @@ static struct command_result *json_utxopsbt(struct command *cmd,
u32 *feerate_per_kw, *weight;
bool all, *reserve, *reserved_ok;
struct amount_sat *amount, input, excess;
u32 current_height;
u32 current_height, *locktime;

if (!param(cmd, buffer, params,
p_req("satoshi", param_sat_or_all, &amount),
Expand All @@ -431,6 +436,7 @@ static struct command_result *json_utxopsbt(struct command *cmd,
p_req("utxos", param_txout, &utxos),
p_opt_def("reserve", param_bool, &reserve, true),
p_opt_def("reservedok", param_bool, &reserved_ok, false),
p_opt("locktime", param_number, &locktime),
NULL))
return command_param_failed();

Expand Down Expand Up @@ -474,7 +480,7 @@ static struct command_result *json_utxopsbt(struct command *cmd,
}

return finish_psbt(cmd, utxos, *feerate_per_kw, *weight, excess,
*reserve);
*reserve, locktime);
}
static const struct json_command utxopsbt_command = {
"utxopsbt",
Expand Down
Loading