Skip to content

Commit ad75591

Browse files
authored
CHIA-2382 Create a mempool item out of a copy of the input one when processing fast forward spends (#19273)
Create a fast forward mempool item out of a copy of the input one.
1 parent 6e542f0 commit ad75591

File tree

4 files changed

+118
-32
lines changed

4 files changed

+118
-32
lines changed

chia/_tests/core/mempool/test_singleton_fast_forward.py

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

3+
import copy
34
import dataclasses
45
from typing import Any, Optional
56

67
import pytest
78
from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey
9+
from chiabip158 import PyBIP158
810

911
from chia._tests.clvm.test_puzzles import public_key_for_index, secret_exponent_for_index
1012
from chia._tests.core.mempool.test_mempool_manager import (
@@ -57,14 +59,14 @@ async def get_unspent_lineage_info_for_puzzle_hash(_: bytes32) -> Optional[Unspe
5759
internal_mempool_item = InternalMempoolItem(sb, item.conds, item.height_added_to_mempool, item.bundle_coin_spends)
5860
original_version = dataclasses.replace(internal_mempool_item)
5961
eligible_coin_spends = EligibleCoinSpends()
60-
await eligible_coin_spends.process_fast_forward_spends(
62+
bundle_coin_spends = await eligible_coin_spends.process_fast_forward_spends(
6163
mempool_item=internal_mempool_item,
6264
get_unspent_lineage_info_for_puzzle_hash=get_unspent_lineage_info_for_puzzle_hash,
6365
height=TEST_HEIGHT,
6466
constants=DEFAULT_CONSTANTS,
6567
)
6668
assert eligible_coin_spends == EligibleCoinSpends()
67-
assert internal_mempool_item == original_version
69+
assert bundle_coin_spends == original_version.bundle_coin_spends
6870

6971

7072
@pytest.mark.anyio
@@ -130,7 +132,7 @@ async def get_unspent_lineage_info_for_puzzle_hash(puzzle_hash: bytes32) -> Opti
130132
internal_mempool_item = InternalMempoolItem(sb, item.conds, item.height_added_to_mempool, item.bundle_coin_spends)
131133
original_version = dataclasses.replace(internal_mempool_item)
132134
eligible_coin_spends = EligibleCoinSpends()
133-
await eligible_coin_spends.process_fast_forward_spends(
135+
bundle_coin_spends = await eligible_coin_spends.process_fast_forward_spends(
134136
mempool_item=internal_mempool_item,
135137
get_unspent_lineage_info_for_puzzle_hash=get_unspent_lineage_info_for_puzzle_hash,
136138
height=TEST_HEIGHT,
@@ -149,7 +151,7 @@ async def get_unspent_lineage_info_for_puzzle_hash(puzzle_hash: bytes32) -> Opti
149151
# We have set the next version from our additions to chain ff spends
150152
assert eligible_coin_spends.fast_forward_spends == expected_fast_forward_spends
151153
# We didn't need to fast forward the item so it stays as is
152-
assert internal_mempool_item == original_version
154+
assert bundle_coin_spends == original_version.bundle_coin_spends
153155

154156

155157
def test_perform_the_fast_forward() -> None:
@@ -607,3 +609,86 @@ async def test_singleton_fast_forward_same_block() -> None:
607609
assert unspent_lineage_info.parent_id == latest_singleton.parent_coin_info
608610
# The one before it should have the second last random amount
609611
assert unspent_lineage_info.parent_amount == random_amounts[-2]
612+
613+
614+
@pytest.mark.anyio
615+
async def test_mempool_items_immutability_on_ff() -> None:
616+
"""
617+
This tests processing singleton fast forward spends for mempool items using
618+
modified copies, without altering those original mempool items.
619+
"""
620+
SINGLETON_AMOUNT = uint64(1337)
621+
async with sim_and_client() as (sim, sim_client):
622+
singleton, eve_coin_spend, inner_puzzle, remaining_coin = await prepare_and_test_singleton(
623+
sim, sim_client, True, SINGLETON_AMOUNT, SINGLETON_AMOUNT
624+
)
625+
singleton_name = singleton.name()
626+
singleton_puzzle_hash = eve_coin_spend.coin.puzzle_hash
627+
inner_puzzle_hash = inner_puzzle.get_tree_hash()
628+
sk = AugSchemeMPL.key_gen(b"1" * 32)
629+
g1 = sk.get_g1()
630+
sig = AugSchemeMPL.sign(sk, b"foobar", g1)
631+
inner_conditions: list[list[Any]] = [
632+
[ConditionOpcode.AGG_SIG_UNSAFE, bytes(g1), b"foobar"],
633+
[ConditionOpcode.CREATE_COIN, inner_puzzle_hash, SINGLETON_AMOUNT],
634+
]
635+
singleton_coin_spend, singleton_signing_puzzle = make_singleton_coin_spend(
636+
eve_coin_spend, singleton, inner_puzzle, inner_conditions
637+
)
638+
remaining_spend_solution = SerializedProgram.from_program(
639+
Program.to([[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, remaining_coin.amount]])
640+
)
641+
remaining_coin_spend = CoinSpend(remaining_coin, IDENTITY_PUZZLE, remaining_spend_solution)
642+
await make_and_send_spend_bundle(
643+
sim,
644+
sim_client,
645+
[remaining_coin_spend, singleton_coin_spend],
646+
signing_puzzle=singleton_signing_puzzle,
647+
signing_coin=singleton,
648+
aggsig=sig,
649+
)
650+
unspent_lineage_info = await sim_client.service.coin_store.get_unspent_lineage_info_for_puzzle_hash(
651+
singleton_puzzle_hash
652+
)
653+
singleton_child, [remaining_coin] = await get_singleton_and_remaining_coins(sim)
654+
singleton_child_name = singleton_child.name()
655+
assert singleton_child.amount == SINGLETON_AMOUNT
656+
assert unspent_lineage_info == UnspentLineageInfo(
657+
coin_id=singleton_child_name,
658+
coin_amount=singleton_child.amount,
659+
parent_id=singleton_name,
660+
parent_amount=singleton.amount,
661+
parent_parent_id=eve_coin_spend.coin.name(),
662+
)
663+
# Now let's spend the first version again (despite being already spent
664+
# by now) to exercise its fast forward.
665+
remaining_spend_solution = SerializedProgram.from_program(
666+
Program.to([[ConditionOpcode.CREATE_COIN, IDENTITY_PUZZLE_HASH, remaining_coin.amount]])
667+
)
668+
remaining_coin_spend = CoinSpend(remaining_coin, IDENTITY_PUZZLE, remaining_spend_solution)
669+
sb = SpendBundle([remaining_coin_spend, singleton_coin_spend], sig)
670+
sb_name = sb.name()
671+
status, error = await sim_client.push_tx(sb)
672+
assert status == MempoolInclusionStatus.SUCCESS
673+
assert error is None
674+
original_item = copy.copy(sim_client.service.mempool_manager.get_mempool_item(sb_name))
675+
original_filter = sim_client.service.mempool_manager.get_filter()
676+
# Let's trigger the fast forward by creating a mempool bundle
677+
result = await sim.mempool_manager.create_bundle_from_mempool(
678+
sim_client.service.block_records[-1].header_hash,
679+
sim_client.service.coin_store.get_unspent_lineage_info_for_puzzle_hash,
680+
)
681+
assert result is not None
682+
bundle, _ = result
683+
# Make sure the mempool bundle we created contains the result of our
684+
# fast forward, instead of our original spend.
685+
assert any(cs.coin.name() == singleton_child_name for cs in bundle.coin_spends)
686+
assert not any(cs.coin.name() == singleton_name for cs in bundle.coin_spends)
687+
# We should have processed our item without modifying it in-place
688+
new_item = copy.copy(sim_client.service.mempool_manager.get_mempool_item(sb_name))
689+
new_filter = sim_client.service.mempool_manager.get_filter()
690+
assert new_item == original_item
691+
assert new_filter == original_filter
692+
sb_filter = PyBIP158(bytearray(original_filter))
693+
items_not_in_sb_filter = sim_client.service.mempool_manager.get_items_not_in_filter(sb_filter)
694+
assert len(items_not_in_sb_filter) == 0

chia/full_node/mempool.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,14 +526,14 @@ async def create_bundle_from_mempool_items(
526526
unique_additions.extend(spend_data.additions)
527527
cost_saving = 0
528528
else:
529-
await eligible_coin_spends.process_fast_forward_spends(
529+
bundle_coin_spends = await eligible_coin_spends.process_fast_forward_spends(
530530
mempool_item=item,
531531
get_unspent_lineage_info_for_puzzle_hash=get_unspent_lineage_info_for_puzzle_hash,
532532
height=height,
533533
constants=constants,
534534
)
535535
unique_coin_spends, cost_saving, unique_additions = eligible_coin_spends.get_deduplication_info(
536-
bundle_coin_spends=item.bundle_coin_spends, max_cost=cost
536+
bundle_coin_spends=bundle_coin_spends, max_cost=cost
537537
)
538538
item_cost = cost - cost_saving
539539
log.info(

chia/types/eligible_coin_spends.py

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import copy
34
import dataclasses
45
from collections.abc import Awaitable
56
from typing import Callable, Optional
@@ -233,26 +234,36 @@ async def process_fast_forward_spends(
233234
get_unspent_lineage_info_for_puzzle_hash: Callable[[bytes32], Awaitable[Optional[UnspentLineageInfo]]],
234235
height: uint32,
235236
constants: ConsensusConstants,
236-
) -> None:
237+
) -> dict[bytes32, BundleCoinSpend]:
237238
"""
238-
Provides the caller with an in-place internal mempool item that has a
239-
proper state of fast forwarded coin spends and additions starting from
239+
Provides the caller with a `bundle_coin_spends` map that has a proper
240+
state of fast forwarded coin spends and additions starting from
240241
the most recent unspent versions of the related singleton spends.
241242
242243
Args:
243-
mempool_item: in-out parameter for the internal mempool item to process
244+
mempool_item: The internal mempool item to process
244245
get_unspent_lineage_info_for_puzzle_hash: to lookup the most recent
245246
version of the singleton from the coin store
246247
constants: needed in order to refresh the mempool item if needed
247248
height: needed in order to refresh the mempool item if needed
248249
250+
Returns:
251+
The resulting `bundle_coin_spends` map of coin IDs to coin spends
252+
and metadata, after fast forwarding
253+
249254
Raises:
250255
If a fast forward cannot proceed, to prevent potential double spends
251256
"""
257+
258+
# Let's first create a copy of the mempool item's `bundle_coin_spends`
259+
# map to work on and return. This way we avoid the possibility of
260+
# propagating a modified version of this item through the network.
261+
bundle_coin_spends = copy.copy(mempool_item.bundle_coin_spends)
252262
new_coin_spends = []
263+
# Map of rebased singleton coin ID to coin spend and metadata
253264
ff_bundle_coin_spends = {}
254265
replaced_coin_ids = []
255-
for coin_id, spend_data in mempool_item.bundle_coin_spends.items():
266+
for coin_id, spend_data in bundle_coin_spends.items():
256267
if not spend_data.eligible_for_fast_forward:
257268
# Nothing to do for this spend, moving on
258269
new_coin_spends.append(spend_data.coin_spend)
@@ -326,21 +337,17 @@ async def process_fast_forward_spends(
326337
new_coin_spends.append(new_coin_spend)
327338
if len(ff_bundle_coin_spends) == 0:
328339
# This item doesn't have any fast forward coins, nothing to do here
329-
return
340+
return bundle_coin_spends
330341
# Update the mempool item after validating the new spend bundle
331342
new_sb = SpendBundle(
332343
coin_spends=new_coin_spends, aggregated_signature=mempool_item.spend_bundle.aggregated_signature
333344
)
334-
# We need to run the new spend bundle to make sure it remains valid
335345
assert mempool_item.conds is not None
336346
try:
337-
new_conditions = get_conditions_from_spendbundle(
338-
new_sb,
339-
mempool_item.conds.cost,
340-
constants,
341-
height,
342-
)
343-
# validate_clvm_and_signature raises a TypeError with an error code
347+
# Run the new spend bundle to make sure it remains valid. What we
348+
# care about here is whether this call throws or not.
349+
get_conditions_from_spendbundle(new_sb, mempool_item.conds.cost, constants, height)
350+
# get_conditions_from_spendbundle raises a TypeError with an error code
344351
except TypeError as e:
345352
# Convert that to a ValidationError
346353
if len(e.args) > 0:
@@ -351,15 +358,9 @@ async def process_fast_forward_spends(
351358
"Mempool item became invalid after singleton fast forward with an unspecified error."
352359
) # pragma: no cover
353360

354-
# Update bundle_coin_spends using the collected data
361+
# Update bundle_coin_spends using the map of rebased singleton coin ID
362+
# to coin spend and metadata.
355363
for coin_id in replaced_coin_ids:
356-
mempool_item.bundle_coin_spends.pop(coin_id, None)
357-
mempool_item.bundle_coin_spends.update(ff_bundle_coin_spends)
358-
# Update the mempool item with the new spend bundle related information
359-
# NOTE: From this point on, in `create_bundle_from_mempool_items`, we rely
360-
# on `bundle_coin_spends` and we don't use this updated spend bundle
361-
# information, as we'll only need `aggregated_signature` which doesn't
362-
# change. Still, it's good form to update the spend bundle with the
363-
# new coin spends
364-
mempool_item.spend_bundle = new_sb
365-
mempool_item.conds = new_conditions
364+
bundle_coin_spends.pop(coin_id, None)
365+
bundle_coin_spends.update(ff_bundle_coin_spends)
366+
return bundle_coin_spends

chia/types/internal_mempool_item.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from chia.util.ints import uint32
1010

1111

12-
@dataclass
12+
@dataclass(frozen=True)
1313
class InternalMempoolItem:
1414
spend_bundle: SpendBundle
1515
conds: SpendBundleConditions

0 commit comments

Comments
 (0)