chia-blockchain/tests/wallet/test_coin_selection.py
Jack Nelson ecada58d36
Coin Selection Refactor With CAT Coin Selection Refactor (#9975)
* add exact match and best exact match algorithms

* optimize algorithm further

this might be good.

* lint

* fix bad logic

* add final algorithms

* delete lint

* oops

* Update coin_selection.py

* simplify and fix knapsack algoritm

* simplify code and correct logic

* make it way better.

* clarify comments and check for edge cases.

* add comments and stuff

* improve coin selection

addressed comments
Thanks!

* add coin_selection rpc tests.

* clean up and add new unit tests

* undo test changes

* add extra test cases

* move coin_selection to its own function and switch to it for cat and main wallet.

* add cat tests

* lint

* make function align with standards

also removed test

* make test better

* add proper types

* Improve code clarity

* wallet: fix coin selection bugs

* wallet: add an assert just in case

* tests: add some sleeps to reduce flakiness

* Isort

Co-authored-by: Kyle Altendorf <sda@fstab.net>

* fix bad merge

* lint

* fix tests

* address aforementioned changes.

* remove wallet test

* isort

* more tests and fixes

* lint

* rename to amount for coin selection rpc

* fix incase we have no smaller coins

* fix tests + lint

* re add asserts

* oops

missed me.

* lint

* fix test

* Squashed commit of the following:

commit 34a2235de5
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Wed Apr 13 10:09:42 2022 -0400

    clarify comment

commit adbf7f4f94
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Tue Apr 12 20:27:05 2022 -0400

    linty lint

commit 5ebc1ac9fd
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Tue Apr 12 20:17:19 2022 -0400

    add failure test and final changes

commit 7e5a21b4c2
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Tue Apr 12 19:35:18 2022 -0400

    add descriptions and slim down code

commit 31c95b916d
Merge: d7b91295b d9b0ef5f3
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Mon Apr 11 10:12:05 2022 -0400

    Merge branch 'jack-cat-coinselection' into jn_coinselection_dust

commit d7b91295b5
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Sun Apr 10 20:31:09 2022 -0400

    lint

commit 30dc7c0ab4
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Sun Apr 10 20:25:52 2022 -0400

    fix tests

commit 6c8c2e4874
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Thu Mar 31 15:06:00 2022 -0400

    remove duplicate code.

commit 9f79b6f304
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Thu Mar 31 15:01:10 2022 -0400

    address more concerns

commit 67c1b3929f
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Thu Mar 31 12:59:05 2022 -0400

    fix logic error

commit 2d19a53245
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Thu Mar 31 11:47:52 2022 -0400

    simplify and de duplicate code

commit 6ab1cc79bb
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Wed Mar 30 21:34:50 2022 -0400

    add function and select individual coin

commit 582c17aa8d
Merge: ce2165942 618fbaeba
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Wed Mar 30 21:14:37 2022 -0400

    Merge branch 'jack-cat-coinselection' into jn_coinselection_dust

commit ce21659429
Merge: 16aabb3fd 6daba28db
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Wed Mar 30 20:53:21 2022 -0400

    Merge branch 'jack-cat-coinselection' into jn_coinselection_dust

commit 16aabb3fd5
Merge: 0b9fc2845 2286fe426
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Wed Mar 30 20:49:02 2022 -0400

    Merge branch 'jack-cat-coinselection' into jn_coinselection_dust

commit 0b9fc28455
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Wed Mar 30 20:38:12 2022 -0400

    lint

commit 62e74c72f4
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Wed Mar 30 20:34:22 2022 -0400

    fix logic and tests

commit e738f44320
Author: Jack Nelson <jack@jacknelson.xyz>
Date:   Wed Mar 30 18:52:05 2022 -0400

    deal with dust and add tests

* make sure that we do not use any dust

* minor change

* address concerns

* adjust comments

* adjust comment

Co-authored-by: Mariano Sorgente <sorgente711@gmail.com>
Co-authored-by: Kyle Altendorf <sda@fstab.net>
2022-05-16 10:42:46 -07:00

291 lines
12 KiB
Python

import logging
from random import randrange
from typing import List, Set
import pytest
from chia.consensus.default_constants import DEFAULT_CONSTANTS
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.hash import std_hash
from chia.util.ints import uint32, uint64, uint128
from chia.wallet.coin_selection import check_for_exact_match, knapsack_coin_algorithm, select_coins
from chia.wallet.util.wallet_types import WalletType
from chia.wallet.wallet_coin_record import WalletCoinRecord
class TestCoinSelection:
@pytest.fixture(scope="function")
def a_hash(self) -> bytes32:
return std_hash(b"a")
def test_exact_match(self, a_hash: bytes32) -> None:
coin_list = [
Coin(a_hash, a_hash, uint64(220000)),
Coin(a_hash, a_hash, uint64(120000)),
Coin(a_hash, a_hash, uint64(22)),
]
assert check_for_exact_match(coin_list, uint64(220000)) == coin_list[0]
assert check_for_exact_match(coin_list, uint64(22)) == coin_list[2]
# check for no match.
assert check_for_exact_match(coin_list, uint64(20)) is None
def test_knapsack_coin_selection(self, a_hash: bytes32) -> None:
tries = 100
coins_to_append = 1000
amounts = list(range(1, coins_to_append))
amounts.sort(reverse=True)
coin_list: List[Coin] = [Coin(a_hash, a_hash, uint64(100000000 * a)) for a in amounts]
for i in range(tries):
knapsack = knapsack_coin_algorithm(coin_list, uint128(30000000000000), DEFAULT_CONSTANTS.MAX_COIN_AMOUNT)
assert knapsack is not None
assert sum([coin.amount for coin in knapsack]) >= 310000000
def test_knapsack_coin_selection_2(self, a_hash: bytes32) -> None:
coin_amounts = [6, 20, 40, 80, 150, 160, 203, 202, 201, 320]
coin_amounts.sort(reverse=True)
coin_list: List[Coin] = [Coin(a_hash, a_hash, uint64(a)) for a in coin_amounts]
# coin_list = set([coin for a in coin_amounts])
for i in range(100):
knapsack = knapsack_coin_algorithm(coin_list, uint128(265), DEFAULT_CONSTANTS.MAX_COIN_AMOUNT)
assert knapsack is not None
selected_sum = sum(coin.amount for coin in list(knapsack))
assert 265 <= selected_sum <= 280 # Selects a set of coins which does exceed by too much
@pytest.mark.asyncio
async def test_coin_selection_randomly(self, a_hash: bytes32) -> None:
coin_base_amounts = [3, 6, 20, 40, 80, 150, 160, 203, 202, 201, 320]
coin_amounts = []
spendable_amount = 0
# this is possibly overkill, but it's a good test.
for i in range(3000):
for amount in coin_base_amounts:
c_amount = randrange(1, 10000000) * amount
coin_amounts.append(c_amount)
spendable_amount += c_amount
spendable_amount = uint128(spendable_amount)
coin_list: List[WalletCoinRecord] = [
WalletCoinRecord(Coin(a_hash, a_hash, uint64(a)), uint32(1), uint32(1), False, True, WalletType(0), 1)
for a in coin_amounts
]
for target_amount in coin_amounts[:100]: # select the first 100 values
result: Set[Coin] = await select_coins(
spendable_amount,
DEFAULT_CONSTANTS.MAX_COIN_AMOUNT,
coin_list,
{},
logging.getLogger("test"),
uint128(target_amount),
)
assert result is not None
assert sum([coin.amount for coin in result]) >= target_amount
assert len(result) <= 500
@pytest.mark.asyncio
async def test_coin_selection_with_dust(self, a_hash: bytes32) -> None:
spendable_amount = uint128(5000000000000 + 10000)
coin_list: List[WalletCoinRecord] = [
WalletCoinRecord(
Coin(a_hash, a_hash, uint64(5000000000000)), uint32(1), uint32(1), False, True, WalletType(0), 1
)
]
for i in range(10000):
coin_list.append(
WalletCoinRecord(
Coin(a_hash, std_hash(i), uint64(1)), uint32(1), uint32(1), False, True, WalletType(0), 1
)
)
# make sure coins are not identical.
for target_amount in [10000, 9999]:
result: Set[Coin] = await select_coins(
spendable_amount,
DEFAULT_CONSTANTS.MAX_COIN_AMOUNT,
coin_list,
{},
logging.getLogger("test"),
uint128(target_amount),
)
assert result is not None
assert sum([coin.amount for coin in result]) >= target_amount
assert len(result) == 1 # only one coin should be selected
for i in range(100):
coin_list.append(
WalletCoinRecord(
Coin(a_hash, std_hash(i), uint64(2000)), uint32(1), uint32(1), False, True, WalletType(0), 1
)
)
spendable_amount = uint128(spendable_amount + 2000 * 100)
for target_amount in [50000, 25000, 15000, 10000, 9000, 3000]: # select the first 100 values
dusty_result: Set[Coin] = await select_coins(
spendable_amount,
DEFAULT_CONSTANTS.MAX_COIN_AMOUNT,
coin_list,
{},
logging.getLogger("test"),
uint128(target_amount),
)
assert dusty_result is not None
assert sum([coin.amount for coin in dusty_result]) >= target_amount
for coin in dusty_result:
assert coin.amount > 1
assert len(dusty_result) <= 500
@pytest.mark.asyncio
async def test_coin_selection_failure(self, a_hash: bytes32) -> None:
spendable_amount = uint128(10000)
coin_list: List[WalletCoinRecord] = []
for i in range(10000):
coin_list.append(
WalletCoinRecord(
Coin(a_hash, std_hash(i), uint64(1)), uint32(1), uint32(1), False, True, WalletType(0), 1
)
)
# make sure coins are not identical.
# test for failure
with pytest.raises(ValueError):
for target_amount in [10000, 9999]:
await select_coins(
spendable_amount,
DEFAULT_CONSTANTS.MAX_COIN_AMOUNT,
coin_list,
{},
logging.getLogger("test"),
uint128(target_amount),
)
# test not enough coin failure.
with pytest.raises(ValueError):
for target_amount in [10001, 20000]:
await select_coins(
spendable_amount,
DEFAULT_CONSTANTS.MAX_COIN_AMOUNT,
coin_list,
{},
logging.getLogger("test"),
uint128(target_amount),
)
@pytest.mark.asyncio
async def test_coin_selection(self, a_hash: bytes32) -> None:
coin_amounts = [3, 6, 20, 40, 80, 150, 160, 203, 202, 201, 320]
coin_list: List[WalletCoinRecord] = [
WalletCoinRecord(Coin(a_hash, a_hash, uint64(a)), uint32(1), uint32(1), False, True, WalletType(0), 1)
for a in coin_amounts
]
spendable_amount = uint128(sum(coin_amounts))
# check for exact match
target_amount = uint128(40)
exact_match_result: Set[Coin] = await select_coins(
spendable_amount,
DEFAULT_CONSTANTS.MAX_COIN_AMOUNT,
coin_list,
{},
logging.getLogger("test"),
target_amount,
)
assert exact_match_result is not None
assert sum([coin.amount for coin in exact_match_result]) >= target_amount
assert len(exact_match_result) == 1
# check for match of 2
target_amount = uint128(153)
match_2: Set[Coin] = await select_coins(
spendable_amount,
DEFAULT_CONSTANTS.MAX_COIN_AMOUNT,
coin_list,
{},
logging.getLogger("test"),
target_amount,
)
assert match_2 is not None
assert sum([coin.amount for coin in match_2]) == target_amount
assert len(match_2) == 2
# check for match of at least 3. it is random after all.
target_amount = uint128(541)
match_3: Set[Coin] = await select_coins(
spendable_amount,
DEFAULT_CONSTANTS.MAX_COIN_AMOUNT,
coin_list,
{},
logging.getLogger("test"),
target_amount,
)
assert match_3 is not None
assert sum([coin.amount for coin in match_3]) >= target_amount
assert len(match_3) >= 3
# check for match of all
target_amount = spendable_amount
match_all: Set[Coin] = await select_coins(
spendable_amount,
DEFAULT_CONSTANTS.MAX_COIN_AMOUNT,
coin_list,
{},
logging.getLogger("test"),
target_amount,
)
assert match_all is not None
assert sum([coin.amount for coin in match_all]) == target_amount
assert len(match_all) == len(coin_list)
# test smallest greater than target
greater_coin_amounts = [1, 2, 5, 20, 400, 700]
greater_coin_list: List[WalletCoinRecord] = [
WalletCoinRecord(Coin(a_hash, a_hash, uint64(a)), uint32(1), uint32(1), False, True, WalletType(0), 1)
for a in greater_coin_amounts
]
greater_spendable_amount = uint128(sum(greater_coin_amounts))
target_amount = uint128(625)
smallest_result: Set[Coin] = await select_coins(
greater_spendable_amount,
DEFAULT_CONSTANTS.MAX_COIN_AMOUNT,
greater_coin_list,
{},
logging.getLogger("test"),
target_amount,
)
assert smallest_result is not None
assert sum([coin.amount for coin in smallest_result]) > target_amount
assert len(smallest_result) == 1
# test smallest greater than target with only 1 large coin.
single_greater_coin_list: List[WalletCoinRecord] = [
WalletCoinRecord(Coin(a_hash, a_hash, uint64(70000)), uint32(1), uint32(1), False, True, WalletType(0), 1)
]
single_greater_spendable_amount = uint128(70000)
target_amount = uint128(50000)
single_greater_result: Set[Coin] = await select_coins(
single_greater_spendable_amount,
DEFAULT_CONSTANTS.MAX_COIN_AMOUNT,
single_greater_coin_list,
{},
logging.getLogger("test"),
target_amount,
)
assert single_greater_result is not None
assert sum([coin.amount for coin in single_greater_result]) > target_amount
assert len(single_greater_result) == 1
# test smallest greater than target with only multiple larger then target coins.
multiple_greater_coin_amounts = [90000, 100000, 120000, 200000, 100000]
multiple_greater_coin_list: List[WalletCoinRecord] = [
WalletCoinRecord(Coin(a_hash, a_hash, uint64(a)), uint32(1), uint32(1), False, True, WalletType(0), 1)
for a in multiple_greater_coin_amounts
]
multiple_greater_spendable_amount = uint128(sum(multiple_greater_coin_amounts))
target_amount = uint128(70000)
multiple_greater_result: Set[Coin] = await select_coins(
multiple_greater_spendable_amount,
DEFAULT_CONSTANTS.MAX_COIN_AMOUNT,
multiple_greater_coin_list,
{},
logging.getLogger("test"),
target_amount,
)
assert multiple_greater_result is not None
assert sum([coin.amount for coin in multiple_greater_result]) > target_amount
assert sum([coin.amount for coin in multiple_greater_result]) == 90000
assert len(multiple_greater_result) == 1