34
34
can_replace ,
35
35
check_removals ,
36
36
compute_assert_height ,
37
+ is_atom_canonical ,
38
+ is_clvm_canonical ,
37
39
optional_max ,
38
40
optional_min ,
39
41
)
88
90
TEST_HEIGHT = uint32 (5 )
89
91
90
92
93
+ @pytest .mark .parametrize ("clvm_hex" , ["80" , "ff8080" , "ff7f03" , "ffff8080ff8080" ])
94
+ def test_clvm_canonical (clvm_hex : str ) -> None :
95
+ clvm_buf = bytes .fromhex (clvm_hex )
96
+ assert is_clvm_canonical (clvm_buf )
97
+
98
+
99
+ @pytest .mark .parametrize (
100
+ "clvm_hex" ,
101
+ [
102
+ "fffe80" ,
103
+ "c000" ,
104
+ "c03f" ,
105
+ "e00000" ,
106
+ "e01fff" ,
107
+ "f0000000" ,
108
+ "f00fffff" ,
109
+ "f800000000" ,
110
+ "f807ffffff" ,
111
+ "fc0000000000" ,
112
+ "fc03ffffffff" ,
113
+ "fe" ,
114
+ "ff808080" ,
115
+ ],
116
+ )
117
+ def test_clvm_not_canonical (clvm_hex : str ) -> None :
118
+ clvm_buf = bytes .fromhex (clvm_hex )
119
+ assert not is_clvm_canonical (clvm_buf )
120
+
121
+
122
+ @pytest .mark .parametrize (
123
+ "clvm_hex, expect" ,
124
+ [
125
+ ("c000" , 2 + 0 ),
126
+ ("c03f" , 2 + 0x3F ),
127
+ ("e00000" , 3 + 0 ),
128
+ ("e01fff" , 3 + 0x1FFF ),
129
+ ("f0000000" , 4 + 0 ),
130
+ ("f00fffff" , 4 + 0xFFFFF ),
131
+ ("f800000000" , 5 + 0 ),
132
+ ("f807ffffff" , 5 + 0x7FFFFFF ),
133
+ ("fc0000000000" , 6 + 0 ),
134
+ ("fc03ffffffff" , 6 + 0x3FFFFFFFF ),
135
+ ],
136
+ )
137
+ def test_atom_not_canonical (clvm_hex : str , expect : int ) -> None :
138
+ clvm_buf = bytes .fromhex (clvm_hex )
139
+ atom_len , is_canonical = is_atom_canonical (clvm_buf , 0 )
140
+ assert atom_len == expect
141
+ assert not is_canonical
142
+
143
+
144
+ @pytest .mark .parametrize (
145
+ "clvm_hex, expect" ,
146
+ [
147
+ ("c040" , 2 + 0x40 ),
148
+ ("e02000" , 3 + 0x2000 ),
149
+ ("f0100000" , 4 + 0x100000 ),
150
+ ("f808000000" , 5 + 0x8000000 ),
151
+ ("fc0400000000" , 6 + 0x400000000 ),
152
+ ],
153
+ )
154
+ def test_atom_canonical (clvm_hex : str , expect : int ) -> None :
155
+ clvm_buf = bytes .fromhex (clvm_hex )
156
+ atom_len , is_canonical = is_atom_canonical (clvm_buf , 0 )
157
+ assert atom_len == expect
158
+ assert is_canonical
159
+
160
+
91
161
@dataclasses .dataclass (frozen = True )
92
162
class TestBlockRecord :
93
163
"""
@@ -760,8 +830,12 @@ def test_optional_max() -> None:
760
830
assert optional_max (uint32 (123 ), uint32 (234 )) == uint32 (234 )
761
831
762
832
763
- def mk_coin_spend (coin : Coin ) -> CoinSpend :
764
- return make_spend (coin , SerializedProgram .to (None ), SerializedProgram .to (None ))
833
+ def mk_coin_spend (coin : Coin , solution : Optional [str ] = None ) -> CoinSpend :
834
+ return make_spend (
835
+ coin ,
836
+ SerializedProgram .to (None ),
837
+ SerializedProgram .to (bytes .fromhex (solution ) if solution is not None else None ),
838
+ )
765
839
766
840
767
841
def mk_bcs (coin_spend : CoinSpend , flags : int = 0 ) -> BundleCoinSpend :
@@ -781,6 +855,7 @@ def mk_item(
781
855
assert_height : Optional [int ] = None ,
782
856
assert_before_height : Optional [int ] = None ,
783
857
assert_before_seconds : Optional [int ] = None ,
858
+ solution : Optional [str ] = None ,
784
859
flags : list [int ] = [],
785
860
) -> MempoolItem :
786
861
# we don't actually care about the puzzle and solutions for the purpose of
@@ -793,7 +868,8 @@ def mk_item(
793
868
for c , f in zip (coins , flags ):
794
869
coin_id = c .name ()
795
870
spend_ids .append ((coin_id , f ))
796
- coin_spend = mk_coin_spend (c )
871
+ coin_spend = mk_coin_spend (c , solution = solution )
872
+ solution = None
797
873
coin_spends .append (coin_spend )
798
874
bundle_coin_spends [coin_id ] = mk_bcs (coin_spend , f )
799
875
spend_bundle = SpendBundle (coin_spends , G2Element ())
@@ -1642,6 +1718,7 @@ async def get_coin_records(coin_ids: Collection[bytes32]) -> list[CoinRecord]:
1642
1718
1643
1719
mempool_manager = await instantiate_mempool_manager (get_coin_records )
1644
1720
# Create a bunch of mempool items that spend the coin in different ways
1721
+ # only the first one will be accepted
1645
1722
for i in range (3 ):
1646
1723
_ , _ , result = await generate_and_add_spendbundle (
1647
1724
mempool_manager ,
@@ -1651,10 +1728,13 @@ async def get_coin_records(coin_ids: Collection[bytes32]) -> list[CoinRecord]:
1651
1728
],
1652
1729
coin ,
1653
1730
)
1654
- assert result [1 ] == MempoolInclusionStatus .SUCCESS
1655
- assert len (list (mempool_manager .mempool .get_items_by_coin_id (coin_id ))) == 3
1656
- assert mempool_manager .mempool .size () == 3
1657
- assert len (list (mempool_manager .mempool .items_by_feerate ())) == 3
1731
+ if i == 0 :
1732
+ assert result [1 ] == MempoolInclusionStatus .SUCCESS
1733
+ else :
1734
+ assert result [1 ] == MempoolInclusionStatus .PENDING
1735
+ assert len (list (mempool_manager .mempool .get_items_by_coin_id (coin_id ))) == 1
1736
+ assert mempool_manager .mempool .size () == 1
1737
+ assert len (list (mempool_manager .mempool .items_by_feerate ())) == 1
1658
1738
# Setup a new peak where the incoming block has spent the coin
1659
1739
# Mark this coin as spent
1660
1740
test_coin_records = {coin_id : CoinRecord (coin , uint32 (0 ), TEST_HEIGHT , False , uint64 (0 ))}
@@ -1833,7 +1913,7 @@ async def make_setup_and_coins(
1833
1913
sb_ef_name = sb_ef .name ()
1834
1914
await send_to_mempool (full_node_api , sb_ef )
1835
1915
# Send also a transaction EG that spends E differently from DE and EF,
1836
- # so that it doesn't get deduplicated on E with them
1916
+ # to ensure it's rejected by the mempool
1837
1917
conditions = [
1838
1918
[ConditionOpcode .CREATE_COIN , IDENTITY_PUZZLE_HASH , e_coin .amount ],
1839
1919
[ConditionOpcode .ASSERT_MY_COIN_ID , e_coin .name ()],
@@ -1851,14 +1931,13 @@ async def make_setup_and_coins(
1851
1931
[tx_g ] = action_scope .side_effects .transactions
1852
1932
assert tx_g .spend_bundle is not None
1853
1933
sb_e2g = SpendBundle .aggregate ([sb_e2 , tx_g .spend_bundle ])
1854
- sb_e2g_name = sb_e2g .name ()
1855
- await send_to_mempool (full_node_api , sb_e2g )
1934
+ await send_to_mempool (full_node_api , sb_e2g , expecting_conflict = True )
1856
1935
1857
1936
# Make sure our coin IDs to spend bundles mappings are correct
1858
1937
assert get_sb_names_by_coin_id (full_node_api , coins [4 ].coin .name ()) == {sb_de_name }
1859
- assert get_sb_names_by_coin_id (full_node_api , e_coin_id ) == {sb_de_name , sb_ef_name , sb_e2g_name }
1938
+ assert get_sb_names_by_coin_id (full_node_api , e_coin_id ) == {sb_de_name , sb_ef_name }
1860
1939
assert get_sb_names_by_coin_id (full_node_api , coins [5 ].coin .name ()) == {sb_ef_name }
1861
- assert get_sb_names_by_coin_id (full_node_api , g_coin_id ) == { sb_e2g_name }
1940
+ assert get_sb_names_by_coin_id (full_node_api , g_coin_id ) == set ()
1862
1941
1863
1942
await farm_a_block (full_node_api , wallet_node , ph )
1864
1943
@@ -2520,7 +2599,7 @@ async def test_advancing_ff(use_optimization: bool) -> None:
2520
2599
assert spend .latest_singleton_coin == spend_c .coin .name ()
2521
2600
2522
2601
2523
- @pytest .mark .parametrize ("flags" , [ELIGIBLE_FOR_DEDUP , ELIGIBLE_FOR_FF ])
2602
+ @pytest .mark .parametrize ("flags" , [ELIGIBLE_FOR_DEDUP , ELIGIBLE_FOR_FF , ELIGIBLE_FOR_FF | ELIGIBLE_FOR_DEDUP ])
2524
2603
@pytest .mark .anyio
2525
2604
async def test_check_removals_with_block_creation (flags : int ) -> None :
2526
2605
LAUNCHER_ID = bytes32 ([1 ] * 32 )
@@ -2560,6 +2639,18 @@ async def test_check_removals_with_block_creation(flags: int) -> None:
2560
2639
assert set (removals ) == {singleton_spend .coin , TEST_COIN }
2561
2640
2562
2641
2642
+ @pytest .mark .anyio
2643
+ async def test_dedup_not_canonical () -> None :
2644
+ # this is 1, but with a non-canonical encoding
2645
+ coin_spend = mk_coin_spend (TEST_COIN , solution = "c00101" )
2646
+ coins = TestCoins (coins = [], lineage = {})
2647
+ mempool_manager = await setup_mempool (coins )
2648
+ sb = SpendBundle ([coin_spend ], G2Element ())
2649
+ sb_conds = make_test_conds (spend_ids = [(TEST_COIN , ELIGIBLE_FOR_DEDUP )])
2650
+ bundle_add_info = await mempool_manager .add_spend_bundle (sb , sb_conds , sb .name (), uint32 (1 ))
2651
+ assert bundle_add_info .status == MempoolInclusionStatus .FAILED
2652
+
2653
+
2563
2654
def make_coin_record (coin : Coin , spent_block_index : int = 0 ) -> CoinRecord :
2564
2655
return CoinRecord (coin , uint32 (0 ), uint32 (spent_block_index ), False , TEST_TIMESTAMP )
2565
2656
@@ -2626,6 +2717,21 @@ class CheckRemovalsCase:
2626
2717
conflicting_mempool_items = {TEST_COIN_ID : [mk_item ([TEST_COIN ], flags = [ELIGIBLE_FOR_DEDUP ])]},
2627
2718
expected_result = (None , []),
2628
2719
),
2720
+ CheckRemovalsCase (
2721
+ id = "Dedup coin, Dedup mempool conflict with different solution" ,
2722
+ removals = {TEST_COIN_ID : TEST_COIN_RECORD },
2723
+ bundle_coin_spends = {TEST_COIN_ID : mk_bcs (mk_coin_spend (TEST_COIN , solution = "ff8080" ), ELIGIBLE_FOR_DEDUP )},
2724
+ conflicting_mempool_items = {TEST_COIN_ID : [mk_item ([TEST_COIN ], flags = [ELIGIBLE_FOR_DEDUP ])]},
2725
+ expected_result = (
2726
+ Err .MEMPOOL_CONFLICT ,
2727
+ [
2728
+ mk_item (
2729
+ [TEST_COIN ],
2730
+ flags = [ELIGIBLE_FOR_DEDUP ],
2731
+ )
2732
+ ],
2733
+ ),
2734
+ ),
2629
2735
CheckRemovalsCase (
2630
2736
id = "Regular coin, mempool conflict" ,
2631
2737
removals = {TEST_COIN_ID : TEST_COIN_RECORD },
0 commit comments