Skip to content

Commit eda986d

Browse files
committed
use per-puzzle cost to estimate DEDUP savings, rather than rerunning puzzles
1 parent 7cee2b2 commit eda986d

File tree

6 files changed

+36
-86
lines changed

6 files changed

+36
-86
lines changed

chia/_tests/core/mempool/test_mempool.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
from chia.consensus.cost_calculator import NPCResult
4343
from chia.consensus.default_constants import DEFAULT_CONSTANTS
4444
from chia.full_node.bitcoin_fee_estimator import create_bitcoin_fee_estimator
45-
from chia.full_node.eligible_coin_spends import run_for_cost
4645
from chia.full_node.fee_estimation import EmptyMempoolInfo, MempoolInfo
4746
from chia.full_node.full_node_api import FullNodeAPI
4847
from chia.full_node.mempool import Mempool
@@ -3135,15 +3134,11 @@ def make_test_spendbundle(coin: Coin, *, fee: int = 0, with_higher_cost: bool =
31353134

31363135

31373136
def test_aggregating_on_a_solution_then_a_more_cost_saving_one_appears() -> None:
3138-
def agg_and_add_sb_returning_cost_info(mempool: Mempool, spend_bundles: list[SpendBundle]) -> uint64:
3137+
def agg_and_add_sb_returning_cost_info(mempool: Mempool, spend_bundles: list[SpendBundle]) -> None:
31393138
sb = SpendBundle.aggregate(spend_bundles)
31403139
mi = mempool_item_from_spendbundle(sb)
31413140
mempool.add_to_pool(mi)
31423141
invariant_check_mempool(mempool)
3143-
saved_cost = run_for_cost(
3144-
sb.coin_spends[0].puzzle_reveal, sb.coin_spends[0].solution, len(mi.additions), mi.cost
3145-
)
3146-
return saved_cost
31473142

31483143
fee_estimator = create_bitcoin_fee_estimator(uint64(11000000000))
31493144
mempool_info = MempoolInfo(
@@ -3163,7 +3158,7 @@ def agg_and_add_sb_returning_cost_info(mempool: Mempool, spend_bundles: list[Spe
31633158
invariant_check_mempool(mempool)
31643159
# Create a ~2 FPC item that spends the eligible coin using the same solution A
31653160
sb_low_rate = make_test_spendbundle(coins[2], fee=highest_fee // 5)
3166-
saved_cost_on_solution_A = agg_and_add_sb_returning_cost_info(mempool, [sb_A, sb_low_rate])
3161+
agg_and_add_sb_returning_cost_info(mempool, [sb_A, sb_low_rate])
31673162
invariant_check_mempool(mempool)
31683163
result = mempool.create_bundle_from_mempool_items(test_constants, uint32(0))
31693164
assert result is not None
@@ -3177,10 +3172,8 @@ def agg_and_add_sb_returning_cost_info(mempool: Mempool, spend_bundles: list[Spe
31773172
# We're picking this fee to get a ~3 FPC, and get picked after sb_A1
31783173
# (which has ~10 FPC) but before sb_A2 (which has ~2 FPC)
31793174
sb_mid_rate = make_test_spendbundle(coins[i], fee=38004852 - i)
3180-
saved_cost_on_solution_B = agg_and_add_sb_returning_cost_info(mempool, [sb_B, sb_mid_rate])
3175+
agg_and_add_sb_returning_cost_info(mempool, [sb_B, sb_mid_rate])
31813176
invariant_check_mempool(mempool)
3182-
# We'd save more cost if we went with solution B instead of A
3183-
assert saved_cost_on_solution_B > saved_cost_on_solution_A
31843177
# If we process everything now, the 3 x ~3 FPC items get skipped because
31853178
# sb_A1 gets picked before them (~10 FPC), so from then on only sb_A2 (~2 FPC)
31863179
# would get picked

chia/_tests/core/mempool/test_mempool_manager.py

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
DedupCoinSpend,
3535
IdenticalSpendDedup,
3636
SkipDedup,
37-
run_for_cost,
3837
)
3938
from chia.full_node.mempool import MAX_SKIPPED_ITEMS, PRIORITY_TX_THRESHOLD
4039
from chia.full_node.mempool_manager import (
@@ -535,7 +534,7 @@ def spend_bundle_from_conditions(
535534

536535
async def add_spendbundle(
537536
mempool_manager: MempoolManager, sb: SpendBundle, sb_name: bytes32
538-
) -> tuple[Optional[uint64], MempoolInclusionStatus, Optional[Err]]:
537+
) -> tuple[uint64, MempoolInclusionStatus, Optional[Err]]:
539538
sbc = await mempool_manager.pre_validate_spendbundle(sb, sb_name)
540539
ret = await mempool_manager.add_spend_bundle(sb, sbc, sb_name, TEST_HEIGHT)
541540
invariant_check_mempool(mempool_manager.mempool)
@@ -547,7 +546,7 @@ async def generate_and_add_spendbundle(
547546
conditions: list[list[Any]],
548547
coin: Coin = TEST_COIN,
549548
aggsig: G2Element = G2Element(),
550-
) -> tuple[SpendBundle, bytes32, tuple[Optional[uint64], MempoolInclusionStatus, Optional[Err]]]:
549+
) -> tuple[SpendBundle, bytes32, tuple[uint64, MempoolInclusionStatus, Optional[Err]]]:
551550
sb = spend_bundle_from_conditions(conditions, coin, aggsig)
552551
sb_name = sb.name()
553552
result = await add_spendbundle(mempool_manager, sb, sb_name)
@@ -579,6 +578,7 @@ def make_bundle_spends_map_and_fee(
579578
eligible_for_dedup=bool(spend_conds.flags & ELIGIBLE_FOR_DEDUP),
580579
eligible_for_fast_forward=bool(spend_conds.flags & ELIGIBLE_FOR_FF),
581580
additions=additions,
581+
cost=uint64(spend_conds.condition_cost + spend_conds.execution_cost),
582582
latest_singleton_lineage=UnspentLineageInfo(coin_id, coin_spend.coin.parent_coin_info, bytes32([0] * 32))
583583
if bool(spend_conds.flags & ELIGIBLE_FOR_FF)
584584
else None,
@@ -703,7 +703,7 @@ async def get_coin_records(_: Collection[bytes32]) -> list[CoinRecord]:
703703
mempool_manager = await instantiate_mempool_manager(get_coin_records)
704704
conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, 1]]
705705
_, _, result = await generate_and_add_spendbundle(mempool_manager, conditions)
706-
assert result == (None, MempoolInclusionStatus.FAILED, Err.UNKNOWN_UNSPENT)
706+
assert result == (0, MempoolInclusionStatus.FAILED, Err.UNKNOWN_UNSPENT)
707707

708708

709709
@pytest.mark.anyio
@@ -885,6 +885,7 @@ def mk_bcs(coin_spend: CoinSpend, flags: int = 0) -> BundleCoinSpend:
885885
eligible_for_dedup=bool(flags & ELIGIBLE_FOR_DEDUP),
886886
eligible_for_fast_forward=bool(flags & ELIGIBLE_FOR_FF),
887887
additions=[],
888+
cost=uint64(0),
888889
)
889890

890891

@@ -1578,21 +1579,6 @@ async def test_replacing_one_with_an_eligible_coin() -> None:
15781579
assert_sb_in_pool(mempool_manager, sb123e4)
15791580

15801581

1581-
@pytest.mark.parametrize("amount", [0, 1])
1582-
def test_run_for_cost(amount: int) -> None:
1583-
conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, amount]]
1584-
solution = SerializedProgram.to(conditions)
1585-
cost = run_for_cost(IDENTITY_PUZZLE, solution, additions_count=1, max_cost=uint64(10000000))
1586-
assert cost == uint64(1800044)
1587-
1588-
1589-
def test_run_for_cost_max_cost() -> None:
1590-
conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, 1]]
1591-
solution = SerializedProgram.to(conditions)
1592-
with pytest.raises(ValueError, match="cost exceeded"):
1593-
run_for_cost(IDENTITY_PUZZLE, solution, additions_count=1, max_cost=uint64(43))
1594-
1595-
15961582
def test_dedup_info_nothing_to_do() -> None:
15971583
# No eligible coins, nothing to deduplicate, item gets considered normally
15981584

@@ -1636,7 +1622,9 @@ def test_dedup_info_eligible_1st_time() -> None:
16361622
Coin(TEST_COIN_ID, IDENTITY_PUZZLE_HASH, uint64(1)),
16371623
Coin(TEST_COIN_ID, IDENTITY_PUZZLE_HASH, uint64(TEST_COIN_AMOUNT - 1)),
16381624
}
1639-
assert dedup_coin_spends == IdenticalSpendDedup({TEST_COIN_ID: DedupCoinSpend(solution=solution, cost=None)})
1625+
assert dedup_coin_spends == IdenticalSpendDedup(
1626+
{TEST_COIN_ID: DedupCoinSpend(solution=solution, cost=uint64(3600044))}
1627+
)
16401628

16411629

16421630
def test_dedup_info_eligible_but_different_solution() -> None:
@@ -1646,7 +1634,7 @@ def test_dedup_info_eligible_but_different_solution() -> None:
16461634
[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT],
16471635
]
16481636
initial_solution = SerializedProgram.to(initial_conditions)
1649-
dedup_coin_spends = IdenticalSpendDedup({TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=None)})
1637+
dedup_coin_spends = IdenticalSpendDedup({TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=uint64(10))})
16501638
conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT]]
16511639
sb = spend_bundle_from_conditions(conditions, TEST_COIN)
16521640
mempool_item = mempool_item_from_spendbundle(sb)
@@ -1663,7 +1651,9 @@ def test_dedup_info_eligible_2nd_time_and_another_1st_time() -> None:
16631651
[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT - 1],
16641652
]
16651653
initial_solution = SerializedProgram.to(initial_conditions)
1666-
dedup_coin_spends = IdenticalSpendDedup({TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=None)})
1654+
dedup_coin_spends = IdenticalSpendDedup(
1655+
{TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=uint64(1337))}
1656+
)
16671657
sb1 = spend_bundle_from_conditions(initial_conditions, TEST_COIN)
16681658
second_conditions = [[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT2]]
16691659
second_solution = SerializedProgram.to(second_conditions)
@@ -1676,15 +1666,14 @@ def test_dedup_info_eligible_2nd_time_and_another_1st_time() -> None:
16761666
)
16771667
# Only the eligible one that we encountered more than once gets deduplicated
16781668
assert unique_coin_spends == sb2.coin_spends
1679-
saved_cost = uint64(3600044)
1680-
assert cost_saving == saved_cost
1669+
assert cost_saving == uint64(1337)
16811670
assert unique_additions == [Coin(TEST_COIN_ID2, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT2)]
16821671
# The coin we encountered a second time has its cost and additions properly updated
16831672
# The coin we encountered for the first time gets cost None and an empty set of additions
16841673
expected_dedup_coin_spends = IdenticalSpendDedup(
16851674
{
1686-
TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=saved_cost),
1687-
TEST_COIN_ID2: DedupCoinSpend(solution=second_solution, cost=None),
1675+
TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=uint64(1337)),
1676+
TEST_COIN_ID2: DedupCoinSpend(solution=second_solution, cost=uint64(1800044)),
16881677
}
16891678
)
16901679
assert dedup_coin_spends == expected_dedup_coin_spends
@@ -1703,7 +1692,7 @@ def test_dedup_info_eligible_3rd_time_another_2nd_time_and_one_non_eligible() ->
17031692
dedup_coin_spends = IdenticalSpendDedup(
17041693
{
17051694
TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=saved_cost),
1706-
TEST_COIN_ID2: DedupCoinSpend(solution=second_solution, cost=None),
1695+
TEST_COIN_ID2: DedupCoinSpend(solution=second_solution, cost=uint64(1337)),
17071696
}
17081697
)
17091698
sb1 = spend_bundle_from_conditions(initial_conditions, TEST_COIN)
@@ -1723,7 +1712,7 @@ def test_dedup_info_eligible_3rd_time_another_2nd_time_and_one_non_eligible() ->
17231712
bundle_coin_spends=mempool_item.bundle_coin_spends, max_cost=mempool_item.conds.cost
17241713
)
17251714
assert unique_coin_spends == sb3.coin_spends
1726-
saved_cost2 = uint64(1800044)
1715+
saved_cost2 = uint64(1337)
17271716
assert cost_saving == saved_cost + saved_cost2
17281717
assert unique_additions == [Coin(TEST_COIN_ID3, IDENTITY_PUZZLE_HASH, TEST_COIN_AMOUNT3)]
17291718
expected_dedup_coin_spends = IdenticalSpendDedup(
@@ -1805,12 +1794,14 @@ async def test_bundle_coin_spends() -> None:
18051794
eligible_for_dedup=False,
18061795
eligible_for_fast_forward=False,
18071796
additions=[Coin(coins[i].name(), IDENTITY_PUZZLE_HASH, coins[i].amount)],
1797+
cost=uint64(3000044),
18081798
)
18091799
assert mi123e.bundle_coin_spends[coins[3].name()] == BundleCoinSpend(
18101800
coin_spend=eligible_sb.coin_spends[0],
18111801
eligible_for_dedup=True,
18121802
eligible_for_fast_forward=False,
18131803
additions=[Coin(coins[3].name(), IDENTITY_PUZZLE_HASH, coins[3].amount)],
1804+
cost=uint64(1800044),
18141805
)
18151806

18161807

@@ -2162,7 +2153,7 @@ async def get_coin_records(coin_ids: Collection[bytes32]) -> list[CoinRecord]:
21622153
result = await add_spendbundle(mempool_manager, bundle, bundle_name)
21632154
print(result)
21642155
if expected is not None:
2165-
assert result == (None, MempoolInclusionStatus.FAILED, expected)
2156+
assert result == (0, MempoolInclusionStatus.FAILED, expected)
21662157
else:
21672158
assert result[0] is not None
21682159
assert result[1] != MempoolInclusionStatus.FAILED
@@ -2453,7 +2444,7 @@ async def test_new_peak_ff_eviction(
24532444

24542445
bundle_add_info = await mempool_manager.add_spend_bundle(
24552446
bundle,
2456-
make_test_conds(spend_ids=[(singleton_spend.coin, ELIGIBLE_FOR_FF), (TEST_COIN, 0)], cost=1000000),
2447+
make_test_conds(spend_ids=[(singleton_spend.coin, ELIGIBLE_FOR_FF), (TEST_COIN, 0)], cost=uint64(1000000)),
24572448
bundle.name(),
24582449
first_added_height=uint32(1),
24592450
)
@@ -2541,7 +2532,7 @@ async def test_multiple_ff(use_optimization: bool) -> None:
25412532
(singleton_spend2.coin, ELIGIBLE_FOR_FF),
25422533
(TEST_COIN, 0),
25432534
],
2544-
cost=1000000,
2535+
cost=uint64(1000000),
25452536
),
25462537
bundle.name(),
25472538
first_added_height=uint32(1),

chia/_tests/core/mempool/test_singleton_fast_forward.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def test_perform_the_fast_forward() -> None:
168168
"517b0dadb0c310ded24dd86dff8205398080ff808080"
169169
)
170170
test_coin_spend = CoinSpend(test_coin, test_puzzle_reveal, test_solution)
171-
test_spend_data = BundleCoinSpend(test_coin_spend, False, True, [test_child_coin])
171+
test_spend_data = BundleCoinSpend(test_coin_spend, False, True, [test_child_coin], uint64(0))
172172
test_unspent_lineage_info = UnspentLineageInfo(
173173
coin_id=latest_unspent_coin.name(),
174174
parent_id=latest_unspent_coin.parent_coin_info,

chia/full_node/eligible_coin_spends.py

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,22 @@
11
from __future__ import annotations
22

33
import dataclasses
4-
from typing import Optional
54

65
from chia_rs import CoinSpend, ConsensusConstants, SpendBundle, fast_forward_singleton, get_conditions_from_spendbundle
76
from chia_rs.sized_bytes import bytes32
87
from chia_rs.sized_ints import uint32, uint64
98

10-
from chia.consensus.condition_costs import ConditionCost
119
from chia.types.blockchain_format.coin import Coin
12-
from chia.types.blockchain_format.program import run_mempool_with_cost
1310
from chia.types.blockchain_format.serialized_program import SerializedProgram
1411
from chia.types.internal_mempool_item import InternalMempoolItem
1512
from chia.types.mempool_item import BundleCoinSpend, UnspentLineageInfo
1613
from chia.util.errors import Err
1714

1815

19-
def run_for_cost(
20-
puzzle_reveal: SerializedProgram, solution: SerializedProgram, additions_count: int, max_cost: int
21-
) -> uint64:
22-
create_coins_cost = additions_count * ConditionCost.CREATE_COIN.value
23-
clvm_cost, _ = run_mempool_with_cost(puzzle_reveal, max_cost, solution)
24-
saved_cost = uint64(clvm_cost + create_coins_cost)
25-
return saved_cost
26-
27-
2816
@dataclasses.dataclass(frozen=True)
2917
class DedupCoinSpend:
3018
solution: SerializedProgram
31-
cost: Optional[uint64]
19+
cost: uint64
3220

3321

3422
def set_next_singleton_version(
@@ -165,13 +153,12 @@ def get_deduplication_info(
165153
if dedup_coin_spend is None:
166154
# We didn't process an item with this coin before. If we end up including
167155
# this item, add this pair to deduplication_spends
168-
new_dedup_spends[coin_id] = DedupCoinSpend(spend_data.coin_spend.solution, None)
156+
new_dedup_spends[coin_id] = DedupCoinSpend(spend_data.coin_spend.solution, spend_data.cost)
169157
unique_coin_spends.append(spend_data.coin_spend)
170158
unique_additions.extend(spend_data.additions)
171159
continue
172160
# See if the solution was identical
173-
current_solution, duplicate_cost = dataclasses.astuple(dedup_coin_spend)
174-
if current_solution != spend_data.coin_spend.solution:
161+
if dedup_coin_spend.solution != spend_data.coin_spend.solution:
175162
# It wasn't, so let's skip this whole item because it's relying on
176163
# spending this coin with a different solution and that would
177164
# conflict with the coin spends that we're deduplicating already
@@ -180,30 +167,7 @@ def get_deduplication_info(
180167
# solution we see from the relatively highest FPC item, to avoid
181168
# severe performance and/or time-complexity impact
182169
raise SkipDedup("Solution is different from what we're deduplicating on")
183-
# Let's calculate the saved cost if we never did that before
184-
if duplicate_cost is None:
185-
# See first if this mempool item had this cost computed before
186-
# This can happen if this item didn't get included in the previous block
187-
spend_cost = spend_data.cost
188-
if spend_cost is None:
189-
spend_cost = run_for_cost(
190-
puzzle_reveal=spend_data.coin_spend.puzzle_reveal,
191-
solution=spend_data.coin_spend.solution,
192-
additions_count=len(spend_data.additions),
193-
max_cost=max_cost,
194-
)
195-
# Update this mempool item's coin spends map
196-
bundle_coin_spends[coin_id] = BundleCoinSpend(
197-
coin_spend=spend_data.coin_spend,
198-
eligible_for_dedup=spend_data.eligible_for_dedup,
199-
eligible_for_fast_forward=spend_data.eligible_for_fast_forward,
200-
additions=spend_data.additions,
201-
cost=spend_cost,
202-
)
203-
duplicate_cost = spend_cost
204-
# If we end up including this item, update this entry's cost
205-
new_dedup_spends[coin_id] = DedupCoinSpend(current_solution, duplicate_cost)
206-
cost_saving += duplicate_cost
170+
cost_saving += dedup_coin_spend.cost
207171
# Update the eligible coin spends data
208172
self.deduplication_spends.update(new_dedup_spends)
209173
return unique_coin_spends, uint64(cost_saving), unique_additions

chia/full_node/mempool_manager.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def compute_assert_height(
124124

125125
@dataclass
126126
class SpendBundleAddInfo:
127-
cost: Optional[uint64]
127+
cost: uint64
128128
status: MempoolInclusionStatus
129129
removals: list[MempoolRemoveInfo]
130130
error: Optional[Err]
@@ -545,7 +545,7 @@ async def add_spend_bundle(
545545
return SpendBundleAddInfo(item.cost, MempoolInclusionStatus.PENDING, [], err)
546546
else:
547547
# Cannot add to the mempool or pending pool.
548-
return SpendBundleAddInfo(None, MempoolInclusionStatus.FAILED, [], err)
548+
return SpendBundleAddInfo(uint64(0), MempoolInclusionStatus.FAILED, [], err)
549549

550550
async def validate_spend_bundle(
551551
self,
@@ -625,6 +625,7 @@ async def validate_spend_bundle(
625625
eligible_for_dedup=bool(spend_conds.flags & ELIGIBLE_FOR_DEDUP),
626626
eligible_for_fast_forward=eligible_for_ff,
627627
additions=spend_additions,
628+
cost=uint64(spend_conds.condition_cost + spend_conds.execution_cost),
628629
latest_singleton_lineage=lineage_info,
629630
)
630631

chia/types/mempool_item.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ class BundleCoinSpend:
2424
eligible_for_dedup: bool
2525
eligible_for_fast_forward: bool
2626
additions: list[Coin]
27-
# cost on the specific solution in this item
28-
cost: Optional[uint64] = None
27+
# cost on the specific solution in this item. The cost includes execution
28+
# cost and conditions cost, not byte-cost.
29+
cost: uint64
2930

3031
# if this spend is eligible for fast forward, this may be set to the
3132
# current unspent lineage belonging to this singleton, that we would rebase

0 commit comments

Comments
 (0)