diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index abba57f23..43692c5a5 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -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, diff --git a/tests/test_wallet.py b/tests/test_wallet.py index a07ec8e06..f12c3abbd 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -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)