mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-08-24 10:05:29 -05:00
if a dedup spend doesn't make it into the block, restore the dedup state (#21115)
* if a dedup spend doesn't make it into the block, restore the dedup state * review comments
This commit is contained in:
@@ -7,6 +7,7 @@ from collections.abc import Callable
|
||||
|
||||
import pytest
|
||||
from chia_rs import (
|
||||
ELIGIBLE_FOR_DEDUP,
|
||||
ENABLE_KECCAK_OPS_OUTSIDE_GUARD,
|
||||
AugSchemeMPL,
|
||||
CoinSpend,
|
||||
@@ -3473,8 +3474,6 @@ def test_max_spends_per_block(old: bool) -> None:
|
||||
|
||||
@pytest.mark.parametrize("old", [True, False])
|
||||
def test_max_spends_per_block_with_dedup(old: bool) -> None:
|
||||
from chia_rs import ELIGIBLE_FOR_DEDUP
|
||||
|
||||
max_cost = uint64(11_000_000_000)
|
||||
fee_estimator = create_bitcoin_fee_estimator(max_cost)
|
||||
mempool_info = MempoolInfo(
|
||||
@@ -3514,6 +3513,62 @@ def test_max_spends_per_block_with_dedup(old: bool) -> None:
|
||||
assert len(generator.removals) > MAX_SPENDS_PER_BLOCK // 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("old", [True, False])
|
||||
def test_skipped_item_does_not_leak_dedup_state(old: bool) -> None:
|
||||
"""
|
||||
an item that is processed (and thus registers a shared
|
||||
dedup coin) but ultimately dropped must not leave that coin registered in
|
||||
the dedup state. Otherwise a later item spending the same coin gets
|
||||
"deduplicated" against a spend that never makes it into the block, dropping
|
||||
the coin from the block entirely.
|
||||
"""
|
||||
max_cost = uint64(11_000_000_000)
|
||||
fee_estimator = create_bitcoin_fee_estimator(max_cost)
|
||||
mempool_info = MempoolInfo(
|
||||
CLVMCost(uint64(max_cost * 10)),
|
||||
FeeRate(uint64(1000000)),
|
||||
CLVMCost(max_cost),
|
||||
)
|
||||
mempool = Mempool(mempool_info, fee_estimator)
|
||||
|
||||
shared_coin = make_coin(0)
|
||||
|
||||
# The "big" item registers the shared dedup coin but has more spends than a
|
||||
# block can hold, so it gets skipped. Its high fee gives it the highest
|
||||
# priority, so it's the first item processed (and the first to touch the
|
||||
# shared coin).
|
||||
big_item = mk_item(
|
||||
[shared_coin, *[make_coin(i + 1) for i in range(MAX_SPENDS_PER_BLOCK)]],
|
||||
cost=1,
|
||||
fee=1_000_000_000,
|
||||
flags=[ELIGIBLE_FOR_DEDUP],
|
||||
)
|
||||
info = mempool.add_to_pool(big_item)
|
||||
assert info.error is None
|
||||
|
||||
# The "small" item spends the same shared dedup coin plus one unique coin.
|
||||
# It's small enough to fit and is processed after the big item.
|
||||
unique_coin = make_coin(MAX_SPENDS_PER_BLOCK + 1)
|
||||
small_item = mk_item(
|
||||
[shared_coin, unique_coin],
|
||||
cost=1,
|
||||
fee=0,
|
||||
flags=[ELIGIBLE_FOR_DEDUP, 0],
|
||||
)
|
||||
info = mempool.add_to_pool(small_item)
|
||||
assert info.error is None
|
||||
|
||||
create_block = mempool.create_block_generator if old else mempool.create_block_generator2
|
||||
generator = create_block(test_constants, uint32(0), 30.0)
|
||||
assert generator is not None
|
||||
|
||||
removals = set(generator.removals)
|
||||
# The small item was included, so every coin it spends must be in the block.
|
||||
# In particular the shared coin must not have been deduplicated away against
|
||||
# the skipped big item.
|
||||
assert removals == {shared_coin, unique_coin}
|
||||
|
||||
|
||||
def test_keccak() -> None:
|
||||
# the keccak operator is 62. The assemble() function doesn't support it
|
||||
# (yet)
|
||||
|
||||
@@ -22,6 +22,7 @@ from chia_rs import (
|
||||
check_time_locks,
|
||||
get_conditions_from_spendbundle,
|
||||
run_block_generator2,
|
||||
supports_fast_forward,
|
||||
)
|
||||
from chia_rs.sized_bytes import bytes32
|
||||
from chia_rs.sized_ints import uint8, uint32, uint64
|
||||
@@ -1684,12 +1685,16 @@ def test_dedup_info_nothing_to_do() -> None:
|
||||
sb = spend_bundle_from_conditions(conditions, TEST_COIN, sig)
|
||||
mempool_item = mempool_item_from_spendbundle(sb)
|
||||
dedup_coin_spends = IdenticalSpendDedup()
|
||||
unique_coin_spends, cost_saving, unique_additions = dedup_coin_spends.get_deduplication_info(
|
||||
unique_coin_spends, cost_saving, unique_additions, dedup_state_update = dedup_coin_spends.get_deduplication_info(
|
||||
bundle_coin_spends=mempool_item.bundle_coin_spends
|
||||
)
|
||||
assert unique_coin_spends == sb.coin_spends
|
||||
assert cost_saving == 0
|
||||
assert unique_additions == [Coin(TEST_COIN_ID, IDENTITY_PUZZLE_HASH, uint64(1))]
|
||||
assert dedup_state_update == {}
|
||||
# get_deduplication_info must not mutate the dedup state on its own
|
||||
assert dedup_coin_spends == IdenticalSpendDedup()
|
||||
dedup_coin_spends.update_deduplication_spends(dedup_state_update)
|
||||
assert dedup_coin_spends == IdenticalSpendDedup()
|
||||
|
||||
|
||||
@@ -1704,7 +1709,7 @@ def test_dedup_info_eligible_1st_time() -> None:
|
||||
assert mempool_item.conds is not None
|
||||
dedup_coin_spends = IdenticalSpendDedup()
|
||||
solution = SerializedProgram.to(conditions)
|
||||
unique_coin_spends, cost_saving, unique_additions = dedup_coin_spends.get_deduplication_info(
|
||||
unique_coin_spends, cost_saving, unique_additions, dedup_state_update = dedup_coin_spends.get_deduplication_info(
|
||||
bundle_coin_spends=mempool_item.bundle_coin_spends
|
||||
)
|
||||
assert unique_coin_spends == sb.coin_spends
|
||||
@@ -1714,6 +1719,10 @@ def test_dedup_info_eligible_1st_time() -> None:
|
||||
Coin(TEST_COIN_ID, IDENTITY_PUZZLE_HASH, uint64(TEST_COIN_AMOUNT - 1)),
|
||||
}
|
||||
expected_cost = mempool_item.bundle_coin_spends[TEST_COIN_ID].cost
|
||||
# The update is returned but not applied until explicitly committed
|
||||
assert dedup_coin_spends == IdenticalSpendDedup()
|
||||
assert dedup_state_update == {TEST_COIN_ID: DedupCoinSpend(solution=solution, cost=expected_cost)}
|
||||
dedup_coin_spends.update_deduplication_spends(dedup_state_update)
|
||||
assert dedup_coin_spends == IdenticalSpendDedup(
|
||||
{TEST_COIN_ID: DedupCoinSpend(solution=solution, cost=expected_cost)}
|
||||
)
|
||||
@@ -1752,7 +1761,7 @@ def test_dedup_info_eligible_2nd_time_and_another_1st_time() -> None:
|
||||
sb = SpendBundle.aggregate([sb1, sb2])
|
||||
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(
|
||||
unique_coin_spends, cost_saving, unique_additions, dedup_state_update = dedup_coin_spends.get_deduplication_info(
|
||||
bundle_coin_spends=mempool_item.bundle_coin_spends
|
||||
)
|
||||
# Only the eligible one that we encountered more than once gets deduplicated
|
||||
@@ -1760,8 +1769,11 @@ def test_dedup_info_eligible_2nd_time_and_another_1st_time() -> None:
|
||||
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 is already in the map
|
||||
# The coin we encountered for the first time gets added with its solution and cost
|
||||
# The coin we encountered for the first time is only reported in the update,
|
||||
# not committed until we explicitly do so
|
||||
test_coin2_cost = mempool_item.bundle_coin_spends[TEST_COIN_ID2].cost
|
||||
assert dedup_state_update == {TEST_COIN_ID2: DedupCoinSpend(solution=second_solution, cost=test_coin2_cost)}
|
||||
dedup_coin_spends.update_deduplication_spends(dedup_state_update)
|
||||
expected_dedup_coin_spends = IdenticalSpendDedup(
|
||||
{
|
||||
TEST_COIN_ID: DedupCoinSpend(solution=initial_solution, cost=test_coin_cost),
|
||||
@@ -1801,13 +1813,16 @@ def test_dedup_info_eligible_3rd_time_another_2nd_time_and_one_non_eligible() ->
|
||||
sb = SpendBundle.aggregate([sb1, sb2, sb3])
|
||||
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(
|
||||
unique_coin_spends, cost_saving, unique_additions, dedup_state_update = dedup_coin_spends.get_deduplication_info(
|
||||
bundle_coin_spends=mempool_item.bundle_coin_spends
|
||||
)
|
||||
assert unique_coin_spends == sb3.coin_spends
|
||||
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
|
||||
# Both eligible coins were already known, so there's nothing new to commit
|
||||
# (TEST_COIN_ID3 is non-eligible, so it never ends up in the map)
|
||||
assert dedup_state_update == {}
|
||||
dedup_coin_spends.update_deduplication_spends(dedup_state_update)
|
||||
expected_dedup_coin_spends = IdenticalSpendDedup(
|
||||
{
|
||||
TEST_COIN_ID: DedupCoinSpend(initial_solution, test_coin_cost),
|
||||
@@ -2465,8 +2480,6 @@ class TestCoins:
|
||||
def make_singleton_spend(
|
||||
launcher_id: bytes32, parent_parent_id: bytes32 = bytes32([3] * 32), child_amount: int = 1
|
||||
) -> CoinSpend:
|
||||
from chia_rs import supports_fast_forward
|
||||
|
||||
from chia.wallet.lineage_proof import LineageProof
|
||||
from chia.wallet.puzzles.singleton_top_layer_v1_1 import puzzle_for_singleton, solution_for_singleton
|
||||
|
||||
|
||||
@@ -117,11 +117,17 @@ class IdenticalSpendDedup:
|
||||
|
||||
def get_deduplication_info(
|
||||
self, *, bundle_coin_spends: dict[bytes32, BundleCoinSpend]
|
||||
) -> tuple[list[CoinSpend], uint64, list[Coin]]:
|
||||
) -> tuple[list[CoinSpend], uint64, list[Coin], dict[bytes32, DedupCoinSpend]]:
|
||||
"""
|
||||
Checks all coin spends of a mempool item for deduplication eligibility and
|
||||
provides the caller with the necessary information that allows it to perform
|
||||
identical spend aggregation on that mempool item if possible
|
||||
identical spend aggregation on that mempool item if possible.
|
||||
|
||||
This does not mutate the deduplication state. The returned state update
|
||||
must be explicitly committed by the caller via
|
||||
`update_deduplication_spends()` once the item is confirmed to be included,
|
||||
so that items which are processed but ultimately dropped don't leave stale
|
||||
deduplication entries behind.
|
||||
|
||||
Args:
|
||||
bundle_coin_spends: the mempool item's coin spends data
|
||||
@@ -130,6 +136,9 @@ class IdenticalSpendDedup:
|
||||
list[CoinSpend]: list of unique coin spends in this mempool item
|
||||
uint64: the cost we're saving by deduplicating eligible coins
|
||||
list[Coin]: list of unique additions in this mempool item
|
||||
dict[bytes32, DedupCoinSpend]: the deduplication state update to
|
||||
commit via `update_deduplication_spends()` if the item ends up
|
||||
included
|
||||
|
||||
Raises:
|
||||
ValueError to skip the mempool item we're currently in, if it's
|
||||
@@ -162,9 +171,13 @@ class IdenticalSpendDedup:
|
||||
# different solutions are rejected in check_removals().
|
||||
raise SkipDedup("Solution is different from what we're deduplicating on")
|
||||
cost_saving += dedup_coin_spend.cost
|
||||
# Update the eligible coin spends data
|
||||
return unique_coin_spends, uint64(cost_saving), unique_additions, new_dedup_spends
|
||||
|
||||
def update_deduplication_spends(self, new_dedup_spends: dict[bytes32, DedupCoinSpend]) -> None:
|
||||
self.deduplication_spends.update(new_dedup_spends)
|
||||
return unique_coin_spends, uint64(cost_saving), unique_additions
|
||||
|
||||
def copy(self) -> IdenticalSpendDedup:
|
||||
return IdenticalSpendDedup(deduplication_spends=dict(self.deduplication_spends))
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
|
||||
+22
-16
@@ -26,6 +26,7 @@ from chia_rs.sized_ints import uint32, uint64
|
||||
|
||||
from chia.consensus.default_constants import DEFAULT_CONSTANTS
|
||||
from chia.full_node.eligible_coin_spends import (
|
||||
DedupCoinSpend,
|
||||
IdenticalSpendDedup,
|
||||
SingletonFastForward,
|
||||
SkipDedup,
|
||||
@@ -634,13 +635,14 @@ class Mempool:
|
||||
unique_coin_spends.append(spend_data.coin_spend)
|
||||
unique_additions.extend(spend_data.additions)
|
||||
ff_state_update: dict[bytes32, UnspentLineageInfo] = {}
|
||||
dedup_state_update: dict[bytes32, DedupCoinSpend] = {}
|
||||
cost_saving = 0
|
||||
else:
|
||||
bundle_coin_spends, ff_state_update = singleton_ff.process_fast_forward_spends(
|
||||
mempool_item=item, prev_tx_height=prev_tx_height, constants=constants
|
||||
)
|
||||
unique_coin_spends, cost_saving, unique_additions = dedup_coin_spends.get_deduplication_info(
|
||||
bundle_coin_spends=bundle_coin_spends
|
||||
unique_coin_spends, cost_saving, unique_additions, dedup_state_update = (
|
||||
dedup_coin_spends.get_deduplication_info(bundle_coin_spends=bundle_coin_spends)
|
||||
)
|
||||
item_cost = cost - cost_saving
|
||||
log.info(
|
||||
@@ -666,6 +668,7 @@ class Mempool:
|
||||
continue
|
||||
break
|
||||
singleton_ff.update_fast_forward_spends(ff_state_update)
|
||||
dedup_coin_spends.update_deduplication_spends(dedup_state_update)
|
||||
coin_spends.extend(unique_coin_spends)
|
||||
additions.extend(unique_additions)
|
||||
sigs.append(item.aggregated_signature)
|
||||
@@ -714,9 +717,10 @@ class Mempool:
|
||||
|
||||
dedup_coin_spends = IdenticalSpendDedup()
|
||||
singleton_ff = SingletonFastForward()
|
||||
# Fast forward state committed so far from accepted batches, used to
|
||||
# rollback on batch rejection.
|
||||
# Fast forward and dedup state committed so far from accepted batches,
|
||||
# used to rollback on batch rejection.
|
||||
committed_ff = singleton_ff.copy()
|
||||
committed_dedup = dedup_coin_spends.copy()
|
||||
log.info(f"Starting to make block, max cost: {self.mempool_info.max_block_clvm_cost}")
|
||||
generator_creation_start = monotonic()
|
||||
cursor = self._db_conn.execute("SELECT name, fee FROM tx ORDER BY priority DESC, seq ASC")
|
||||
@@ -749,8 +753,10 @@ class Mempool:
|
||||
bundle_coin_spends, ff_state_update = singleton_ff.process_fast_forward_spends(
|
||||
mempool_item=item, prev_tx_height=prev_tx_height, constants=constants
|
||||
)
|
||||
unique_coin_spends, cost_saving, unique_additions = dedup_coin_spends.get_deduplication_info(
|
||||
bundle_coin_spends=bundle_coin_spends
|
||||
# This `dedup_state_update` is only committed later on via
|
||||
# `update_deduplication_spends` if the item gets batched.
|
||||
unique_coin_spends, cost_saving, unique_additions, dedup_state_update = (
|
||||
dedup_coin_spends.get_deduplication_info(bundle_coin_spends=bundle_coin_spends)
|
||||
)
|
||||
new_fee_sum = fee_sum + fee
|
||||
if new_fee_sum > DEFAULT_CONSTANTS.MAX_COIN_AMOUNT:
|
||||
@@ -776,9 +782,10 @@ class Mempool:
|
||||
|
||||
block_cost = builder.cost()
|
||||
if added:
|
||||
# Update the checkpoint to include the fast forward
|
||||
# state from all the items in this accepted batch.
|
||||
# Update the checkpoint to include the fast forward and
|
||||
# dedup state from all the items in this accepted batch.
|
||||
committed_ff = singleton_ff.copy()
|
||||
committed_dedup = dedup_coin_spends.copy()
|
||||
added_spends += batch_spends
|
||||
additions.extend(batch_additions)
|
||||
removals.extend([cs.coin for sb in batch_transactions for cs in sb.coin_spends])
|
||||
@@ -793,29 +800,28 @@ class Mempool:
|
||||
else:
|
||||
log.info(f"Skipping transaction batch cumulative cost: {block_cost} batch cost: {batch_cost}")
|
||||
skipped_items += 1
|
||||
# Restore FF state
|
||||
# Restore FF and dedup state
|
||||
singleton_ff = committed_ff.copy()
|
||||
dedup_coin_spends = committed_dedup.copy()
|
||||
# Reset the batch
|
||||
batch_cost = 0
|
||||
batch_transactions = []
|
||||
batch_additions = []
|
||||
batch_spends = 0
|
||||
# Reprocess the current item against the correct fast
|
||||
# forward state.
|
||||
# forward and dedup state.
|
||||
bundle_coin_spends, ff_state_update = singleton_ff.process_fast_forward_spends(
|
||||
mempool_item=item, prev_tx_height=prev_tx_height, constants=constants
|
||||
)
|
||||
unique_coin_spends = []
|
||||
unique_additions = []
|
||||
for spend_data in bundle_coin_spends.values():
|
||||
unique_coin_spends.append(spend_data.coin_spend)
|
||||
unique_additions.extend(spend_data.additions)
|
||||
cost_saving = uint64(0)
|
||||
unique_coin_spends, cost_saving, unique_additions, dedup_state_update = (
|
||||
dedup_coin_spends.get_deduplication_info(bundle_coin_spends=bundle_coin_spends)
|
||||
)
|
||||
|
||||
if done:
|
||||
break
|
||||
|
||||
singleton_ff.update_fast_forward_spends(ff_state_update)
|
||||
dedup_coin_spends.update_deduplication_spends(dedup_state_update)
|
||||
batch_cost += cost - cost_saving
|
||||
batch_transactions.append(SpendBundle(unique_coin_spends, item.aggregated_signature))
|
||||
batch_spends += len(unique_coin_spends)
|
||||
|
||||
Reference in New Issue
Block a user