This commit is contained in:
Jonathan Moody 2023-04-05 04:07:55 +02:00 committed by GitHub
commit 5f16fa505f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 68 additions and 24 deletions
lbry/wallet
tests
integration/claims
unit/wallet

View file

@ -808,25 +808,34 @@ class Transaction:
tx.get_base_fee(ledger) +
tx.get_total_output_sum(ledger)
)
cost_of_change = (
tx.get_base_fee(ledger) +
Output.pay_pubkey_hash(COIN, NULL_HASH32).get_fee(ledger)
)
# value of the inputs less the cost to spend those inputs
payment = tx.get_effective_input_sum(ledger)
try:
for _ in range(5):
for i in range(2):
if payment < cost:
if payment < cost or (i > 0 and not tx._outputs):
deficit = cost - payment
# this condition and the outer range(2) loop cover an edge case
# whereby a single input is just enough to cover the fee and
# has some change left over, but the change left over is less
# than the cost_of_change: thus the input is completely
# consumed and no output is added, which is an invalid tx.
# to be able to spend this input we must increase the cost
# in order to make a change output > DUST.
if i > 0 and not tx._outputs:
deficit += (cost_of_change + DUST + 1)
spendables = await ledger.get_spendable_utxos(deficit, funding_accounts)
if not spendables:
raise InsufficientFundsError()
payment += sum(s.effective_amount for s in spendables)
tx.add_inputs(s.txi for s in spendables)
cost_of_change = (
tx.get_base_fee(ledger) +
Output.pay_pubkey_hash(COIN, NULL_HASH32).get_fee(ledger)
)
if payment > cost:
change = payment - cost
change_amount = change - cost_of_change
@ -839,17 +848,11 @@ class Transaction:
if tx._outputs:
break
# this condition and the outer range(5) loop cover an edge case
# whereby a single input is just enough to cover the fee and
# has some change left over, but the change left over is less
# than the cost_of_change: thus the input is completely
# consumed and no output is added, which is an invalid tx.
# to be able to spend this input we must increase the cost
# of the TX and run through the balance algorithm a second time
# adding an extra input and change output, making tx valid.
# we do this 5 times in case the other UTXOs added are also
# less than the fee, after 5 attempts we give up and go home
cost += cost_of_change + 1
# We need to run through the balance algorithm a second time
# adding extra inputs and change output, making tx valid.
if not tx._outputs:
raise InsufficientFundsError()
if sign:
await tx.sign(funding_accounts)

View file

@ -2119,7 +2119,7 @@ class StreamCommands(ClaimTestCase):
tx = await self.stream_create(bid='0.0001')
await self.assertBalance(self.account, '9.979793')
await self.stream_abandon(self.get_claim_id(tx))
await self.assertBalance(self.account, '9.97968399')
await self.assertBalance(self.account, '9.979712')
async def test_publish(self):

View file

@ -5,11 +5,11 @@ import shutil
from binascii import hexlify, unhexlify
from itertools import cycle
from lbry.error import InsufficientFundsError
from lbry.testcase import AsyncioTestCase
from lbry.wallet.constants import CENT, COIN, NULL_HASH32
from lbry.wallet.constants import CENT, COIN, DUST, NULL_HASH32
from lbry.wallet import Wallet, Account, Ledger, Database, Headers, Transaction, Output, Input
NULL_HASH = b'\x00'*32
FEE_PER_BYTE = 50
FEE_PER_CHAR = 200000
@ -395,12 +395,12 @@ class TransactionIOBalancing(AsyncioTestCase):
return utxos
@staticmethod
def inputs(tx):
return [round(i.amount/COIN, 2) for i in tx.inputs]
def inputs(tx, precision=2):
return [round(i.amount/COIN, precision) for i in tx.inputs]
@staticmethod
def outputs(tx):
return [round(o.amount/COIN, 2) for o in tx.outputs]
def outputs(tx, precision=2):
return [round(o.amount/COIN, precision) for o in tx.outputs]
async def test_basic_use_cases(self):
self.ledger.fee_per_byte = int(.01*CENT)
@ -532,3 +532,44 @@ class TransactionIOBalancing(AsyncioTestCase):
self.assertListEqual([0.01, 1], self.inputs(tx))
# change is now needed to consume extra input
self.assertListEqual([0.97], self.outputs(tx))
async def test_liquidate_at_loss(self):
#self.ledger.coin_selection_strategy = 'sqlite'
self.ledger.fee_per_byte = int(0.01*CENT)
# Create UTXOs with values large enough that they can be spent.
utxos = await self.create_utxos([a/COIN for a in range(1490*DUST, 1510*DUST, int(DUST/10))])
tx = await self.tx(
[self.txi(self.txo(0.01))], # inputs
[] # outputs
)
# A very tiny amount of change is generated as the only output.
self.assertListEqual([1100], [o.amount for o in tx.outputs])
# A large number of additional UTXOs are added to cover fees.
self.assertListEqual([
1000000, 1509900, 1509800, 1509700, 1509600, 1509500, 1509400, 1509300, 1509200, 1509100,
1509000, 1508900, 1508800, 1508700, 1508600, 1508500, 1508400, 1508300, 1508200, 1508100,
1494600, 1494400, 1508000, 1507900, 1507800, 1507700, 1507600, 1507500, 1507400, 1507300,
1507200, 1507100, 1507000, 1506900, 1506800, 1506700, 1506600, 1506500, 1506400, 1506300,
1506200, 1505200, 1501000],
[i.amount for i in tx.inputs])
self.assertIn(tx.size, range(6350, 6430))
self.assertEqual(64300000, tx.fee)
await self.ledger.release_outputs(utxos)
async def test_liquidate_at_loss_tiny_utxos(self):
#self.ledger.coin_selection_strategy = 'sqlite'
self.ledger.fee_per_byte = int(0.01*CENT)
# Create UTXOs with values so tiny that they cannot be spent.
utxos = await self.create_utxos([a/COIN for a in range(1460*DUST, 1490*DUST, int(DUST/10))])
with self.assertRaises(InsufficientFundsError):
tx = await self.tx(
[self.txi(self.txo(0.01))], # inputs
[] # outputs
)
self.assertFalse([i.amount for i in tx.inputs])