mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2024-09-20 16:08:51 +03:00
ecada58d36
* 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: commit34a2235de5
Author: Jack Nelson <jack@jacknelson.xyz> Date: Wed Apr 13 10:09:42 2022 -0400 clarify comment commitadbf7f4f94
Author: Jack Nelson <jack@jacknelson.xyz> Date: Tue Apr 12 20:27:05 2022 -0400 linty lint commit5ebc1ac9fd
Author: Jack Nelson <jack@jacknelson.xyz> Date: Tue Apr 12 20:17:19 2022 -0400 add failure test and final changes commit7e5a21b4c2
Author: Jack Nelson <jack@jacknelson.xyz> Date: Tue Apr 12 19:35:18 2022 -0400 add descriptions and slim down code commit31c95b916d
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 commitd7b91295b5
Author: Jack Nelson <jack@jacknelson.xyz> Date: Sun Apr 10 20:31:09 2022 -0400 lint commit30dc7c0ab4
Author: Jack Nelson <jack@jacknelson.xyz> Date: Sun Apr 10 20:25:52 2022 -0400 fix tests commit6c8c2e4874
Author: Jack Nelson <jack@jacknelson.xyz> Date: Thu Mar 31 15:06:00 2022 -0400 remove duplicate code. commit9f79b6f304
Author: Jack Nelson <jack@jacknelson.xyz> Date: Thu Mar 31 15:01:10 2022 -0400 address more concerns commit67c1b3929f
Author: Jack Nelson <jack@jacknelson.xyz> Date: Thu Mar 31 12:59:05 2022 -0400 fix logic error commit2d19a53245
Author: Jack Nelson <jack@jacknelson.xyz> Date: Thu Mar 31 11:47:52 2022 -0400 simplify and de duplicate code commit6ab1cc79bb
Author: Jack Nelson <jack@jacknelson.xyz> Date: Wed Mar 30 21:34:50 2022 -0400 add function and select individual coin commit582c17aa8d
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 commitce21659429
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 commit16aabb3fd5
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 commit0b9fc28455
Author: Jack Nelson <jack@jacknelson.xyz> Date: Wed Mar 30 20:38:12 2022 -0400 lint commit62e74c72f4
Author: Jack Nelson <jack@jacknelson.xyz> Date: Wed Mar 30 20:34:22 2022 -0400 fix logic and tests commite738f44320
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>
291 lines
12 KiB
Python
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
|