22
33import dataclasses
44import logging
5- from collections .abc import Awaitable , Collection
6- from typing import Any , Callable , ClassVar , Optional
5+ from collections .abc import Awaitable , Collection , Sequence
6+ from typing import Any , Callable , ClassVar , Optional , Union
77
88import pytest
99from chia_rs import (
@@ -208,15 +208,22 @@ def make_test_conds(
208208 before_seconds_relative : Optional [int ] = None ,
209209 before_seconds_absolute : Optional [int ] = None ,
210210 cost : int = 0 ,
211- spend_ids : list [tuple [bytes32 , int ]] = [(TEST_COIN_ID , 0 )],
211+ spend_ids : Sequence [tuple [Union [ bytes32 , Coin ] , int ]] = [(TEST_COIN_ID , 0 )],
212212) -> SpendBundleConditions :
213+ spend_info : list [tuple [bytes32 , bytes32 , bytes32 , uint64 , int ]] = []
214+ for coin , flags in spend_ids :
215+ if isinstance (coin , Coin ):
216+ spend_info .append ((coin .name (), coin .parent_coin_info , coin .puzzle_hash , coin .amount , flags ))
217+ else :
218+ spend_info .append ((coin , IDENTITY_PUZZLE_HASH , IDENTITY_PUZZLE_HASH , TEST_COIN_AMOUNT , flags ))
219+
213220 return SpendBundleConditions (
214221 [
215222 SpendConditions (
216- spend_id ,
217- IDENTITY_PUZZLE_HASH ,
218- IDENTITY_PUZZLE_HASH ,
219- TEST_COIN_AMOUNT ,
223+ coin_id ,
224+ parent_id ,
225+ puzzle_hash ,
226+ amount ,
220227 None if height_relative is None else uint32 (height_relative ),
221228 None if seconds_relative is None else uint64 (seconds_relative ),
222229 None if before_height_relative is None else uint32 (before_height_relative ),
@@ -233,7 +240,7 @@ def make_test_conds(
233240 [],
234241 flags ,
235242 )
236- for spend_id , flags in spend_ids
243+ for coin_id , parent_id , puzzle_hash , amount , flags in spend_info
237244 ],
238245 0 ,
239246 uint32 (height_absolute ),
@@ -2181,3 +2188,316 @@ async def test_height_added_to_mempool(optimized_path: bool) -> None:
21812188 mempool_item = mempool_manager .get_mempool_item (sb_name )
21822189 assert mempool_item is not None
21832190 assert mempool_item .height_added_to_mempool == original_height
2191+
2192+
2193+ # This is a test utility to provide a simple view of the coin table for the
2194+ # mempool manager.
2195+ class TestCoins :
2196+ coin_records : dict [bytes32 , CoinRecord ]
2197+ lineage_info : dict [bytes32 , UnspentLineageInfo ]
2198+
2199+ def __init__ (self , coins : list [Coin ], lineage : dict [bytes32 , Coin ]) -> None :
2200+ self .coin_records = {}
2201+ for c in coins :
2202+ self .coin_records [c .name ()] = CoinRecord (c , uint32 (0 ), uint32 (0 ), False , TEST_TIMESTAMP )
2203+ self .lineage_info = {}
2204+ for ph , c in lineage .items ():
2205+ self .lineage_info [ph ] = UnspentLineageInfo (
2206+ c .name (), c .amount , c .parent_coin_info , uint64 (1337 ), bytes32 ([42 ] * 32 )
2207+ )
2208+
2209+ def spend_coin (self , coin_id : bytes32 , height : uint32 = uint32 (10 )) -> None :
2210+ self .coin_records [coin_id ] = dataclasses .replace (self .coin_records [coin_id ], spent_block_index = height )
2211+
2212+ def update_lineage (self , puzzle_hash : bytes32 , coin : Optional [Coin ]) -> None :
2213+ if coin is None :
2214+ self .lineage_info .pop (puzzle_hash )
2215+ else :
2216+ assert coin .puzzle_hash == puzzle_hash
2217+ prev = self .lineage_info [puzzle_hash ]
2218+ self .lineage_info [puzzle_hash ] = UnspentLineageInfo (
2219+ coin .name (), coin .amount , coin .parent_coin_info , prev .coin_amount , prev .coin_id
2220+ )
2221+
2222+ async def get_coin_records (self , coin_ids : Collection [bytes32 ]) -> list [CoinRecord ]:
2223+ ret = []
2224+ for coin_id in coin_ids :
2225+ rec = self .coin_records .get (coin_id )
2226+ if rec is not None :
2227+ ret .append (rec )
2228+
2229+ return ret
2230+
2231+ async def get_unspent_lineage_info (self , ph : bytes32 ) -> Optional [UnspentLineageInfo ]:
2232+ return self .lineage_info .get (ph )
2233+
2234+
2235+ # creates a CoinSpend of a made up
2236+ def make_singleton_spend (launcher_id : bytes32 , parent_parent_id : bytes32 = bytes32 ([3 ] * 32 )) -> CoinSpend :
2237+ from chia_rs import supports_fast_forward
2238+
2239+ from chia .wallet .lineage_proof import LineageProof
2240+ from chia .wallet .puzzles .singleton_top_layer_v1_1 import (
2241+ puzzle_for_singleton ,
2242+ solution_for_singleton ,
2243+ )
2244+
2245+ singleton_puzzle = SerializedProgram .from_program (puzzle_for_singleton (launcher_id , Program .to (1 )))
2246+
2247+ PARENT_COIN = Coin (parent_parent_id , singleton_puzzle .get_tree_hash (), uint64 (1 ))
2248+ COIN = Coin (PARENT_COIN .name (), singleton_puzzle .get_tree_hash (), uint64 (1 ))
2249+
2250+ lineage_proof = LineageProof (parent_parent_id , IDENTITY_PUZZLE_HASH , uint64 (1 ))
2251+
2252+ inner_solution = Program .to ([[ConditionOpcode .CREATE_COIN , IDENTITY_PUZZLE_HASH , uint64 (1 )]])
2253+ singleton_solution = SerializedProgram .from_program (
2254+ solution_for_singleton (lineage_proof , uint64 (1 ), inner_solution )
2255+ )
2256+
2257+ ret = CoinSpend (COIN , singleton_puzzle , singleton_solution )
2258+
2259+ # we make sure the spend actually supports fast forward
2260+ assert supports_fast_forward (ret )
2261+ assert ret .coin .puzzle_hash == ret .puzzle_reveal .get_tree_hash ()
2262+ return ret
2263+
2264+
2265+ async def setup_mempool (coins : TestCoins ) -> MempoolManager :
2266+ mempool_manager = MempoolManager (
2267+ coins .get_coin_records ,
2268+ coins .get_unspent_lineage_info ,
2269+ DEFAULT_CONSTANTS ,
2270+ )
2271+ test_block_record = create_test_block_record (height = uint32 (10 ), timestamp = uint64 (12345678 ))
2272+ await mempool_manager .new_peak (test_block_record , None )
2273+ return mempool_manager
2274+
2275+
2276+ # adds a new peak to the memepool manager with the specified coin IDs spent
2277+ async def advance_mempool (
2278+ mempool : MempoolManager , spent_coins : list [bytes32 ], * , use_optimization : bool = True
2279+ ) -> None :
2280+ br = mempool .peak
2281+ assert br is not None
2282+
2283+ if use_optimization :
2284+ next_height = uint32 (br .height + 1 )
2285+ else :
2286+ next_height = uint32 (br .height + 2 )
2287+
2288+ assert br .timestamp is not None
2289+ prev_block_hash = br .header_hash
2290+ br = create_test_block_record (height = next_height , timestamp = uint64 (br .timestamp + 10 ))
2291+
2292+ if use_optimization :
2293+ assert prev_block_hash == br .prev_transaction_block_hash
2294+ else :
2295+ assert prev_block_hash != br .prev_transaction_block_hash
2296+
2297+ await mempool .new_peak (br , spent_coins )
2298+ invariant_check_mempool (mempool .mempool )
2299+
2300+
2301+ @pytest .mark .anyio
2302+ @pytest .mark .parametrize ("spend_singleton" , [True , False ])
2303+ @pytest .mark .parametrize ("spend_plain" , [True , False ])
2304+ @pytest .mark .parametrize ("use_optimization" , [True , False ])
2305+ @pytest .mark .parametrize ("reverse_spend_order" , [True , False ])
2306+ async def test_new_peak_ff_eviction (
2307+ spend_singleton : bool , spend_plain : bool , use_optimization : bool , reverse_spend_order : bool
2308+ ) -> None :
2309+ LAUNCHER_ID = bytes32 ([1 ] * 32 )
2310+ singleton_spend = make_singleton_spend (LAUNCHER_ID )
2311+
2312+ coin_spend = make_spend (
2313+ TEST_COIN ,
2314+ IDENTITY_PUZZLE ,
2315+ Program .to ([[ConditionOpcode .CREATE_COIN , IDENTITY_PUZZLE_HASH , 1336 ]]),
2316+ )
2317+ bundle = SpendBundle ([singleton_spend , coin_spend ], G2Element ())
2318+
2319+ coins = TestCoins ([singleton_spend .coin , TEST_COIN ], {singleton_spend .coin .puzzle_hash : singleton_spend .coin })
2320+
2321+ mempool_manager = await setup_mempool (coins )
2322+
2323+ bundle_add_info = await mempool_manager .add_spend_bundle (
2324+ bundle ,
2325+ make_test_conds (spend_ids = [(singleton_spend .coin , ELIGIBLE_FOR_FF ), (TEST_COIN , 0 )], cost = 1000000 ),
2326+ bundle .name (),
2327+ first_added_height = uint32 (1 ),
2328+ )
2329+
2330+ assert bundle_add_info .status == MempoolInclusionStatus .SUCCESS
2331+ item = mempool_manager .get_mempool_item (bundle .name ())
2332+ assert item is not None
2333+ assert item .bundle_coin_spends [singleton_spend .coin .name ()].eligible_for_fast_forward
2334+ assert item .bundle_coin_spends [singleton_spend .coin .name ()].latest_singleton_coin == singleton_spend .coin .name ()
2335+
2336+ spent_coins : list [bytes32 ] = []
2337+
2338+ if spend_singleton :
2339+ # pretend that we melted the singleton, the FF spend
2340+ coins .update_lineage (singleton_spend .coin .puzzle_hash , None )
2341+ coins .spend_coin (singleton_spend .coin .name (), uint32 (11 ))
2342+ spent_coins .append (singleton_spend .coin .name ())
2343+
2344+ if spend_plain :
2345+ # pretend that we spend singleton, the FF spend
2346+ coins .spend_coin (coin_spend .coin .name (), uint32 (11 ))
2347+ spent_coins .append (coin_spend .coin .name ())
2348+
2349+ assert bundle_add_info .status == MempoolInclusionStatus .SUCCESS
2350+ invariant_check_mempool (mempool_manager .mempool )
2351+
2352+ if reverse_spend_order :
2353+ spent_coins .reverse ()
2354+
2355+ await advance_mempool (mempool_manager , spent_coins , use_optimization = use_optimization )
2356+
2357+ # make sure the mempool item is evicted
2358+ if spend_singleton or spend_plain :
2359+ assert mempool_manager .get_mempool_item (bundle .name ()) is None
2360+ else :
2361+ item = mempool_manager .get_mempool_item (bundle .name ())
2362+ assert item is not None
2363+ assert item .bundle_coin_spends [singleton_spend .coin .name ()].eligible_for_fast_forward
2364+ assert item .bundle_coin_spends [singleton_spend .coin .name ()].latest_singleton_coin == singleton_spend .coin .name ()
2365+
2366+
2367+ @pytest .mark .anyio
2368+ @pytest .mark .parametrize ("use_optimization" , [True , False ])
2369+ async def test_multiple_ff (use_optimization : bool ) -> None :
2370+ # create two different singleton spends of the same singleton, that support
2371+ # fast forward. Then update the latest singleton coin and ensure both
2372+ # entries in the mempool are updated accordingly
2373+
2374+ PARENT_PARENT1 = bytes32 ([4 ] * 32 )
2375+ PARENT_PARENT2 = bytes32 ([5 ] * 32 )
2376+ PARENT_PARENT3 = bytes32 ([6 ] * 32 )
2377+
2378+ # two different spends of the same singleton. both can be fast-forwarded
2379+ LAUNCHER_ID = bytes32 ([1 ] * 32 )
2380+ singleton_spend1 = make_singleton_spend (LAUNCHER_ID , PARENT_PARENT1 )
2381+ singleton_spend2 = make_singleton_spend (LAUNCHER_ID , PARENT_PARENT2 )
2382+
2383+ # in the next block, this will be the latest singleton coin
2384+ singleton_spend3 = make_singleton_spend (LAUNCHER_ID , PARENT_PARENT3 )
2385+
2386+ coin_spend = make_spend (
2387+ TEST_COIN ,
2388+ IDENTITY_PUZZLE ,
2389+ Program .to ([[ConditionOpcode .CREATE_COIN , IDENTITY_PUZZLE_HASH , 1336 ]]),
2390+ )
2391+ bundle = SpendBundle ([singleton_spend1 , singleton_spend2 , coin_spend ], G2Element ())
2392+
2393+ # the singleton puzzle hash resulves to the most recent singleton coin, number 2
2394+ # pretend that coin1 is spent
2395+ singleton_ph = singleton_spend2 .coin .puzzle_hash
2396+ coins = TestCoins ([singleton_spend1 .coin , singleton_spend2 .coin , TEST_COIN ], {singleton_ph : singleton_spend2 .coin })
2397+
2398+ mempool_manager = await setup_mempool (coins )
2399+
2400+ bundle_add_info = await mempool_manager .add_spend_bundle (
2401+ bundle ,
2402+ make_test_conds (
2403+ spend_ids = [
2404+ (singleton_spend1 .coin , ELIGIBLE_FOR_FF ),
2405+ (singleton_spend2 .coin , ELIGIBLE_FOR_FF ),
2406+ (TEST_COIN , 0 ),
2407+ ],
2408+ cost = 1000000 ,
2409+ ),
2410+ bundle .name (),
2411+ first_added_height = uint32 (1 ),
2412+ )
2413+ assert bundle_add_info .status == MempoolInclusionStatus .SUCCESS
2414+ invariant_check_mempool (mempool_manager .mempool )
2415+
2416+ item = mempool_manager .get_mempool_item (bundle .name ())
2417+ assert item is not None
2418+ assert item .bundle_coin_spends [singleton_spend1 .coin .name ()].eligible_for_fast_forward
2419+ assert item .bundle_coin_spends [singleton_spend2 .coin .name ()].eligible_for_fast_forward
2420+ assert not item .bundle_coin_spends [coin_spend .coin .name ()].eligible_for_fast_forward
2421+
2422+ # spend the singleton coin2 and make coin3 the latest version
2423+ coins .update_lineage (singleton_ph , singleton_spend3 .coin )
2424+ coins .spend_coin (singleton_spend2 .coin .name (), uint32 (11 ))
2425+
2426+ await advance_mempool (mempool_manager , [singleton_spend2 .coin .name ()], use_optimization = use_optimization )
2427+
2428+ # we can still fast-forward the singleton spends, the bundle should still be valid
2429+ item = mempool_manager .get_mempool_item (bundle .name ())
2430+ assert item is not None
2431+ spend = item .bundle_coin_spends [singleton_spend1 .coin .name ()]
2432+ assert spend .latest_singleton_coin == singleton_spend3 .coin .name ()
2433+ spend = item .bundle_coin_spends [singleton_spend2 .coin .name ()]
2434+ assert spend .latest_singleton_coin == singleton_spend3 .coin .name ()
2435+
2436+
2437+ @pytest .mark .anyio
2438+ @pytest .mark .parametrize ("use_optimization" , [True , False ])
2439+ async def test_advancing_ff (use_optimization : bool ) -> None :
2440+ # add a FF spend under coin1, advance it twice
2441+ # the second time we have to search for it with a linear search, because
2442+ # it's filed under the original coin
2443+
2444+ PARENT_PARENT1 = bytes32 ([4 ] * 32 )
2445+ PARENT_PARENT2 = bytes32 ([5 ] * 32 )
2446+ PARENT_PARENT3 = bytes32 ([6 ] * 32 )
2447+
2448+ # two different spends of the same singleton. both can be fast-forwarded
2449+ LAUNCHER_ID = bytes32 ([1 ] * 32 )
2450+ spend_a = make_singleton_spend (LAUNCHER_ID , PARENT_PARENT1 )
2451+ spend_b = make_singleton_spend (LAUNCHER_ID , PARENT_PARENT2 )
2452+ spend_c = make_singleton_spend (LAUNCHER_ID , PARENT_PARENT3 )
2453+
2454+ coin_spend = make_spend (
2455+ TEST_COIN ,
2456+ IDENTITY_PUZZLE ,
2457+ Program .to ([[ConditionOpcode .CREATE_COIN , IDENTITY_PUZZLE_HASH , 1336 ]]),
2458+ )
2459+ bundle = SpendBundle ([spend_a , coin_spend ], G2Element ())
2460+
2461+ # the singleton puzzle hash resulves to the most recent singleton coin, number 2
2462+ # pretend that coin1 is spent
2463+ singleton_ph = spend_a .coin .puzzle_hash
2464+ coins = TestCoins ([spend_a .coin , spend_b .coin , spend_c .coin , TEST_COIN ], {singleton_ph : spend_a .coin })
2465+
2466+ mempool_manager = await setup_mempool (coins )
2467+
2468+ bundle_add_info = await mempool_manager .add_spend_bundle (
2469+ bundle ,
2470+ make_test_conds (spend_ids = [(spend_a .coin , ELIGIBLE_FOR_FF ), (TEST_COIN , 0 )], cost = 1000000 ),
2471+ bundle .name (),
2472+ first_added_height = uint32 (1 ),
2473+ )
2474+ assert bundle_add_info .status == MempoolInclusionStatus .SUCCESS
2475+ invariant_check_mempool (mempool_manager .mempool )
2476+
2477+ item = mempool_manager .get_mempool_item (bundle .name ())
2478+ assert item is not None
2479+ spend = item .bundle_coin_spends [spend_a .coin .name ()]
2480+ assert spend .eligible_for_fast_forward
2481+ assert spend .latest_singleton_coin == spend_a .coin .name ()
2482+
2483+ coins .update_lineage (singleton_ph , spend_b .coin )
2484+ coins .spend_coin (spend_a .coin .name (), uint32 (11 ))
2485+
2486+ await advance_mempool (mempool_manager , [spend_a .coin .name ()])
2487+
2488+ item = mempool_manager .get_mempool_item (bundle .name ())
2489+ assert item is not None
2490+ spend = item .bundle_coin_spends [spend_a .coin .name ()]
2491+ assert spend .eligible_for_fast_forward
2492+ assert spend .latest_singleton_coin == spend_b .coin .name ()
2493+
2494+ coins .update_lineage (singleton_ph , spend_c .coin )
2495+ coins .spend_coin (spend_b .coin .name (), uint32 (12 ))
2496+
2497+ await advance_mempool (mempool_manager , [spend_b .coin .name ()], use_optimization = use_optimization )
2498+
2499+ item = mempool_manager .get_mempool_item (bundle .name ())
2500+ assert item is not None
2501+ spend = item .bundle_coin_spends [spend_a .coin .name ()]
2502+ assert spend .eligible_for_fast_forward
2503+ assert spend .latest_singleton_coin == spend_c .coin .name ()
0 commit comments