|
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,
|
| 28 | + is_canonical_serialization, |
25 | 29 | )
|
26 | 30 | from chia_rs.sized_bytes import bytes32
|
27 | 31 | from chia_rs.sized_ints import uint8, uint32, uint64
|
28 |
| -from clvm.casts import int_to_bytes |
29 | 32 |
|
30 | 33 | from chia._tests.blockchain.blockchain_test_utils import (
|
31 | 34 | _validate_and_add_block,
|
|
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
|
|
61 | 64 | from chia.types.condition_with_args import ConditionWithArgs
|
62 | 65 | from chia.types.generator_types import BlockGenerator
|
63 | 66 | from chia.types.validation_state import ValidationState
|
| 67 | +from chia.util.casts import int_to_bytes |
64 | 68 | from chia.util.errors import Err
|
65 | 69 | from chia.util.hash import std_hash
|
66 | 70 | from chia.util.keychain import Keychain
|
@@ -3648,6 +3652,41 @@ async def test_get_blocks_at(self, empty_blockchain: Blockchain, default_1000_bl
|
3648 | 3652 | assert len(blocks) == 200
|
3649 | 3653 | assert blocks[-1].height == 199
|
3650 | 3654 |
|
| 3655 | + @pytest.mark.anyio |
| 3656 | + async def test_overlong_generator_encoding( |
| 3657 | + self, empty_blockchain: Blockchain, bt: BlockTools, consensus_mode: ConsensusMode |
| 3658 | + ) -> None: |
| 3659 | + # add enough blocks to pass the hard fork |
| 3660 | + blocks = bt.get_consecutive_blocks(10) |
| 3661 | + for b in blocks[:-1]: |
| 3662 | + await _validate_and_add_block(empty_blockchain, b) |
| 3663 | + |
| 3664 | + while not blocks[-1].is_transaction_block(): |
| 3665 | + await _validate_and_add_block(empty_blockchain, blocks[-1]) |
| 3666 | + blocks = bt.get_consecutive_blocks(1, block_list_input=blocks) |
| 3667 | + original_block: FullBlock = blocks[-1] |
| 3668 | + |
| 3669 | + # overlong encoding |
| 3670 | + generator = SerializedProgram.fromhex("c00101") |
| 3671 | + assert not is_canonical_serialization(bytes(generator)) |
| 3672 | + |
| 3673 | + block = recursive_replace(original_block, "transactions_generator", generator) |
| 3674 | + block = recursive_replace(block, "transactions_info.generator_root", std_hash(bytes(generator))) |
| 3675 | + block = recursive_replace( |
| 3676 | + block, "foliage_transaction_block.transactions_info_hash", std_hash(bytes(block.transactions_info)) |
| 3677 | + ) |
| 3678 | + block = recursive_replace( |
| 3679 | + block, "foliage.foliage_transaction_block_hash", std_hash(bytes(block.foliage_transaction_block)) |
| 3680 | + ) |
| 3681 | + |
| 3682 | + # overlong encoding became invalid in the 3.0 hard fork |
| 3683 | + if consensus_mode == ConsensusMode.HARD_FORK_3_0: |
| 3684 | + expected_error = Err.INVALID_TRANSACTIONS_GENERATOR_ENCODING |
| 3685 | + else: |
| 3686 | + expected_error = None |
| 3687 | + |
| 3688 | + await _validate_and_add_block(empty_blockchain, block, expected_error=expected_error, skip_prevalidation=True) |
| 3689 | + |
3651 | 3690 |
|
3652 | 3691 | @pytest.mark.anyio
|
3653 | 3692 | async def test_reorg_new_ref(empty_blockchain: Blockchain, bt: BlockTools) -> None:
|
@@ -4199,3 +4238,175 @@ async def test_get_header_blocks_in_range_tx_filter_non_tx_block(empty_blockchai
|
4199 | 4238 | blocks_with_filter = await b.get_header_blocks_in_range(0, 42, tx_filter=True)
|
4200 | 4239 | empty_tx_filter = b"\x00"
|
4201 | 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