|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
3 | 3 | import asyncio
|
| 4 | +import copy |
4 | 5 | import logging
|
5 | 6 | import random
|
6 | 7 | import time
|
7 | 8 | from collections.abc import AsyncIterator, Awaitable
|
8 | 9 | from contextlib import asynccontextmanager
|
9 |
| -from dataclasses import replace |
| 10 | +from dataclasses import dataclass, replace |
10 | 11 | from typing import Optional
|
11 | 12 |
|
12 | 13 | import pytest
|
|
20 | 21 | InfusedChallengeChainSubSlot,
|
21 | 22 | MerkleSet,
|
22 | 23 | SpendBundle,
|
| 24 | + SpendBundleConditions, |
| 25 | + SpendConditions, |
23 | 26 | TransactionsInfo,
|
24 | 27 | UnfinishedBlock,
|
25 | 28 | is_canonical_serialization,
|
|
38 | 41 | from chia._tests.util.blockchain import create_blockchain
|
39 | 42 | from chia._tests.util.get_name_puzzle_conditions import get_name_puzzle_conditions
|
40 | 43 | from chia.consensus.augmented_chain import AugmentedBlockchain
|
41 |
| -from chia.consensus.block_body_validation import ForkInfo |
| 44 | +from chia.consensus.block_body_validation import ForkAdd, ForkInfo |
42 | 45 | from chia.consensus.block_header_validation import validate_finished_header_block
|
43 | 46 | from chia.consensus.block_rewards import calculate_base_farmer_reward
|
44 | 47 | from chia.consensus.blockchain import AddBlockResult, Blockchain
|
@@ -4235,3 +4238,175 @@ async def test_get_header_blocks_in_range_tx_filter_non_tx_block(empty_blockchai
|
4235 | 4238 | blocks_with_filter = await b.get_header_blocks_in_range(0, 42, tx_filter=True)
|
4236 | 4239 | empty_tx_filter = b"\x00"
|
4237 | 4240 | assert blocks_with_filter[non_tx_block.header_hash].transactions_filter == empty_tx_filter
|
| 4241 | + |
| 4242 | + |
| 4243 | +@dataclass(frozen=True) |
| 4244 | +class ForkInfoTestSetup: |
| 4245 | + fork_info: ForkInfo |
| 4246 | + initial_additions_since_fork: dict[bytes32, ForkAdd] |
| 4247 | + test_block: FullBlock |
| 4248 | + coin: Coin |
| 4249 | + child_coin: Coin |
| 4250 | + |
| 4251 | + @classmethod |
| 4252 | + def create(cls, same_ph_as_parent: bool, same_amount_as_parent: bool) -> ForkInfoTestSetup: |
| 4253 | + from chia._tests.util.network_protocol_data import full_block as test_block |
| 4254 | + |
| 4255 | + unrelated_coin = Coin(bytes32([0] * 32), bytes32([1] * 32), uint64(42)) |
| 4256 | + # We add this initial state with an unrelated addition, to create a |
| 4257 | + # difference between the `rollback` state and the completely empty |
| 4258 | + # `reset` state. |
| 4259 | + initial_additions_since_fork = { |
| 4260 | + unrelated_coin.name(): ForkAdd( |
| 4261 | + coin=unrelated_coin, |
| 4262 | + confirmed_height=uint32(1), |
| 4263 | + timestamp=uint64(0), |
| 4264 | + hint=None, |
| 4265 | + is_coinbase=False, |
| 4266 | + same_as_parent=False, |
| 4267 | + ) |
| 4268 | + } |
| 4269 | + fork_info = ForkInfo( |
| 4270 | + test_block.height - 1, |
| 4271 | + test_block.height - 1, |
| 4272 | + test_block.prev_header_hash, |
| 4273 | + additions_since_fork=copy.copy(initial_additions_since_fork), |
| 4274 | + ) |
| 4275 | + puzzle_hash = bytes32([2] * 32) |
| 4276 | + amount = uint64(1337) |
| 4277 | + coin = Coin(bytes32([3] * 32), puzzle_hash, amount) |
| 4278 | + child_coin_ph = puzzle_hash if same_ph_as_parent else bytes32([4] * 32) |
| 4279 | + child_coin_amount = amount if same_amount_as_parent else uint64(0) |
| 4280 | + child_coin = Coin(coin.name(), child_coin_ph, child_coin_amount) |
| 4281 | + return cls( |
| 4282 | + fork_info=fork_info, |
| 4283 | + initial_additions_since_fork=initial_additions_since_fork, |
| 4284 | + test_block=test_block, |
| 4285 | + coin=coin, |
| 4286 | + child_coin=child_coin, |
| 4287 | + ) |
| 4288 | + |
| 4289 | + def check_additions(self, expected_same_parent_additions: set[bytes32]) -> None: |
| 4290 | + assert all( |
| 4291 | + a in self.fork_info.additions_since_fork and self.fork_info.additions_since_fork[a].same_as_parent |
| 4292 | + for a in expected_same_parent_additions |
| 4293 | + ) |
| 4294 | + remaining_additions = set(self.fork_info.additions_since_fork) - expected_same_parent_additions |
| 4295 | + assert not any(self.fork_info.additions_since_fork[a].same_as_parent for a in remaining_additions) |
| 4296 | + |
| 4297 | + |
| 4298 | +@pytest.mark.parametrize("same_ph_as_parent", [True, False]) |
| 4299 | +@pytest.mark.parametrize("same_amount_as_parent", [True, False]) |
| 4300 | +@pytest.mark.parametrize("rollback", [True, False]) |
| 4301 | +@pytest.mark.parametrize("reset", [True, False]) |
| 4302 | +@pytest.mark.anyio |
| 4303 | +async def test_include_spends_same_as_parent( |
| 4304 | + same_ph_as_parent: bool, same_amount_as_parent: bool, rollback: bool, reset: bool |
| 4305 | +) -> None: |
| 4306 | + """ |
| 4307 | + Tests that `ForkInfo` properly tracks same-as-parent created coins. |
| 4308 | + A created coin is tracked as such if its puzzle hash and amount match the |
| 4309 | + parent. We're covering here `include_spends`, `rollback` and `reset` in the |
| 4310 | + context of same-as-parent coins. |
| 4311 | + """ |
| 4312 | + test_setup = ForkInfoTestSetup.create(same_ph_as_parent, same_amount_as_parent) |
| 4313 | + # Now let's prepare the test spend bundle conditions |
| 4314 | + create_coin = [(test_setup.child_coin.puzzle_hash, test_setup.child_coin.amount, None)] |
| 4315 | + conds = SpendBundleConditions( |
| 4316 | + [ |
| 4317 | + SpendConditions( |
| 4318 | + test_setup.coin.name(), |
| 4319 | + test_setup.coin.parent_coin_info, |
| 4320 | + test_setup.coin.puzzle_hash, |
| 4321 | + test_setup.coin.amount, |
| 4322 | + None, |
| 4323 | + None, |
| 4324 | + None, |
| 4325 | + None, |
| 4326 | + None, |
| 4327 | + None, |
| 4328 | + create_coin, |
| 4329 | + [], |
| 4330 | + [], |
| 4331 | + [], |
| 4332 | + [], |
| 4333 | + [], |
| 4334 | + [], |
| 4335 | + [], |
| 4336 | + 0, |
| 4337 | + 0, |
| 4338 | + 0, |
| 4339 | + ) |
| 4340 | + ], |
| 4341 | + 0, |
| 4342 | + 0, |
| 4343 | + 0, |
| 4344 | + None, |
| 4345 | + None, |
| 4346 | + [], |
| 4347 | + 0, |
| 4348 | + 0, |
| 4349 | + 0, |
| 4350 | + True, |
| 4351 | + 0, |
| 4352 | + 0, |
| 4353 | + ) |
| 4354 | + # Now let's run the test |
| 4355 | + test_setup.fork_info.include_spends(conds, test_setup.test_block, test_setup.test_block.header_hash) |
| 4356 | + # Let's make sure the results are as expected |
| 4357 | + expected_same_parent_additions = ( |
| 4358 | + {test_setup.child_coin.name()} if same_ph_as_parent and same_amount_as_parent else set() |
| 4359 | + ) |
| 4360 | + test_setup.check_additions(expected_same_parent_additions) |
| 4361 | + if rollback: |
| 4362 | + # Now we rollback before the spend that belongs to the test conditions |
| 4363 | + test_setup.fork_info.rollback(test_setup.test_block.prev_header_hash, test_setup.test_block.height - 1) |
| 4364 | + # That should leave only the initial additions we started with, which |
| 4365 | + # are unrelated to the test conditions. We added this initial state to |
| 4366 | + # create a difference between `rollback` state and the completely empty |
| 4367 | + # `reset` state. |
| 4368 | + assert test_setup.fork_info.additions_since_fork == test_setup.initial_additions_since_fork |
| 4369 | + if reset: |
| 4370 | + # Now we reset to a test height and header hash |
| 4371 | + test_setup.fork_info.reset(1, bytes32([0] * 32)) |
| 4372 | + # That should leave this empty |
| 4373 | + assert test_setup.fork_info.additions_since_fork == {} |
| 4374 | + |
| 4375 | + |
| 4376 | +@pytest.mark.parametrize("same_ph_as_parent", [True, False]) |
| 4377 | +@pytest.mark.parametrize("same_amount_as_parent", [True, False]) |
| 4378 | +@pytest.mark.parametrize("rollback", [True, False]) |
| 4379 | +@pytest.mark.parametrize("reset", [True, False]) |
| 4380 | +@pytest.mark.anyio |
| 4381 | +async def test_include_block_same_as_parent_coins( |
| 4382 | + same_ph_as_parent: bool, same_amount_as_parent: bool, rollback: bool, reset: bool |
| 4383 | +) -> None: |
| 4384 | + """ |
| 4385 | + Tests that `ForkInfo` properly tracks same-as-parent created coins. |
| 4386 | + A created coin is tracked as such if its puzzle hash and amount match the |
| 4387 | + parent. We're covering here `include_block`, `rollback` and `reset` in the |
| 4388 | + context of such coins. |
| 4389 | + """ |
| 4390 | + test_setup = ForkInfoTestSetup.create(same_ph_as_parent, same_amount_as_parent) |
| 4391 | + # Now let's run the test |
| 4392 | + test_setup.fork_info.include_block( |
| 4393 | + [(test_setup.child_coin, None)], |
| 4394 | + [(test_setup.coin.name(), test_setup.coin)], |
| 4395 | + test_setup.test_block, |
| 4396 | + test_setup.test_block.header_hash, |
| 4397 | + ) |
| 4398 | + # Let's make sure the results are as expected |
| 4399 | + expected_same_as_parent_additions = ( |
| 4400 | + {test_setup.child_coin.name()} if same_ph_as_parent and same_amount_as_parent else set() |
| 4401 | + ) |
| 4402 | + test_setup.check_additions(expected_same_as_parent_additions) |
| 4403 | + if rollback: |
| 4404 | + # Now we rollback before the spend that belongs to the test conditions |
| 4405 | + test_setup.fork_info.rollback(test_setup.test_block.prev_header_hash, test_setup.test_block.height - 1) |
| 4406 | + # That should leave only the initial additions we started with |
| 4407 | + assert test_setup.fork_info.additions_since_fork == test_setup.initial_additions_since_fork |
| 4408 | + if reset: |
| 4409 | + # Now we reset to a test height and header hash |
| 4410 | + test_setup.fork_info.reset(1, bytes32([0] * 32)) |
| 4411 | + # That should leave this empty |
| 4412 | + assert test_setup.fork_info.additions_since_fork == {} |
0 commit comments