diff --git a/chia/_tests/core/mempool/test_mempool_manager.py b/chia/_tests/core/mempool/test_mempool_manager.py index 0db312df78b1..45c80258a2a0 100644 --- a/chia/_tests/core/mempool/test_mempool_manager.py +++ b/chia/_tests/core/mempool/test_mempool_manager.py @@ -770,7 +770,16 @@ def make_test_coins() -> list[Coin]: return ret +def make_ephemeral(coins: list[Coin]) -> list[Coin]: + ret: list[Coin] = [] + for i, parent in enumerate(coins): + ret.append(Coin(parent.name(), height_hash(i + 150), uint64(i * 100))) + return ret + + coins = make_test_coins() +eph = make_ephemeral(coins) +eph2 = make_ephemeral(eph) @pytest.mark.parametrize( @@ -800,6 +809,26 @@ def make_test_coins() -> list[Coin]: ([mk_item(coins[0:2])], mk_item(coins[0:2], fee=10000000), True), # or if we spend the same coins with additional coins ([mk_item(coins[0:2])], mk_item(coins[0:3], fee=10000000), True), + # SUPERSET RULE WITH EPHEMERAL COINS + # the super set rule only takes non-ephemeral coins into account. The + # ephmeral coins depend on how we spend, and might prevent legitimate + # replace-by-fee attempts. + # replace a spend that includes an ephemeral coin with one that doesn't + ([mk_item(coins[0:2] + eph[0:1])], mk_item(coins[0:2], fee=10000000), True), + # replace a spend with two-levels of ephemeral coins, with one that + # only has 1-level + ([mk_item(coins[0:2] + eph[0:1] + eph2[0:1])], mk_item(coins[0:2] + eph[0:1], fee=10000000), True), + # replace a spend with two-levels of ephemeral coins, with one that + # doesn't + ([mk_item(coins[0:2] + eph[0:1] + eph2[0:1])], mk_item(coins[0:2], fee=10000000), True), + # replace a spend with two-levels of ephemeral coins, with one that + # has *different* ephemeral coins + ([mk_item(coins[0:2] + eph[0:1] + eph2[0:1])], mk_item(coins[0:2] + eph[1:2] + eph2[1:2], fee=10000000), True), + # it's OK to add new ephemeral spends + ([mk_item(coins[0:2])], mk_item(coins[0:2] + eph[1:2] + eph2[1:2], fee=10000000), True), + # eph2[0:1] is not an ephemeral coin here, this violates the superset + # rule. eph[0:1] is missing for that + ([mk_item(coins[0:2] + eph2[0:1])], mk_item(coins[0:2] + eph[1:2] + eph2[1:2], fee=10000000), False), # FEE- AND FEE RATE RULES # if we're replacing two items, each paying a fee of 100, we need to # spend (at least) the same coins and pay at least 10000000 higher fee diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index 6a627c08cb94..ddb7c9e315ee 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -455,7 +455,7 @@ async def validate_spend_bundle( ) if removal_names != removal_names_from_coin_spends: - # If you reach here it's probably because your program reveal doesn't match the coin's puzzle hash + # If you reach here it's probably because your puzzle reveal doesn't match the coin's puzzle hash return Err.INVALID_SPEND_BUNDLE, None, [] removal_record_dict: dict[bytes32, CoinRecord] = {} @@ -806,8 +806,13 @@ def can_replace( # bundle with AB with a higher fee. An attacker then replaces the bundle with just B with a higher # fee than AB therefore kicking out A altogether. The better way to solve this would be to keep a cache # of booted transactions like A, and retry them after they get removed from mempool due to a conflict. - for coin in item.removals: - if coin.name() not in removal_names: + conflicting_removals = {c.name(): c for c in item.removals} + for coin in conflicting_removals.values(): + coin_name = coin.name() + # if the parent of this coin is one of the spends in this + # transaction, it means it's an ephemeral coin spend. Such spends + # are not considered by the superset rule + if coin_name not in removal_names and coin.parent_coin_info not in conflicting_removals: log.debug(f"Rejecting conflicting tx as it does not spend conflicting coin {coin.name()}") return False