11from __future__ import annotations
22
3+ import copy
34import dataclasses
45from typing import Any , Optional
56
67import pytest
78from chia_rs import AugSchemeMPL , G1Element , G2Element , PrivateKey
9+ from chiabip158 import PyBIP158
810
911from chia ._tests .clvm .test_puzzles import public_key_for_index , secret_exponent_for_index
1012from 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
155157def 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
0 commit comments