lbry-sdk/tests/unit/wallet/test_coinselection.py

193 lines
6.8 KiB
Python
Raw Permalink Normal View History

2018-06-14 02:57:57 +02:00
from types import GeneratorType
2018-05-25 08:03:25 +02:00
2019-12-31 21:30:13 +01:00
from lbry.testcase import AsyncioTestCase
2018-10-15 06:45:21 +02:00
2020-01-03 04:18:49 +01:00
from lbry.wallet import Ledger, Database, Headers
2020-01-03 07:55:19 +01:00
from lbry.wallet.coinselection import CoinSelector, MAXIMUM_TRIES
2020-01-03 04:18:49 +01:00
from lbry.constants import CENT
2018-05-25 08:03:25 +02:00
from tests.unit.wallet.test_transaction import get_output as utxo
2018-05-25 08:03:25 +02:00
NULL_HASH = b'\x00'*32
def search(*args, **kwargs):
selection = CoinSelector(*args[1:], **kwargs).select(args[0], 'branch_and_bound')
2018-06-14 02:57:57 +02:00
return [o.txo.amount for o in selection] if selection else selection
2018-05-25 08:03:25 +02:00
2018-10-15 06:45:21 +02:00
class BaseSelectionTestCase(AsyncioTestCase):
2018-05-25 08:03:25 +02:00
2018-10-15 06:45:21 +02:00
async def asyncSetUp(self):
2020-01-03 04:18:49 +01:00
self.ledger = Ledger({
'db': Database(':memory:'),
'headers': Headers(':memory:'),
2018-08-16 07:21:45 +02:00
})
2018-10-15 06:45:21 +02:00
await self.ledger.db.open()
async def asyncTearDown(self):
await self.ledger.db.close()
2018-05-25 08:03:25 +02:00
def estimates(self, *args):
2018-06-14 02:57:57 +02:00
txos = args[0] if isinstance(args[0], (GeneratorType, list)) else args
2018-06-11 15:33:32 +02:00
return [txo.get_estimator(self.ledger) for txo in txos]
2018-05-25 08:03:25 +02:00
class TestCoinSelectionTests(BaseSelectionTestCase):
def test_empty_coins(self):
self.assertListEqual(CoinSelector(0, 0).select([]), [])
2018-05-25 08:03:25 +02:00
def test_skip_binary_search_if_total_not_enough(self):
2018-06-11 15:33:32 +02:00
fee = utxo(CENT).get_estimator(self.ledger).fee
2018-05-25 08:03:25 +02:00
big_pool = self.estimates(utxo(CENT+fee) for _ in range(100))
selector = CoinSelector(101 * CENT, 0)
self.assertListEqual(selector.select(big_pool), [])
2018-05-25 08:03:25 +02:00
self.assertEqual(selector.tries, 0) # Never tried.
# check happy path
selector = CoinSelector(100 * CENT, 0)
self.assertEqual(len(selector.select(big_pool)), 100)
2018-05-25 08:03:25 +02:00
self.assertEqual(selector.tries, 201)
def test_exact_match(self):
2018-06-14 02:57:57 +02:00
fee = utxo(CENT).get_estimator(self.ledger).fee
2018-05-25 08:03:25 +02:00
utxo_pool = self.estimates(
utxo(CENT + fee),
utxo(CENT),
utxo(CENT - fee)
)
selector = CoinSelector(CENT, 0)
match = selector.select(utxo_pool)
self.assertListEqual([CENT + fee], [c.txo.amount for c in match])
2018-05-25 08:03:25 +02:00
self.assertTrue(selector.exact_match)
def test_random_draw(self):
utxo_pool = self.estimates(
utxo(2 * CENT),
utxo(3 * CENT),
utxo(4 * CENT)
)
selector = CoinSelector(CENT, 0, '\x00')
match = selector.select(utxo_pool)
self.assertListEqual([2 * CENT], [c.txo.amount for c in match])
2018-05-25 08:03:25 +02:00
self.assertFalse(selector.exact_match)
def test_pick(self):
utxo_pool = self.estimates(
2019-10-08 01:53:46 +02:00
utxo(1*CENT),
utxo(1*CENT),
utxo(3*CENT),
utxo(5*CENT),
utxo(10*CENT),
)
2019-10-08 01:53:46 +02:00
selector = CoinSelector(3*CENT, 0)
match = selector.select(utxo_pool)
2019-10-08 01:53:46 +02:00
self.assertListEqual([5*CENT], [c.txo.amount for c in match])
def test_confirmed_strategies(self):
2019-06-18 00:29:17 +02:00
utxo_pool = self.estimates(
utxo(11*CENT, height=5),
utxo(11*CENT, height=0),
utxo(11*CENT, height=-2),
utxo(11*CENT, height=5),
)
2019-10-08 01:53:46 +02:00
match = CoinSelector(20*CENT, 0).select(utxo_pool, "only_confirmed")
self.assertListEqual([5, 5], [c.txo.tx_ref.height for c in match])
2019-10-08 01:53:46 +02:00
match = CoinSelector(25*CENT, 0).select(utxo_pool, "only_confirmed")
self.assertListEqual([], [c.txo.tx_ref.height for c in match])
2019-10-08 01:53:46 +02:00
match = CoinSelector(20*CENT, 0).select(utxo_pool, "prefer_confirmed")
self.assertListEqual([5, 5], [c.txo.tx_ref.height for c in match])
2019-10-08 01:53:46 +02:00
match = CoinSelector(25*CENT, 0, '\x00').select(utxo_pool, "prefer_confirmed")
self.assertListEqual([5, 0, -2], [c.txo.tx_ref.height for c in match])
2019-06-19 12:00:22 +02:00
2018-05-25 08:03:25 +02:00
class TestOfficialBitcoinCoinSelectionTests(BaseSelectionTestCase):
# Bitcoin implementation:
# https://github.com/bitcoin/bitcoin/blob/master/src/wallet/coinselection.cpp
#
# Bitcoin implementation tests:
# https://github.com/bitcoin/bitcoin/blob/master/src/wallet/test/coinselector_tests.cpp
#
# Branch and Bound coin selection white paper:
# https://murch.one/wp-content/uploads/2016/11/erhardt2016coinselection.pdf
def make_hard_case(self, utxos):
target = 0
utxo_pool = []
for i in range(utxos):
amount = 1 << (utxos+i)
target += amount
utxo_pool.append(utxo(amount))
utxo_pool.append(utxo(amount + (1 << (utxos-1-i))))
return self.estimates(utxo_pool), target
def test_branch_and_bound_coin_selection(self):
2018-06-14 02:57:57 +02:00
self.ledger.fee_per_byte = 0
2018-05-25 08:03:25 +02:00
utxo_pool = self.estimates(
utxo(1 * CENT),
utxo(2 * CENT),
utxo(3 * CENT),
utxo(4 * CENT)
)
# Select 1 Cent
self.assertListEqual([1 * CENT], search(utxo_pool, 1 * CENT, 0.5 * CENT))
2018-05-25 08:03:25 +02:00
# Select 2 Cent
self.assertListEqual([2 * CENT], search(utxo_pool, 2 * CENT, 0.5 * CENT))
2018-05-25 08:03:25 +02:00
# Select 5 Cent
self.assertListEqual([3 * CENT, 2 * CENT], search(utxo_pool, 5 * CENT, 0.5 * CENT))
2018-05-25 08:03:25 +02:00
# Select 11 Cent, not possible
self.assertListEqual([], search(utxo_pool, 11 * CENT, 0.5 * CENT))
2018-05-25 08:03:25 +02:00
# Select 10 Cent
utxo_pool += self.estimates(utxo(5 * CENT))
self.assertListEqual(
2018-05-25 08:03:25 +02:00
[4 * CENT, 3 * CENT, 2 * CENT, 1 * CENT],
search(utxo_pool, 10 * CENT, 0.5 * CENT)
)
# Negative effective value
# Select 10 Cent but have 1 Cent not be possible because too small
# TODO: bitcoin has [5, 3, 2]
self.assertListEqual(
2018-05-25 08:03:25 +02:00
[4 * CENT, 3 * CENT, 2 * CENT, 1 * CENT],
search(utxo_pool, 10 * CENT, 5000)
)
# Select 0.25 Cent, not possible
self.assertListEqual(search(utxo_pool, 0.25 * CENT, 0.5 * CENT), [])
2018-05-25 08:03:25 +02:00
# Iteration exhaustion test
utxo_pool, target = self.make_hard_case(17)
selector = CoinSelector(target, 0)
self.assertListEqual(selector.select(utxo_pool, 'branch_and_bound'), [])
2018-05-25 08:03:25 +02:00
self.assertEqual(selector.tries, MAXIMUM_TRIES) # Should exhaust
utxo_pool, target = self.make_hard_case(14)
self.assertIsNotNone(search(utxo_pool, target, 0)) # Should not exhaust
# Test same value early bailout optimization
utxo_pool = self.estimates([
utxo(7 * CENT),
utxo(7 * CENT),
utxo(7 * CENT),
utxo(7 * CENT),
utxo(2 * CENT)
] + [utxo(5 * CENT)]*50000)
self.assertListEqual(
2018-05-25 08:03:25 +02:00
[7 * CENT, 7 * CENT, 7 * CENT, 7 * CENT, 2 * CENT],
search(utxo_pool, 30 * CENT, 5000)
)
# Select 1 Cent with pool of only greater than 5 Cent
utxo_pool = self.estimates(utxo(i * CENT) for i in range(5, 21))
for _ in range(100):
self.assertListEqual(search(utxo_pool, 1 * CENT, 2 * CENT), [])