pytest: test reserve and unreserve.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell 2020-07-15 15:03:49 +09:30
parent be17a9392f
commit 4ee527a59c
2 changed files with 35 additions and 116 deletions

View File

@ -1107,22 +1107,19 @@ class LightningRpc(UnixDomainSocketRpc):
}
return self.call("txsend", payload)
def reserveinputs(self, outputs, feerate=None, minconf=None, utxos=None):
def reserveinputs(self, psbt, exclusive=True):
"""
Reserve UTXOs and return a psbt for a 'stub' transaction that
spends the reserved UTXOs.
Reserve any inputs in this psbt.
"""
payload = {
"outputs": outputs,
"feerate": feerate,
"minconf": minconf,
"utxos": utxos,
"psbt": psbt,
"exclusive": exclusive,
}
return self.call("reserveinputs", payload)
def unreserveinputs(self, psbt):
"""
Unreserve UTXOs that were previously reserved.
Unreserve (or reduce reservation) on any UTXOs in this psbt were previously reserved.
"""
payload = {
"psbt": psbt,

View File

@ -438,130 +438,52 @@ def test_txprepare(node_factory, bitcoind, chainparams):
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']
outputs = []
# 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)
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']) == 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 not any(o['reserved'] for o in l1.rpc.listfunds()['outputs'])
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]
# Try reserving one at a time.
for out in outputs:
psbt = bitcoind.rpc.createpsbt([{'txid': out[0], 'vout': out[1]}], [])
l1.rpc.reserveinputs(psbt)
# We should have two outputs
for vout in psbt['tx']['vout']:
if chainparams['elements'] and vout['scriptPubKey']['type'] == 'fee':
continue
if vout['scriptPubKey']['addresses'][0] == addr:
assert vout['value'] == sent
out_found = True
assert out_found
assert all(o['reserved'] for o in l1.rpc.listfunds()['outputs'])
# 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)}])
# Unreserve as a batch.
psbt = bitcoind.rpc.createpsbt([{'txid': out[0], 'vout': out[1]} for out in outputs], [])
l1.rpc.unreserveinputs(psbt)
assert not any(o['reserved'] for o in l1.rpc.listfunds()['outputs'])
utxo_count -= 1
sent = Decimal('0.01') * (utxo_count - 1)
reserved = l1.rpc.reserveinputs(outputs=[{addr: Millisatoshi(amount * (utxo_count - 1) * 1000)}], feerate='10000perkw')
# Reserve twice fails unless exclusive.
l1.rpc.reserveinputs(psbt)
with pytest.raises(RpcError, match=r"already reserved"):
l1.rpc.reserveinputs(psbt)
l1.rpc.reserveinputs(psbt, False)
l1.rpc.unreserveinputs(psbt)
assert all(o['reserved'] for o in l1.rpc.listfunds()['outputs'])
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 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'])):
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 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
# 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
# Stays reserved across restarts.
l1.restart()
assert len(l1.rpc.listfunds()['outputs']) == 12
assert all(o['reserved'] for o in l1.rpc.listfunds()['outputs'])
# Final unreserve works.
l1.rpc.unreserveinputs(psbt)
assert not any(o['reserved'] for o in l1.rpc.listfunds()['outputs'])
@pytest.mark.xfail(strict=True)