diff --git a/chia/_tests/core/mempool/test_mempool.py b/chia/_tests/core/mempool/test_mempool.py index 2895ee00efd7..cb6bf686becc 100644 --- a/chia/_tests/core/mempool/test_mempool.py +++ b/chia/_tests/core/mempool/test_mempool.py @@ -42,7 +42,6 @@ from chia.consensus.cost_calculator import NPCResult from chia.consensus.default_constants import DEFAULT_CONSTANTS from chia.full_node.bitcoin_fee_estimator import create_bitcoin_fee_estimator -from chia.full_node.eligible_coin_spends import run_for_cost from chia.full_node.fee_estimation import EmptyMempoolInfo, MempoolInfo from chia.full_node.full_node_api import FullNodeAPI from chia.full_node.mempool import Mempool @@ -2908,14 +2907,7 @@ async def test_invalid_coin_spend_coin( ], ) def test_items_by_feerate(items: list[MempoolItem], expected: list[Coin]) -> None: - fee_estimator = create_bitcoin_fee_estimator(uint64(11000000000)) - - mempool_info = MempoolInfo( - CLVMCost(uint64(11000000000 * 3)), - FeeRate(uint64(1000000)), - CLVMCost(uint64(11000000000)), - ) - mempool = Mempool(mempool_info, fee_estimator) + mempool = construct_mempool() for i in items: mempool.add_to_pool(i) @@ -2933,13 +2925,7 @@ def test_items_by_feerate(items: list[MempoolItem], expected: list[Coin]) -> Non @pytest.mark.parametrize("old", [True, False]) def test_timeout(old: bool) -> None: - fee_estimator = create_bitcoin_fee_estimator(uint64(11000000000)) - mempool_info = MempoolInfo( - CLVMCost(uint64(11000000000 * 3)), - FeeRate(uint64(1000000)), - CLVMCost(uint64(11000000000)), - ) - mempool = Mempool(mempool_info, fee_estimator) + mempool = construct_mempool() for i in range(50): item = mk_item(coins[i : i + 1], flags=[0], fee=0, cost=50) @@ -3109,13 +3095,7 @@ def test_limit_expiring_transactions(height: bool, items: list[int], expected: l ], ) def test_get_items_by_coin_ids(items: list[MempoolItem], coin_ids: list[bytes32], expected: list[MempoolItem]) -> None: - fee_estimator = create_bitcoin_fee_estimator(uint64(11000000000)) - mempool_info = MempoolInfo( - CLVMCost(uint64(11000000000 * 3)), - FeeRate(uint64(1000000)), - CLVMCost(uint64(11000000000)), - ) - mempool = Mempool(mempool_info, fee_estimator) + mempool = construct_mempool() for i in items: mempool.add_to_pool(i) invariant_check_mempool(mempool) @@ -3129,69 +3109,92 @@ def make_test_spendbundle(coin: Coin, *, fee: int = 0, with_higher_cost: bool = if with_higher_cost: conditions.extend([[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, i] for i in range(3)]) actual_fee += 3 - conditions.append([ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, coin.amount - actual_fee]) + conditions.append([ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, uint64(coin.amount - actual_fee)]) sb = spend_bundle_from_conditions(conditions, coin) return sb -def test_aggregating_on_a_solution_then_a_more_cost_saving_one_appears() -> None: - def agg_and_add_sb_returning_cost_info(mempool: Mempool, spend_bundles: list[SpendBundle]) -> uint64: +def construct_mempool() -> Mempool: + fee_estimator = create_bitcoin_fee_estimator(test_constants.MAX_BLOCK_COST_CLVM) + mempool_info = MempoolInfo( + CLVMCost(uint64(test_constants.MAX_BLOCK_COST_CLVM * 3)), + FeeRate(uint64(1000000)), + CLVMCost(test_constants.MAX_BLOCK_COST_CLVM), + ) + return Mempool(mempool_info, fee_estimator) + + +def make_coin(idx: int) -> Coin: + return Coin(IDENTITY_PUZZLE_HASH, IDENTITY_PUZZLE_HASH, uint64(2_000_000_000 + idx * 2)) + + +@pytest.mark.parametrize("old", [True, False]) +def test_dedup_by_fee(old: bool) -> None: + """ + We pick the solution to use for dedup based on the spendbundle with the highest + fee per cost, not based on which one would give the overall best fee per cost + """ + mempool = construct_mempool() + + def add_spend_bundles(spend_bundles: list[SpendBundle]) -> None: sb = SpendBundle.aggregate(spend_bundles) mi = mempool_item_from_spendbundle(sb) mempool.add_to_pool(mi) invariant_check_mempool(mempool) - saved_cost = run_for_cost( - sb.coin_spends[0].puzzle_reveal, sb.coin_spends[0].solution, len(mi.additions), mi.cost - ) - return saved_cost - fee_estimator = create_bitcoin_fee_estimator(uint64(11000000000)) - mempool_info = MempoolInfo( - CLVMCost(uint64(11000000000 * 3)), - FeeRate(uint64(1000000)), - CLVMCost(uint64(11000000000)), - ) - mempool = Mempool(mempool_info, fee_estimator) - coins = [ - Coin(IDENTITY_PUZZLE_HASH, IDENTITY_PUZZLE_HASH, uint64(amount)) for amount in range(2000000000, 2000000020, 2) - ] - # Create a ~10 FPC item that spends the eligible coin[0] - sb_A = make_test_spendbundle(coins[0]) - highest_fee = 58282830 - sb_high_rate = make_test_spendbundle(coins[1], fee=highest_fee) - agg_and_add_sb_returning_cost_info(mempool, [sb_A, sb_high_rate]) - invariant_check_mempool(mempool) - # Create a ~2 FPC item that spends the eligible coin using the same solution A - sb_low_rate = make_test_spendbundle(coins[2], fee=highest_fee // 5) - saved_cost_on_solution_A = agg_and_add_sb_returning_cost_info(mempool, [sb_A, sb_low_rate]) - invariant_check_mempool(mempool) - result = mempool.create_bundle_from_mempool_items(test_constants, uint32(0)) + DEDUP_COIN = make_coin(0) + COIN_A1 = make_coin(1) + COIN_A2 = make_coin(2) + # all other coins belong to solution B, the dedup alternative to solution A + + # Create a spend bundle with a high fee, spending sb_A, which supports dedup + sb_A = make_test_spendbundle(DEDUP_COIN) + sb_high_rate = make_test_spendbundle(COIN_A1, fee=10) + add_spend_bundles([sb_A, sb_high_rate]) + + # Create a spend bundle, with a low fee, that spends the dedup coin using the same solution A + sb_low_rate = make_test_spendbundle(COIN_A2, fee=10) + add_spend_bundles([sb_A, sb_low_rate]) + + create_block = mempool.create_block_generator if old else mempool.create_block_generator2 + # validate that dedup happens at all for sb_A + result = create_block(test_constants, uint32(0), 5.0) assert result is not None - agg, _ = result # Make sure both items would be processed - assert [c.coin for c in agg.coin_spends] == [coins[0], coins[1], coins[2]] - # Now let's add 3 x ~3 FPC items that spend the eligible coin differently - # (solution B). It creates a higher (saved) cost than solution A - sb_B = make_test_spendbundle(coins[0], with_higher_cost=True) - for i in range(3, 6): - # We're picking this fee to get a ~3 FPC, and get picked after sb_A1 - # (which has ~10 FPC) but before sb_A2 (which has ~2 FPC) - sb_mid_rate = make_test_spendbundle(coins[i], fee=38004852 - i) - saved_cost_on_solution_B = agg_and_add_sb_returning_cost_info(mempool, [sb_B, sb_mid_rate]) - invariant_check_mempool(mempool) - # We'd save more cost if we went with solution B instead of A - assert saved_cost_on_solution_B > saved_cost_on_solution_A - # If we process everything now, the 3 x ~3 FPC items get skipped because - # sb_A1 gets picked before them (~10 FPC), so from then on only sb_A2 (~2 FPC) - # would get picked - result = mempool.create_bundle_from_mempool_items(test_constants, uint32(0)) + assert result.removals == [DEDUP_COIN, COIN_A1, COIN_A2] + + # Now we add a bunch of alternative spends for coin 0, with lower fees + # Even though the total fee would be higher if we deduped on this solution, + # we won't. + sb_B = make_test_spendbundle(DEDUP_COIN, with_higher_cost=True) + for i in range(3, 600): + sb_high_rate = make_test_spendbundle(make_coin(i), fee=10) + add_spend_bundles([sb_B, sb_high_rate]) + + result = create_block(test_constants, uint32(0), 5.0) assert result is not None - agg, _ = result - # The 3 items got skipped here # We ran with solution A and missed bigger savings on solution B - assert mempool.size() == 5 - assert [c.coin for c in agg.coin_spends] == [coins[0], coins[1], coins[2]] - invariant_check_mempool(mempool) + # we've added 599 spend bundles now. 2 with solution A and 598 with solution B + assert mempool.size() == 599 + assert result.removals == [DEDUP_COIN, COIN_A1, COIN_A2] + + # Now, if we add a high fee per-cost-for sb_B, it should be picked + sb_high_rate = make_test_spendbundle(make_coin(600), fee=1_000_000_000) + add_spend_bundles([sb_B, sb_high_rate]) + + result = create_block(test_constants, uint32(0), 5.0) + assert result is not None + # The 3 items got skipped here + # We ran with solution B + # we've added 600 spend bundles now. 2 with solution A and 599 with solution B + assert mempool.size() == 600 + spends_in_block = set(result.removals) + assert DEDUP_COIN in spends_in_block + assert COIN_A1 not in spends_in_block + assert COIN_A2 not in spends_in_block + + for i in range(3, 601): + assert make_coin(i) in spends_in_block def test_get_puzzle_and_solution_for_coin_failure() -> None: @@ -3203,14 +3206,7 @@ def test_get_puzzle_and_solution_for_coin_failure() -> None: @pytest.mark.parametrize("old", [True, False]) def test_create_block_generator(old: bool) -> None: - mempool_info = MempoolInfo( - CLVMCost(uint64(11000000000 * 3)), - FeeRate(uint64(1000000)), - CLVMCost(uint64(11000000000)), - ) - - fee_estimator = create_bitcoin_fee_estimator(test_constants.MAX_BLOCK_COST_CLVM) - mempool = Mempool(mempool_info, fee_estimator) + mempool = construct_mempool() coins = [ Coin(IDENTITY_PUZZLE_HASH, IDENTITY_PUZZLE_HASH, uint64(amount)) for amount in range(2000000000, 2000000020, 2) ] diff --git a/chia/_tests/core/mempool/test_mempool_manager.py b/chia/_tests/core/mempool/test_mempool_manager.py index aa658a714206..a2197ccd29e1 100644 --- a/chia/_tests/core/mempool/test_mempool_manager.py +++ b/chia/_tests/core/mempool/test_mempool_manager.py @@ -34,7 +34,6 @@ DedupCoinSpend, IdenticalSpendDedup, SkipDedup, - run_for_cost, ) from chia.full_node.mempool import MAX_SKIPPED_ITEMS, PRIORITY_TX_THRESHOLD from chia.full_node.mempool_manager import ( @@ -568,21 +567,23 @@ def make_bundle_spends_map_and_fee( removals_amount += coin_spend.coin.amount spend_conds = spend_conditions.pop(coin_id) - additions_amount += coin_spend.coin.amount - additions = [] for puzzle_hash, amount, _ in spend_conds.create_coin: additions.append(Coin(coin_id, puzzle_hash, uint64(amount))) + additions_amount += amount bundle_coin_spends[coin_id] = BundleCoinSpend( coin_spend=coin_spend, eligible_for_dedup=bool(spend_conds.flags & ELIGIBLE_FOR_DEDUP), eligible_for_fast_forward=bool(spend_conds.flags & ELIGIBLE_FOR_FF), additions=additions, + cost=uint64(spend_conds.condition_cost + spend_conds.execution_cost), latest_singleton_lineage=UnspentLineageInfo(coin_id, coin_spend.coin.parent_coin_info, bytes32([0] * 32)) if bool(spend_conds.flags & ELIGIBLE_FOR_FF) else None, ) + assert additions_amount == conds.addition_amount + assert removals_amount == conds.removal_amount fee = uint64(removals_amount - additions_amount) return bundle_coin_spends, fee @@ -885,6 +886,7 @@ def mk_bcs(coin_spend: CoinSpend, flags: int = 0) -> BundleCoinSpend: eligible_for_dedup=bool(flags & ELIGIBLE_FOR_DEDUP), eligible_for_fast_forward=bool(flags & ELIGIBLE_FOR_FF), additions=[], + cost=uint64(0), ) @@ -1578,21 +1580,6 @@ async def test_replacing_one_with_an_eligible_coin() -> None: assert_sb_in_pool(mempool_manager, sb123e4) -@pytest.mark.parametrize("amount", [0, 1]) -def test_run_for_cost(amount: int) -> None: - conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, amount]] - solution = SerializedProgram.to(conditions) - cost = run_for_cost(IDENTITY_PUZZLE, solution, additions_count=1, max_cost=uint64(10000000)) - assert cost == uint64(1800044) - - -def test_run_for_cost_max_cost() -> None: - conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, 1]] - solution = SerializedProgram.to(conditions) - with pytest.raises(ValueError, match="cost exceeded"): - run_for_cost(IDENTITY_PUZZLE, solution, additions_count=1, max_cost=uint64(43)) - - def test_dedup_info_nothing_to_do() -> None: # No eligible coins, nothing to deduplicate, item gets considered normally @@ -1608,7 +1595,7 @@ def test_dedup_info_nothing_to_do() -> None: mempool_item = mempool_item_from_spendbundle(sb) dedup_coin_spends = IdenticalSpendDedup() unique_coin_spends, cost_saving, unique_additions = dedup_coin_spends.get_deduplication_info( - bundle_coin_spends=mempool_item.bundle_coin_spends, max_cost=mempool_item.conds.cost + bundle_coin_spends=mempool_item.bundle_coin_spends ) assert unique_coin_spends == sb.coin_spends assert cost_saving == 0 @@ -1628,7 +1615,7 @@ def test_dedup_info_eligible_1st_time() -> None: dedup_coin_spends = IdenticalSpendDedup() solution = SerializedProgram.to(conditions) unique_coin_spends, cost_saving, unique_additions = dedup_coin_spends.get_deduplication_info( - bundle_coin_spends=mempool_item.bundle_coin_spends, max_cost=mempool_item.conds.cost + bundle_coin_spends=mempool_item.bundle_coin_spends ) assert unique_coin_spends == sb.coin_spends assert cost_saving == 0 @@ -1636,7 +1623,10 @@ def test_dedup_info_eligible_1st_time() -> None: Coin(TEST_COIN_ID, IDENTITY_PUZZLE_HASH, uint64(1)), Coin(TEST_COIN_ID, IDENTITY_PUZZLE_HASH, uint64(TEST_COIN_AMOUNT - 1)), } - assert dedup_coin_spends == IdenticalSpendDedup({TEST_COIN_ID: DedupCoinSpend(solution=solution, cost=None)}) + expected_cost = mempool_item.bundle_coin_spends[TEST_COIN_ID].cost + assert dedup_coin_spends == IdenticalSpendDedup( + {TEST_COIN_ID: DedupCoinSpend(solution=solution, cost=expected_cost)} + ) def test_dedup_info_eligible_but_different_solution() -> None: @@ -1646,14 +1636,12 @@ def test_dedup_info_eligible_but_different_solution() -> None: [ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT], ] initial_solution = SerializedProgram.to(initial_conditions) - dedup_coin_spends = IdenticalSpendDedup({TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=None)}) + dedup_coin_spends = IdenticalSpendDedup({TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=uint64(10))}) conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT]] sb = spend_bundle_from_conditions(conditions, TEST_COIN) mempool_item = mempool_item_from_spendbundle(sb) with pytest.raises(SkipDedup, match="Solution is different from what we're deduplicating on"): - dedup_coin_spends.get_deduplication_info( - bundle_coin_spends=mempool_item.bundle_coin_spends, max_cost=mempool_item.conds.cost - ) + dedup_coin_spends.get_deduplication_info(bundle_coin_spends=mempool_item.bundle_coin_spends) def test_dedup_info_eligible_2nd_time_and_another_1st_time() -> None: @@ -1663,7 +1651,10 @@ def test_dedup_info_eligible_2nd_time_and_another_1st_time() -> None: [ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT - 1], ] initial_solution = SerializedProgram.to(initial_conditions) - dedup_coin_spends = IdenticalSpendDedup({TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=None)}) + test_coin_cost = uint64(1337) + dedup_coin_spends = IdenticalSpendDedup( + {TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=test_coin_cost)} + ) sb1 = spend_bundle_from_conditions(initial_conditions, TEST_COIN) second_conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT2]] second_solution = SerializedProgram.to(second_conditions) @@ -1672,19 +1663,19 @@ def test_dedup_info_eligible_2nd_time_and_another_1st_time() -> None: mempool_item = mempool_item_from_spendbundle(sb) assert mempool_item.conds is not None unique_coin_spends, cost_saving, unique_additions = dedup_coin_spends.get_deduplication_info( - bundle_coin_spends=mempool_item.bundle_coin_spends, max_cost=mempool_item.conds.cost + bundle_coin_spends=mempool_item.bundle_coin_spends ) # Only the eligible one that we encountered more than once gets deduplicated assert unique_coin_spends == sb2.coin_spends - saved_cost = uint64(3600044) - assert cost_saving == saved_cost + assert cost_saving == test_coin_cost assert unique_additions == [Coin(TEST_COIN_ID2, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT2)] - # The coin we encountered a second time has its cost and additions properly updated - # The coin we encountered for the first time gets cost None and an empty set of additions + # The coin we encountered a second time is already in the map + # The coin we encountered for the first time gets added with its solution and cost + test_coin2_cost = mempool_item.bundle_coin_spends[TEST_COIN_ID2].cost expected_dedup_coin_spends = IdenticalSpendDedup( { - TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=saved_cost), - TEST_COIN_ID2: DedupCoinSpend(solution=second_solution, cost=None), + TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=test_coin_cost), + TEST_COIN_ID2: DedupCoinSpend(solution=second_solution, cost=test_coin2_cost), } ) assert dedup_coin_spends == expected_dedup_coin_spends @@ -1699,11 +1690,12 @@ def test_dedup_info_eligible_3rd_time_another_2nd_time_and_one_non_eligible() -> initial_solution = SerializedProgram.to(initial_conditions) second_conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT2]] second_solution = SerializedProgram.to(second_conditions) - saved_cost = uint64(3600044) + test_coin_cost = uint64(42) + test_coin2_cost = uint64(1337) dedup_coin_spends = IdenticalSpendDedup( { - TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=saved_cost), - TEST_COIN_ID2: DedupCoinSpend(solution=second_solution, cost=None), + TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=test_coin_cost), + TEST_COIN_ID2: DedupCoinSpend(solution=second_solution, cost=test_coin2_cost), } ) sb1 = spend_bundle_from_conditions(initial_conditions, TEST_COIN) @@ -1720,16 +1712,16 @@ def test_dedup_info_eligible_3rd_time_another_2nd_time_and_one_non_eligible() -> mempool_item = mempool_item_from_spendbundle(sb) assert mempool_item.conds is not None unique_coin_spends, cost_saving, unique_additions = dedup_coin_spends.get_deduplication_info( - bundle_coin_spends=mempool_item.bundle_coin_spends, max_cost=mempool_item.conds.cost + bundle_coin_spends=mempool_item.bundle_coin_spends ) assert unique_coin_spends == sb3.coin_spends - saved_cost2 = uint64(1800044) - assert cost_saving == saved_cost + saved_cost2 + assert cost_saving == test_coin_cost + test_coin2_cost assert unique_additions == [Coin(TEST_COIN_ID3, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT3)] + # TEST_COIN_ID3 is non-eligible, so it doesn't end up in this map expected_dedup_coin_spends = IdenticalSpendDedup( { - TEST_COIN_ID: DedupCoinSpend(initial_solution, saved_cost), - TEST_COIN_ID2: DedupCoinSpend(second_solution, saved_cost2), + TEST_COIN_ID: DedupCoinSpend(initial_solution, test_coin_cost), + TEST_COIN_ID2: DedupCoinSpend(second_solution, test_coin2_cost), } ) assert dedup_coin_spends == expected_dedup_coin_spends @@ -1799,18 +1791,21 @@ async def test_bundle_coin_spends() -> None: await send_spendbundle(mempool_manager, sb123e) mi123e = mempool_manager.get_mempool_item(sb123e.name()) assert mi123e is not None + execution_cost = 44 for i in range(3): assert mi123e.bundle_coin_spends[coins[i].name()] == BundleCoinSpend( coin_spend=sb123.coin_spends[i], eligible_for_dedup=False, eligible_for_fast_forward=False, additions=[Coin(coins[i].name(), IDENTITY_PUZZLE_HASH, coins[i].amount)], + cost=uint64(ConditionCost.CREATE_COIN.value + ConditionCost.AGG_SIG.value + execution_cost), ) assert mi123e.bundle_coin_spends[coins[3].name()] == BundleCoinSpend( coin_spend=eligible_sb.coin_spends[0], eligible_for_dedup=True, eligible_for_fast_forward=False, additions=[Coin(coins[3].name(), IDENTITY_PUZZLE_HASH, coins[3].amount)], + cost=uint64(ConditionCost.CREATE_COIN.value + execution_cost), ) diff --git a/chia/_tests/core/mempool/test_singleton_fast_forward.py b/chia/_tests/core/mempool/test_singleton_fast_forward.py index f96e31bf44fe..ccbb367cbc39 100644 --- a/chia/_tests/core/mempool/test_singleton_fast_forward.py +++ b/chia/_tests/core/mempool/test_singleton_fast_forward.py @@ -168,7 +168,7 @@ def test_perform_the_fast_forward() -> None: "517b0dadb0c310ded24dd86dff8205398080ff808080" ) test_coin_spend = CoinSpend(test_coin, test_puzzle_reveal, test_solution) - test_spend_data = BundleCoinSpend(test_coin_spend, False, True, [test_child_coin]) + test_spend_data = BundleCoinSpend(test_coin_spend, False, True, [test_child_coin], uint64(0)) test_unspent_lineage_info = UnspentLineageInfo( coin_id=latest_unspent_coin.name(), parent_id=latest_unspent_coin.parent_coin_info, diff --git a/chia/full_node/eligible_coin_spends.py b/chia/full_node/eligible_coin_spends.py index 3bdcca74818a..5a93fb22708d 100644 --- a/chia/full_node/eligible_coin_spends.py +++ b/chia/full_node/eligible_coin_spends.py @@ -1,34 +1,22 @@ from __future__ import annotations import dataclasses -from typing import Optional from chia_rs import CoinSpend, ConsensusConstants, SpendBundle, fast_forward_singleton, get_conditions_from_spendbundle from chia_rs.sized_bytes import bytes32 from chia_rs.sized_ints import uint32, uint64 -from chia.consensus.condition_costs import ConditionCost from chia.types.blockchain_format.coin import Coin -from chia.types.blockchain_format.program import run_mempool_with_cost from chia.types.blockchain_format.serialized_program import SerializedProgram from chia.types.internal_mempool_item import InternalMempoolItem from chia.types.mempool_item import BundleCoinSpend, UnspentLineageInfo from chia.util.errors import Err -def run_for_cost( - puzzle_reveal: SerializedProgram, solution: SerializedProgram, additions_count: int, max_cost: int -) -> uint64: - create_coins_cost = additions_count * ConditionCost.CREATE_COIN.value - clvm_cost, _ = run_mempool_with_cost(puzzle_reveal, max_cost, solution) - saved_cost = uint64(clvm_cost + create_coins_cost) - return saved_cost - - @dataclasses.dataclass(frozen=True) class DedupCoinSpend: solution: SerializedProgram - cost: Optional[uint64] + cost: uint64 def set_next_singleton_version( @@ -128,7 +116,7 @@ class IdenticalSpendDedup: deduplication_spends: dict[bytes32, DedupCoinSpend] = dataclasses.field(default_factory=dict) def get_deduplication_info( - self, *, bundle_coin_spends: dict[bytes32, BundleCoinSpend], max_cost: int + self, *, bundle_coin_spends: dict[bytes32, BundleCoinSpend] ) -> tuple[list[CoinSpend], uint64, list[Coin]]: """ Checks all coin spends of a mempool item for deduplication eligibility and @@ -137,7 +125,6 @@ def get_deduplication_info( Args: bundle_coin_spends: the mempool item's coin spends data - max_cost: the maximum limit when running for cost Returns: list[CoinSpend]: list of unique coin spends in this mempool item @@ -146,7 +133,7 @@ def get_deduplication_info( Raises: ValueError to skip the mempool item we're currently in, if it's - attempting to spend an eligible coin with a different solution than the + attempting to spend an dedup coin with a different solution than the one we're already deduplicating on. """ cost_saving = 0 @@ -165,45 +152,16 @@ def get_deduplication_info( if dedup_coin_spend is None: # We didn't process an item with this coin before. If we end up including # this item, add this pair to deduplication_spends - new_dedup_spends[coin_id] = DedupCoinSpend(spend_data.coin_spend.solution, None) + new_dedup_spends[coin_id] = DedupCoinSpend(spend_data.coin_spend.solution, spend_data.cost) unique_coin_spends.append(spend_data.coin_spend) unique_additions.extend(spend_data.additions) continue # See if the solution was identical - current_solution, duplicate_cost = dataclasses.astuple(dedup_coin_spend) - if current_solution != spend_data.coin_spend.solution: - # It wasn't, so let's skip this whole item because it's relying on - # spending this coin with a different solution and that would - # conflict with the coin spends that we're deduplicating already - # NOTE: We can miss an opportunity to deduplicate on other solutions - # even if they end up saving more cost, as we're going for the first - # solution we see from the relatively highest FPC item, to avoid - # severe performance and/or time-complexity impact + if dedup_coin_spend.solution != spend_data.coin_spend.solution: + # This should not happen. DEDUP spends of the same coin with + # different solutions are rejected in check_removals(). raise SkipDedup("Solution is different from what we're deduplicating on") - # Let's calculate the saved cost if we never did that before - if duplicate_cost is None: - # See first if this mempool item had this cost computed before - # This can happen if this item didn't get included in the previous block - spend_cost = spend_data.cost - if spend_cost is None: - spend_cost = run_for_cost( - puzzle_reveal=spend_data.coin_spend.puzzle_reveal, - solution=spend_data.coin_spend.solution, - additions_count=len(spend_data.additions), - max_cost=max_cost, - ) - # Update this mempool item's coin spends map - bundle_coin_spends[coin_id] = BundleCoinSpend( - coin_spend=spend_data.coin_spend, - eligible_for_dedup=spend_data.eligible_for_dedup, - eligible_for_fast_forward=spend_data.eligible_for_fast_forward, - additions=spend_data.additions, - cost=spend_cost, - ) - duplicate_cost = spend_cost - # If we end up including this item, update this entry's cost - new_dedup_spends[coin_id] = DedupCoinSpend(current_solution, duplicate_cost) - cost_saving += duplicate_cost + cost_saving += dedup_coin_spend.cost # Update the eligible coin spends data self.deduplication_spends.update(new_dedup_spends) return unique_coin_spends, uint64(cost_saving), unique_additions diff --git a/chia/full_node/mempool.py b/chia/full_node/mempool.py index a747d456585e..e4ea1afc0efe 100644 --- a/chia/full_node/mempool.py +++ b/chia/full_node/mempool.py @@ -627,7 +627,7 @@ def create_bundle_from_mempool_items( mempool_item=item, height=height, constants=constants ) unique_coin_spends, cost_saving, unique_additions = dedup_coin_spends.get_deduplication_info( - bundle_coin_spends=bundle_coin_spends, max_cost=cost + bundle_coin_spends=bundle_coin_spends ) item_cost = cost - cost_saving log.info( @@ -726,7 +726,7 @@ def create_block_generator2( mempool_item=item, height=height, constants=constants ) unique_coin_spends, cost_saving, unique_additions = dedup_coin_spends.get_deduplication_info( - bundle_coin_spends=bundle_coin_spends, max_cost=cost + bundle_coin_spends=bundle_coin_spends ) new_fee_sum = fee_sum + fee if new_fee_sum > DEFAULT_CONSTANTS.MAX_COIN_AMOUNT: diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index c621cd37d8a0..1943be4dc1b0 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -625,6 +625,7 @@ async def validate_spend_bundle( eligible_for_dedup=bool(spend_conds.flags & ELIGIBLE_FOR_DEDUP), eligible_for_fast_forward=eligible_for_ff, additions=spend_additions, + cost=uint64(spend_conds.condition_cost + spend_conds.execution_cost), latest_singleton_lineage=lineage_info, ) diff --git a/chia/types/mempool_item.py b/chia/types/mempool_item.py index a375100225e5..76f233c592c5 100644 --- a/chia/types/mempool_item.py +++ b/chia/types/mempool_item.py @@ -24,8 +24,9 @@ class BundleCoinSpend: eligible_for_dedup: bool eligible_for_fast_forward: bool additions: list[Coin] - # cost on the specific solution in this item - cost: Optional[uint64] = None + # cost on the specific solution in this item. The cost includes execution + # cost and conditions cost, not byte-cost. + cost: uint64 # if this spend is eligible for fast forward, this may be set to the # current unspent lineage belonging to this singleton, that we would rebase