Files
chia-blockchain/chia/_tests/wallet/sync/test_wallet_sync.py
T
Arvid NorbergandGitHub 4576684c14 integrate PLOT_V1_PHASE_OUT constant (#19820)
* integrate PLOT_V1_PHASE_OUT constant

* current test chains won't work after the plot v1 phase-out
2025-07-21 08:59:16 -07:00

1776 lines
82 KiB
Python

from __future__ import annotations
import asyncio
import contextlib
import functools
import logging
from collections.abc import Awaitable
from dataclasses import dataclass
from typing import Callable, Optional
from unittest.mock import MagicMock
import pytest
from aiosqlite import Error as AIOSqliteError
from chia_rs import (
BlockRecord,
CoinState,
ConsensusConstants,
FullBlock,
G2Element,
SpendBundle,
confirm_not_included_already_hashed,
)
from chia_rs.sized_bytes import bytes32
from chia_rs.sized_ints import uint32, uint64, uint128
from chiabip158 import PyBIP158
from colorlog import getLogger
from chia._tests.conftest import ConsensusMode
from chia._tests.connection_utils import connect_and_get_peer, disconnect_all, disconnect_all_and_reconnect
from chia._tests.util.blockchain_mock import BlockchainMock
from chia._tests.util.misc import patch_request_handler, wallet_height_at_least
from chia._tests.util.setup_nodes import OldSimulatorsAndWallets
from chia._tests.util.time_out_assert import time_out_assert, time_out_assert_not_none
from chia._tests.weight_proof.test_weight_proof import load_blocks_dont_validate
from chia.consensus.augmented_chain import AugmentedBlockchain
from chia.consensus.block_body_validation import ForkInfo
from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward
from chia.consensus.difficulty_adjustment import get_next_sub_slot_iters_and_difficulty
from chia.full_node.full_node_api import FullNodeAPI
from chia.full_node.weight_proof import WeightProofHandler
from chia.protocols import full_node_protocol, wallet_protocol
from chia.protocols.outbound_message import Message, make_msg
from chia.protocols.protocol_message_types import ProtocolMessageTypes
from chia.protocols.shared_protocol import Capability
from chia.protocols.wallet_protocol import (
RequestAdditions,
RespondAdditions,
RespondBlockHeader,
RespondBlockHeaders,
SendTransaction,
)
from chia.server.server import ChiaServer
from chia.server.ws_connection import WSChiaConnection
from chia.simulator.add_blocks_in_batches import add_blocks_in_batches
from chia.simulator.block_tools import BlockTools
from chia.simulator.full_node_simulator import FullNodeSimulator
from chia.simulator.simulator_protocol import FarmNewBlockProtocol
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.serialized_program import SerializedProgram
from chia.types.coin_spend import make_spend
from chia.types.condition_opcodes import ConditionOpcode
from chia.types.peer_info import PeerInfo
from chia.types.validation_state import ValidationState
from chia.util.hash import std_hash
from chia.wallet.conditions import CreateCoin
from chia.wallet.nft_wallet.nft_wallet import NFTWallet
from chia.wallet.util.compute_memos import compute_memos
from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG
from chia.wallet.util.wallet_sync_utils import PeerRequestException
from chia.wallet.util.wallet_types import WalletIdentifier
from chia.wallet.wallet_state_manager import WalletStateManager
from chia.wallet.wallet_weight_proof_handler import get_wp_fork_point
async def get_tx_count(wsm: WalletStateManager, wallet_id: int) -> int:
txs = await wsm.get_all_transactions(wallet_id)
return len(txs)
async def get_nft_count(wallet: NFTWallet) -> int:
return await wallet.get_nft_count()
log = getLogger(__name__)
pytestmark = pytest.mark.standard_block_tools
@pytest.mark.limit_consensus_modes(reason="save time")
@pytest.mark.anyio
async def test_request_block_headers(
simulator_and_wallet: OldSimulatorsAndWallets, default_400_blocks: list[FullBlock]
) -> None:
# Tests the edge case of receiving funds right before the recent blocks in weight proof
[full_node_api], [(wallet_node, _)], bt = simulator_and_wallet
wallet = wallet_node.wallet_state_manager.main_wallet
async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
ph = await action_scope.get_puzzle_hash(wallet.wallet_state_manager)
await add_blocks_in_batches(default_400_blocks[:100], full_node_api.full_node)
msg = await full_node_api.request_block_headers(wallet_protocol.RequestBlockHeaders(uint32(10), uint32(15), False))
assert msg is not None
assert msg.type == ProtocolMessageTypes.respond_block_headers.value
res_block_headers = RespondBlockHeaders.from_bytes(msg.data)
bh = res_block_headers.header_blocks
assert len(bh) == 6
assert [x.reward_chain_block.height for x in default_400_blocks[10:16]] == [x.reward_chain_block.height for x in bh]
assert [x.foliage for x in default_400_blocks[10:16]] == [x.foliage for x in bh]
assert [x.transactions_filter for x in bh] == [b"\x00"] * 6
num_blocks = 20
new_blocks = bt.get_consecutive_blocks(num_blocks, block_list_input=default_400_blocks, pool_reward_puzzle_hash=ph)
await add_blocks_in_batches(new_blocks, full_node_api.full_node)
msg = await full_node_api.request_block_headers(wallet_protocol.RequestBlockHeaders(uint32(110), uint32(115), True))
assert msg is not None
res_block_headers = RespondBlockHeaders.from_bytes(msg.data)
bh = res_block_headers.header_blocks
assert len(bh) == 6
@pytest.mark.limit_consensus_modes(reason="save time")
@pytest.mark.anyio
@pytest.mark.parametrize("rewards_only_tx_block", [True, False])
async def test_request_block_headers_transactions_filter(
one_node_one_block: tuple[FullNodeSimulator, ChiaServer, BlockTools], rewards_only_tx_block: bool
) -> None:
"""
Tests that `request_block_headers` returns a transactions filter that
correctly reflects the blocks transactions.
We use `rewards_only_tx_block` to control whether the test transaction
block contains our test spend as well, or just the reward coins.
For completeness, we're also comparing the outcome of
`request_block_headers` in this regard, to `request_header_blocks` as
well as `request_block_header`.
"""
full_node_api, _, bt = one_node_one_block
ph = SerializedProgram.to(1).get_tree_hash()
for _ in range(2):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
if rewards_only_tx_block:
# Generate a transaction block without any spends
sb = None
else:
# Generate a transaction block with our test spend
coins = await full_node_api.full_node.coin_store.get_coin_records_by_puzzle_hash(False, ph)
[parent_coin] = [c.coin for c in coins if c.coin.amount == 250_000_000_000]
sb = SpendBundle(
[
make_spend(
parent_coin, SerializedProgram.to(1), SerializedProgram.to([[ConditionOpcode.CREATE_COIN, ph, 42]])
)
],
G2Element(),
)
blocks = await full_node_api.get_all_full_blocks()
blocks = bt.get_consecutive_blocks(1, blocks, guarantee_transaction_block=True, transaction_data=sb)
new_block = blocks[-1]
await full_node_api.full_node.add_block(new_block)
# Compute the expected transactions filter
if rewards_only_tx_block:
byte_array_tx = [bytearray(coin.puzzle_hash) for coin in new_block.get_included_reward_coins()]
else:
assert sb is not None
[test_spend] = sb.additions()
byte_array_tx = (
[bytearray(test_spend.puzzle_hash)]
+ [bytearray(coin.puzzle_hash) for coin in new_block.get_included_reward_coins()]
+ [bytearray(parent_coin.name())]
)
expected_transactions_filter = bytes(PyBIP158(byte_array_tx).GetEncoded())
# Perform the request and check the transactions filter
msg = await full_node_api.request_block_headers(
wallet_protocol.RequestBlockHeaders(uint32(new_block.height), uint32(new_block.height), True)
)
assert msg is not None
res_block_headers = RespondBlockHeaders.from_bytes(msg.data)
block_headers = res_block_headers.header_blocks
assert len(block_headers) == 1
block_header = block_headers[0]
assert block_header.transactions_filter == expected_transactions_filter
# Go further and compare this to the outcome of request_header_blocks
msg = await full_node_api.request_header_blocks(
wallet_protocol.RequestHeaderBlocks(uint32(new_block.height), uint32(new_block.height))
)
assert msg is not None
block_headers_res = RespondBlockHeaders.from_bytes(msg.data)
assert block_headers_res.header_blocks == block_headers
assert block_headers_res.header_blocks[0].transactions_filter == expected_transactions_filter
# Go even further and compare this to the outcome of request_block_header
msg = await full_node_api.request_block_header(wallet_protocol.RequestBlockHeader(uint32(new_block.height)))
assert msg is not None
block_header_res = RespondBlockHeader.from_bytes(msg.data)
assert block_header_res.header_block == block_header
assert block_header_res.header_block.transactions_filter == expected_transactions_filter
# @pytest.mark.parametrize(
# "test_case",
# [(1_000_000, 10_000_010, False, ProtocolMessageTypes.reject_block_headers)],
# [(80, 99, False, ProtocolMessageTypes.respond_block_headers)],
# [(10, 8, False, None)],
# )
@pytest.mark.anyio
async def test_request_block_headers_rejected(
simulator_and_wallet: OldSimulatorsAndWallets, default_400_blocks: list[FullBlock]
) -> None:
# Tests the edge case of receiving funds right before the recent blocks in weight proof
[full_node_api], _, _ = simulator_and_wallet
# start_height, end_height, return_filter, expected_res = test_case
msg = await full_node_api.request_block_headers(
wallet_protocol.RequestBlockHeaders(uint32(1_000_000), uint32(1_000_010), False)
)
assert msg is not None
assert msg.type == ProtocolMessageTypes.reject_block_headers.value
await add_blocks_in_batches(default_400_blocks[:150], full_node_api.full_node)
msg = await full_node_api.request_block_headers(wallet_protocol.RequestBlockHeaders(uint32(80), uint32(99), False))
assert msg is not None
assert msg.type == ProtocolMessageTypes.respond_block_headers.value
msg = await full_node_api.request_block_headers(wallet_protocol.RequestBlockHeaders(uint32(10), uint32(8), False))
assert msg is not None
assert msg.type == ProtocolMessageTypes.reject_block_headers.value
msg = await full_node_api.request_block_headers(wallet_protocol.RequestBlockHeaders(uint32(10), uint32(8), True))
assert msg is not None
assert msg.type == ProtocolMessageTypes.reject_block_headers.value
# test for 128 blocks to fetch at once limit
msg = await full_node_api.request_block_headers(wallet_protocol.RequestBlockHeaders(uint32(10), uint32(140), True))
assert msg is not None
assert msg.type == ProtocolMessageTypes.reject_block_headers.value
msg = await full_node_api.request_block_headers(wallet_protocol.RequestBlockHeaders(uint32(90), uint32(160), False))
assert msg is not None
assert msg.type == ProtocolMessageTypes.reject_block_headers.value
msg = await full_node_api.request_block_headers(wallet_protocol.RequestBlockHeaders(uint32(90), uint32(160), True))
assert msg is not None
assert msg.type == ProtocolMessageTypes.reject_block_headers.value
@pytest.mark.parametrize(
"two_wallet_nodes",
[dict(disable_capabilities=[Capability.BLOCK_HEADERS]), dict(disable_capabilities=[Capability.BASE])],
indirect=True,
)
@pytest.mark.limit_consensus_modes(reason="save time")
@pytest.mark.anyio
async def test_basic_sync_wallet(
two_wallet_nodes: OldSimulatorsAndWallets,
default_400_blocks: list[FullBlock],
self_hostname: str,
use_delta_sync: bool,
) -> None:
[full_node_api], wallets, bt = two_wallet_nodes
full_node = full_node_api.full_node
full_node_server = full_node.server
# Trusted node sync
wallets[0][0].config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()}
wallets[0][0].config["use_delta_sync"] = use_delta_sync
# Untrusted node sync
wallets[1][0].config["trusted_peers"] = {}
wallets[1][0].config["use_delta_sync"] = use_delta_sync
await add_blocks_in_batches(default_400_blocks, full_node)
for wallet_node, wallet_server in wallets:
await wallet_server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
for wallet_node, wallet_server in wallets:
await time_out_assert(100, wallet_height_at_least, True, wallet_node, len(default_400_blocks) - 1)
# Tests a reorg with the wallet
num_blocks = 30
blocks_reorg = bt.get_consecutive_blocks(num_blocks - 1, block_list_input=default_400_blocks[:-5])
blocks_reorg = bt.get_consecutive_blocks(1, blocks_reorg, guarantee_transaction_block=True, current_time=True)
await add_blocks_in_batches(blocks_reorg[1:], full_node)
for wallet_node, wallet_server in wallets:
await time_out_assert(
100, wallet_height_at_least, True, wallet_node, len(default_400_blocks) + num_blocks - 5 - 1
)
await time_out_assert(20, wallet_node.wallet_state_manager.synced)
await disconnect_all(wallet_server)
assert not (await wallet_node.wallet_state_manager.synced())
@pytest.mark.parametrize(
"two_wallet_nodes",
[dict(disable_capabilities=[Capability.BLOCK_HEADERS]), dict(disable_capabilities=[Capability.BASE])],
indirect=True,
)
@pytest.mark.limit_consensus_modes(reason="save time")
@pytest.mark.anyio
async def test_almost_recent(
two_wallet_nodes: OldSimulatorsAndWallets,
default_400_blocks: list[FullBlock],
self_hostname: str,
blockchain_constants: ConsensusConstants,
use_delta_sync: bool,
) -> None:
# Tests the edge case of receiving funds right before the recent blocks in weight proof
[full_node_api], wallets, bt = two_wallet_nodes
full_node = full_node_api.full_node
full_node_server = full_node.server
# Trusted node sync
wallets[0][0].config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()}
wallets[0][0].config["use_delta_sync"] = use_delta_sync
# Untrusted node sync
wallets[1][0].config["trusted_peers"] = {}
wallets[1][0].config["use_delta_sync"] = use_delta_sync
base_num_blocks = 400
await add_blocks_in_batches(default_400_blocks, full_node)
all_blocks = default_400_blocks
both_phs = []
for wallet_node, wallet_server in wallets:
wallet = wallet_node.wallet_state_manager.main_wallet
async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
ph = await action_scope.get_puzzle_hash(wallet.wallet_state_manager)
both_phs.append(ph)
for i in range(20):
# Tests a reorg with the wallet
ph = both_phs[i % 2]
all_blocks = bt.get_consecutive_blocks(1, block_list_input=all_blocks, pool_reward_puzzle_hash=ph)
await full_node.add_block(all_blocks[-1])
new_blocks = bt.get_consecutive_blocks(
blockchain_constants.WEIGHT_PROOF_RECENT_BLOCKS + 10, block_list_input=all_blocks
)
await add_blocks_in_batches(new_blocks[base_num_blocks + 20 :], full_node)
for wallet_node, wallet_server in wallets:
wallet = wallet_node.wallet_state_manager.main_wallet
await wallet_server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
await time_out_assert(30, wallet.get_confirmed_balance, 10 * calculate_pool_reward(uint32(1000)))
@pytest.mark.anyio
async def test_backtrack_sync_wallet(
two_wallet_nodes: OldSimulatorsAndWallets,
default_400_blocks: list[FullBlock],
self_hostname: str,
use_delta_sync: bool,
) -> None:
full_nodes, wallets, _ = two_wallet_nodes
full_node_api = full_nodes[0]
full_node_server = full_node_api.full_node.server
# Trusted node sync
wallets[0][0].config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()}
wallets[0][0].config["use_delta_sync"] = use_delta_sync
# Untrusted node sync
wallets[1][0].config["trusted_peers"] = {}
wallets[1][0].config["use_delta_sync"] = use_delta_sync
for block in default_400_blocks[:20]:
await full_node_api.full_node.add_block(block)
for wallet_node, wallet_server in wallets:
await wallet_server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
for wallet_node, wallet_server in wallets:
await time_out_assert(100, wallet_height_at_least, True, wallet_node, 19)
# Tests a reorg with the wallet
@pytest.mark.anyio
async def test_short_batch_sync_wallet(
two_wallet_nodes: OldSimulatorsAndWallets,
default_400_blocks: list[FullBlock],
self_hostname: str,
use_delta_sync: bool,
) -> None:
[full_node_api], wallets, _ = two_wallet_nodes
full_node = full_node_api.full_node
full_node_server = full_node.server
# Trusted node sync
wallets[0][0].config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()}
wallets[0][0].config["use_delta_sync"] = use_delta_sync
# Untrusted node sync
wallets[1][0].config["trusted_peers"] = {}
wallets[1][0].config["use_delta_sync"] = use_delta_sync
await add_blocks_in_batches(default_400_blocks[:200], full_node)
for wallet_node, wallet_server in wallets:
await wallet_server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
for wallet_node, wallet_server in wallets:
await time_out_assert(100, wallet_height_at_least, True, wallet_node, 199)
@pytest.mark.limit_consensus_modes(reason="save time")
@pytest.mark.anyio
async def test_long_sync_wallet(
two_wallet_nodes: OldSimulatorsAndWallets,
default_1000_blocks: list[FullBlock],
default_400_blocks: list[FullBlock],
self_hostname: str,
use_delta_sync: bool,
) -> None:
[full_node_api], wallets, bt = two_wallet_nodes
full_node = full_node_api.full_node
full_node_server = full_node.server
# Trusted node sync
wallets[0][0].config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()}
wallets[0][0].config["use_delta_sync"] = use_delta_sync
# Untrusted node sync
wallets[1][0].config["trusted_peers"] = {}
wallets[1][0].config["use_delta_sync"] = use_delta_sync
await add_blocks_in_batches(default_400_blocks, full_node)
for wallet_node, wallet_server in wallets:
await wallet_server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
for wallet_node, wallet_server in wallets:
await time_out_assert(600, wallet_height_at_least, True, wallet_node, len(default_400_blocks) - 1)
# Tests a long reorg
await add_blocks_in_batches(default_1000_blocks, full_node)
# ony the wallet with untrusted sync needs to reconnect
await disconnect_all_and_reconnect(wallets[1][1], full_node_server, self_hostname)
for wallet_node, wallet_server in wallets:
log.info(f"wallet node height is {await wallet_node.wallet_state_manager.blockchain.get_finished_sync_up_to()}")
await time_out_assert(200, wallet_height_at_least, True, wallet_node, len(default_1000_blocks) - 1)
# Tests a short reorg
num_blocks = 30
blocks_reorg = bt.get_consecutive_blocks(num_blocks, block_list_input=default_1000_blocks[:-5])
block_record = await full_node.blockchain.get_block_record_from_db(blocks_reorg[-num_blocks - 10].header_hash)
sub_slot_iters, difficulty = get_next_sub_slot_iters_and_difficulty(
full_node.constants, True, block_record, full_node.blockchain
)
fork_height = blocks_reorg[-num_blocks - 10].height - 1
blockchain = AugmentedBlockchain(full_node.blockchain)
await full_node.add_block_batch(
blocks_reorg[-num_blocks - 10 : -1],
PeerInfo("0.0.0.0", 0),
ForkInfo(fork_height, fork_height, blocks_reorg[-num_blocks - 10].prev_header_hash),
ValidationState(sub_slot_iters, difficulty, None),
blockchain,
)
await full_node.add_block(blocks_reorg[-1])
for wallet_node, wallet_server in wallets:
await time_out_assert(
120, wallet_height_at_least, True, wallet_node, len(default_1000_blocks) + num_blocks - 5 - 1
)
@pytest.mark.limit_consensus_modes(reason="save time")
@pytest.mark.anyio
async def test_wallet_reorg_sync(
two_wallet_nodes: OldSimulatorsAndWallets,
default_400_blocks: list[FullBlock],
self_hostname: str,
use_delta_sync: bool,
) -> None:
num_blocks = 5
[full_node_api], wallets, bt = two_wallet_nodes
full_node = full_node_api.full_node
full_node_server = full_node.server
# Trusted node sync
wallets[0][0].config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()}
wallets[0][0].config["use_delta_sync"] = use_delta_sync
# Untrusted node sync
wallets[1][0].config["trusted_peers"] = {}
wallets[1][0].config["use_delta_sync"] = use_delta_sync
phs = []
for wallet_node, wallet_server in wallets:
wallet = wallet_node.wallet_state_manager.main_wallet
async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
ph = await action_scope.get_puzzle_hash(wallet.wallet_state_manager)
phs.append(ph)
await wallet_server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
# Insert 400 blocks
await add_blocks_in_batches(default_400_blocks, full_node)
# Farm few more with reward
wallet_node1, _ = wallets[0]
wallet1 = wallet_node.wallet_state_manager.main_wallet
wallet_node2, _ = wallets[1]
wallet2 = wallet_node2.wallet_state_manager.main_wallet
await time_out_assert(60, wallet_height_at_least, True, wallet1, 399)
await time_out_assert(60, wallet_height_at_least, True, wallet2, 399)
for _ in range(num_blocks - 1):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(phs[0]))
for _ in range(num_blocks):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(phs[1]))
# Confirm we have the funds
funds = sum(
calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks)
)
await time_out_assert(60, wallet_height_at_least, True, wallet1, 408)
await time_out_assert(60, wallet1.get_confirmed_balance, funds)
await time_out_assert(60, get_tx_count, 2 * (num_blocks - 1), wallet_node1.wallet_state_manager, 1)
await time_out_assert(60, wallet_height_at_least, True, wallet2, 408)
await time_out_assert(60, wallet2.get_confirmed_balance, funds)
await time_out_assert(60, get_tx_count, 2 * (num_blocks - 1), wallet_node2.wallet_state_manager, 1)
# Reorg blocks that carry reward
num_blocks = 30
blocks_reorg = bt.get_consecutive_blocks(num_blocks, block_list_input=default_400_blocks[:-5])
await add_blocks_in_batches(blocks_reorg[-30:], full_node)
for wallet_node, wallet_server in wallets:
wallet = wallet_node.wallet_state_manager.main_wallet
await time_out_assert(60, get_tx_count, 0, wallet_node.wallet_state_manager, 1)
await time_out_assert(60, wallet.get_confirmed_balance, 0)
@pytest.mark.limit_consensus_modes(reason="save time")
@pytest.mark.anyio
async def test_wallet_reorg_get_coinbase(
two_wallet_nodes: OldSimulatorsAndWallets, default_400_blocks: list[FullBlock], self_hostname: str
) -> None:
[full_node_api], wallets, bt = two_wallet_nodes
full_node = full_node_api.full_node
full_node_server = full_node.server
# Trusted node sync
wallets[0][0].config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()}
# Untrusted node sync
wallets[1][0].config["trusted_peers"] = {}
for wallet_node, wallet_server in wallets:
await wallet_server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
# Insert 400 blocks
await add_blocks_in_batches(default_400_blocks, full_node)
# Reorg blocks that carry reward
num_blocks_reorg = 30
blocks_reorg = bt.get_consecutive_blocks(num_blocks_reorg, block_list_input=default_400_blocks[:-5])
await add_blocks_in_batches(blocks_reorg[:-6], full_node)
await full_node.add_block(blocks_reorg[-6])
for wallet_node, wallet_server in wallets:
await time_out_assert(30, get_tx_count, 0, wallet_node.wallet_state_manager, 1)
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=30)
num_blocks_reorg_1 = 40
all_blocks_reorg_2 = blocks_reorg[:-30]
for wallet_node, wallet_server in wallets:
wallet = wallet_node.wallet_state_manager.main_wallet
async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
ph = await action_scope.get_puzzle_hash(wallet.wallet_state_manager)
all_blocks_reorg_2 = bt.get_consecutive_blocks(
1, pool_reward_puzzle_hash=ph, farmer_reward_puzzle_hash=ph, block_list_input=all_blocks_reorg_2
)
blocks_reorg_2 = bt.get_consecutive_blocks(num_blocks_reorg_1, block_list_input=all_blocks_reorg_2)
block_record = await full_node.blockchain.get_block_record_from_db(blocks_reorg_2[-45].header_hash)
sub_slot_iters, difficulty = get_next_sub_slot_iters_and_difficulty(
full_node.constants, True, block_record, full_node.blockchain
)
blockchain = AugmentedBlockchain(full_node.blockchain)
await full_node.add_block_batch(
blocks_reorg_2[-44:],
PeerInfo("0.0.0.0", 0),
ForkInfo(blocks_reorg_2[-45].height, blocks_reorg_2[-45].height, blocks_reorg_2[-45].header_hash),
ValidationState(sub_slot_iters, difficulty, None),
blockchain,
)
for wallet_node, wallet_server in wallets:
await disconnect_all_and_reconnect(wallet_server, full_node_server, self_hostname)
# Confirm we have the funds
funds = calculate_pool_reward(uint32(len(all_blocks_reorg_2))) + calculate_base_farmer_reward(
uint32(len(all_blocks_reorg_2))
)
for wallet_node, wallet_server in wallets:
wallet = wallet_node.wallet_state_manager.main_wallet
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=60)
await time_out_assert(20, get_tx_count, 2, wallet_node.wallet_state_manager, 1)
await time_out_assert(20, wallet.get_confirmed_balance, funds)
@pytest.mark.anyio
async def test_request_additions_errors(simulator_and_wallet: OldSimulatorsAndWallets, self_hostname: str) -> None:
full_nodes, wallets, _ = simulator_and_wallet
wallet_node, wallet_server = wallets[0]
wallet = wallet_node.wallet_state_manager.main_wallet
async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
ph = await action_scope.get_puzzle_hash(wallet.wallet_state_manager)
full_node_api = full_nodes[0]
await wallet_server.start_client(PeerInfo(self_hostname, full_node_api.full_node.server.get_port()), None)
for _ in range(2):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20)
last_block: Optional[BlockRecord] = full_node_api.full_node.blockchain.get_peak()
assert last_block is not None
# Invalid height
with pytest.raises(ValueError):
await full_node_api.request_additions(RequestAdditions(uint32(100), last_block.header_hash, [ph]))
# Invalid header hash
with pytest.raises(ValueError):
await full_node_api.request_additions(RequestAdditions(last_block.height, std_hash(b""), [ph]))
# No results
fake_coin = std_hash(b"")
assert ph != fake_coin
res1 = await full_node_api.request_additions(
RequestAdditions(last_block.height, last_block.header_hash, [fake_coin])
)
assert res1 is not None
response = RespondAdditions.from_bytes(res1.data)
assert response.height == last_block.height
assert response.header_hash == last_block.header_hash
assert response.proofs is not None
assert len(response.proofs) == 1
assert len(response.coins) == 1
full_block = await full_node_api.full_node.block_store.get_full_block(last_block.header_hash)
assert full_block is not None
assert full_block.foliage_transaction_block is not None
root = full_block.foliage_transaction_block.additions_root
assert confirm_not_included_already_hashed(root, response.proofs[0][0], response.proofs[0][1])
# proofs is a tuple of (puzzlehash, proof, proof_2)
# proof is a proof of inclusion (or exclusion) of that puzzlehash
# proof_2 is a proof of all the coins with that puzzlehash
# all coin names are concatenated and hashed into one entry in the merkle set for proof_2
# the response contains the list of coins so you can check the proof_2
assert response.proofs[0][0] == std_hash(b"")
assert response.proofs[0][1] is not None
assert response.proofs[0][2] is None
@pytest.mark.anyio
async def test_request_additions_success(simulator_and_wallet: OldSimulatorsAndWallets, self_hostname: str) -> None:
full_nodes, wallets, _ = simulator_and_wallet
wallet_node, wallet_server = wallets[0]
wallet = wallet_node.wallet_state_manager.main_wallet
async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
ph = await action_scope.get_puzzle_hash(wallet.wallet_state_manager)
full_node_api = full_nodes[0]
await wallet_server.start_client(PeerInfo(self_hostname, full_node_api.full_node.server.get_port()), None)
for _ in range(2):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20)
async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
payees: list[CreateCoin] = []
for i in range(10):
payee_ph = await action_scope.get_puzzle_hash(
wallet.wallet_state_manager, override_reuse_puzhash_with=False
)
payees.append(CreateCoin(payee_ph, uint64(i + 100)))
payees.append(CreateCoin(payee_ph, uint64(i + 200)))
await wallet.generate_signed_transaction(
[uint64(0), *(payee.amount for payee in payees)],
[ph, *(payee.puzzle_hash for payee in payees)],
action_scope,
)
[tx] = action_scope.side_effects.transactions
assert tx.spend_bundle is not None
await full_node_api.send_transaction(SendTransaction(tx.spend_bundle))
await full_node_api.wait_transaction_records_entered_mempool([tx])
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
last_block = full_node_api.full_node.blockchain.get_peak()
assert last_block is not None
await full_node_api.wait_for_wallet_synced(wallet_node=wallet_node, timeout=20)
res2 = await full_node_api.request_additions(
RequestAdditions(last_block.height, None, [payees[0].puzzle_hash, payees[2].puzzle_hash, std_hash(b"1")])
)
assert res2 is not None
response = RespondAdditions.from_bytes(res2.data)
assert response.height == last_block.height
assert response.header_hash == last_block.header_hash
assert response.proofs is not None
assert len(response.proofs) == 3
# First two PHs are included
for i in range(2):
assert response.proofs[i][0] in {payees[j].puzzle_hash for j in (0, 2)}
assert response.proofs[i][1] is not None
assert response.proofs[i][2] is not None
# Third PH is not included
assert response.proofs[2][2] is None
coin_list_dict = {p: coin_list for p, coin_list in response.coins}
assert len(coin_list_dict) == 3
for p, coin_list in coin_list_dict.items():
if p == std_hash(b"1"):
# this is the one that is not included
assert len(coin_list) == 0
else:
for coin in coin_list:
assert coin.puzzle_hash == p
# The other ones are included
assert len(coin_list) == 2
# None for puzzle hashes returns all coins and no proofs
res3 = await full_node_api.request_additions(RequestAdditions(last_block.height, last_block.header_hash, None))
assert res3 is not None
response = RespondAdditions.from_bytes(res3.data)
assert response.height == last_block.height
assert response.header_hash == last_block.header_hash
assert response.proofs is None
assert len(response.coins) == 12
assert sum(len(c_list) for _, c_list in response.coins) == 24
# [] for puzzle hashes returns nothing
res4 = await full_node_api.request_additions(RequestAdditions(last_block.height, last_block.header_hash, []))
assert res4 is not None
response = RespondAdditions.from_bytes(res4.data)
assert response.proofs == []
assert len(response.coins) == 0
@pytest.mark.anyio
async def test_get_wp_fork_point(
default_10000_blocks: list[FullBlock], blockchain_constants: ConsensusConstants
) -> None:
blocks = default_10000_blocks
header_cache, height_to_hash, sub_blocks, summaries = await load_blocks_dont_validate(blocks, blockchain_constants)
wpf = WeightProofHandler(blockchain_constants, BlockchainMock(sub_blocks, header_cache, height_to_hash, summaries))
wp1 = await wpf.get_proof_of_weight(header_cache[height_to_hash[uint32(9_000)]].header_hash)
assert wp1 is not None
wp2 = await wpf.get_proof_of_weight(header_cache[height_to_hash[uint32(9_030)]].header_hash)
assert wp2 is not None
wp3 = await wpf.get_proof_of_weight(header_cache[height_to_hash[uint32(7_500)]].header_hash)
assert wp3 is not None
wp4 = await wpf.get_proof_of_weight(header_cache[height_to_hash[uint32(8_700)]].header_hash)
assert wp4 is not None
wp5 = await wpf.get_proof_of_weight(header_cache[height_to_hash[uint32(9_700)]].header_hash)
assert wp5 is not None
wp6 = await wpf.get_proof_of_weight(header_cache[height_to_hash[uint32(9_010)]].header_hash)
assert wp6 is not None
fork12 = get_wp_fork_point(blockchain_constants, wp1, wp2)
fork13 = get_wp_fork_point(blockchain_constants, wp3, wp1)
fork14 = get_wp_fork_point(blockchain_constants, wp4, wp1)
fork23 = get_wp_fork_point(blockchain_constants, wp3, wp2)
fork24 = get_wp_fork_point(blockchain_constants, wp4, wp2)
fork34 = get_wp_fork_point(blockchain_constants, wp3, wp4)
fork45 = get_wp_fork_point(blockchain_constants, wp4, wp5)
fork16 = get_wp_fork_point(blockchain_constants, wp1, wp6)
# overlap between recent chain in wps, fork point is the tip of the shorter wp
assert fork12 == wp1.recent_chain_data[-1].height
assert fork16 == wp1.recent_chain_data[-1].height
# if there is an overlap between the recent chains we can find the exact fork point
# if not we should get the latest block with a sub epoch summary that exists in both wp's
# this can happen in fork24 and fork14 since they are not very far and also not very close
if wp2.recent_chain_data[0].height > wp4.recent_chain_data[-1].height:
assert fork24 in summaries.keys()
assert fork24 < wp4.recent_chain_data[-1].height
else:
assert fork24 == wp4.recent_chain_data[-1].height
if wp1.recent_chain_data[0].height > wp4.recent_chain_data[-1].height:
assert fork14 in summaries.keys()
assert fork14 < wp4.recent_chain_data[-1].height
else:
assert fork14 == wp4.recent_chain_data[-1].height
# no overlap between recent chain in wps, fork point
# is the latest block with a sub epoch summary that exists in both wp's
assert fork13 in summaries.keys()
assert fork13 < wp3.recent_chain_data[-1].height
assert fork23 in summaries.keys()
assert fork23 < wp3.recent_chain_data[-1].height
assert fork34 in summaries.keys()
assert fork23 < wp3.recent_chain_data[-1].height
assert fork45 in summaries.keys()
assert fork45 < wp4.recent_chain_data[-1].height
"""
This tests that a wallet filters out the dust properly.
It runs in seven phases:
1. Create a single dust coin.
Typically (though there are edge cases), this coin will not be filtered.
2. Create dust coins until the filter threshold has been reached.
At this point, none of the dust should be filtered.
3. Create 10 coins that are exactly the size of the filter threshold.
These should not be filtered because they are not dust.
4. Create one more dust coin. This coin should be filtered.
5. Create 5 coins below the threshold and 5 at or above.
Those below the threshold should get filtered, and those above should not.
6. Clear all coins from the dust wallet.
Send to the dust wallet "spam_filter_after_n_txs" coins that are equal in value to "xch_spam_amount".
Send 1 mojo from the dust wallet. The dust wallet should receive a change coin valued at "xch_spam_amount-1".
7: Create an NFT wallet for the farmer wallet, and generate an NFT in that wallet.
Create an NFT wallet for the dust wallet.
Send the NFT to the dust wallet. The NFT should not be filtered.
"""
@pytest.mark.anyio
@pytest.mark.parametrize(
"spam_filter_after_n_txs, xch_spam_amount, dust_value",
[
# In the following tests, the filter is run right away:
(0, 1, 1), # nothing is filtered
# In the following tests, 1 coin will be created in part 1, and 9 in part 2:
(10, 10_000_000_000, 1), # everything is dust
(10, 10_000_000_000, 10_000_000_000), # max dust threshold, dust is same size so not filtered
# Test with more coins
(105, 1_000_000, 1), # default filter level (1m mojos), default dust size (1)
],
)
async def test_dusted_wallet(
self_hostname: str,
two_wallet_nodes_custom_spam_filtering: OldSimulatorsAndWallets,
spam_filter_after_n_txs: int,
xch_spam_amount: int,
dust_value: int,
use_delta_sync: bool,
) -> None:
full_nodes, wallets, _ = two_wallet_nodes_custom_spam_filtering
farm_wallet_node, farm_wallet_server = wallets[0]
farm_wallet_node.config["use_delta_sync"] = use_delta_sync
dust_wallet_node, dust_wallet_server = wallets[1]
dust_wallet_node.config["use_delta_sync"] = use_delta_sync
# Create two wallets, one for farming (not used for testing), and one for testing dust.
farm_wallet = farm_wallet_node.wallet_state_manager.main_wallet
dust_wallet = dust_wallet_node.wallet_state_manager.main_wallet
async with farm_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
ph = await action_scope.get_puzzle_hash(farm_wallet.wallet_state_manager)
full_node_api = full_nodes[0]
# It's also possible to obtain the current settings for spam_filter_after_n_txs and xch_spam_amount
# spam_filter_after_n_txs = wallets[0][0].config["spam_filter_after_n_txs"]
# xch_spam_amount = wallets[0][0].config["xch_spam_amount"]
# dust_value=1
# Verify legal values for the settings to be tested
# If spam_filter_after_n_txs is greater than 250, this test will take a long time to run.
# Current max value for xch_spam_amount is 0.01 XCH.
# If needed, this could be increased but we would need to farm more blocks.
# The max dust_value could be increased, but would require farming more blocks.
assert spam_filter_after_n_txs >= 0
assert spam_filter_after_n_txs <= 250
assert xch_spam_amount >= 1
assert xch_spam_amount <= 10_000_000_000
assert dust_value >= 1
assert dust_value <= 10_000_000_000
# start both clients
await farm_wallet_server.start_client(PeerInfo(self_hostname, full_node_api.full_node.server.get_port()), None)
await dust_wallet_server.start_client(PeerInfo(self_hostname, full_node_api.full_node.server.get_port()), None)
# Farm two blocks
for _ in range(2):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
# sync both nodes
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# Part 1: create a single dust coin
async with dust_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
payees: list[CreateCoin] = []
payee_ph = await action_scope.get_puzzle_hash(dust_wallet.wallet_state_manager)
payees.append(CreateCoin(payee_ph, uint64(dust_value)))
# construct and send tx
async with farm_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await farm_wallet.generate_signed_transaction(
[uint64(0), *(payee.amount for payee in payees)],
[ph, *(payee.puzzle_hash for payee in payees)],
action_scope,
)
[tx] = action_scope.side_effects.transactions
assert tx.spend_bundle is not None
await full_node_api.send_transaction(SendTransaction(tx.spend_bundle))
await full_node_api.wait_transaction_records_entered_mempool([tx])
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# The dust is only filtered at this point if spam_filter_after_n_txs is 0 and xch_spam_amount is > dust_value.
if spam_filter_after_n_txs > 0:
dust_coins = 1
large_dust_coins = 0
large_dust_balance = 0
elif xch_spam_amount <= dust_value:
dust_coins = 0
large_dust_coins = 1
large_dust_balance = dust_value
else:
dust_coins = 0
large_dust_coins = 0
large_dust_balance = 0
# Obtain and log important values
all_unspent = await dust_wallet_node.wallet_state_manager.coin_store.get_all_unspent_coins()
log.info(f"all_unspent is {all_unspent}")
small_unspent_count = len([r for r in all_unspent if r.coin.amount < xch_spam_amount])
balance = await dust_wallet.get_confirmed_balance()
async with dust_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=False) as action_scope:
num_coins = len(await dust_wallet.select_coins(uint64(balance), action_scope))
log.info(f"Small coin count is {small_unspent_count}")
log.info(f"Wallet balance is {balance}")
log.info(f"Number of coins is {num_coins}")
log.info(f"spam_filter_after_n_txs {spam_filter_after_n_txs}")
log.info(f"xch_spam_amount {xch_spam_amount}")
log.info(f"dust_value {dust_value}")
# Verify balance and number of coins not filtered.
assert balance == dust_coins * dust_value + large_dust_balance
assert num_coins == dust_coins + large_dust_coins
# Part 2: Create dust coins until the filter threshold has been reached.
# Nothing should be filtered yet (unless spam_filter_after_n_txs is 0).
payees = []
# Determine how much dust to create, recalling that there already is one dust coin.
new_dust = spam_filter_after_n_txs - 1
dust_remaining = new_dust
while dust_remaining > 0:
async with dust_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
payee_ph = await action_scope.get_puzzle_hash(
dust_wallet.wallet_state_manager, override_reuse_puzhash_with=False
)
payees.append(CreateCoin(payee_ph, uint64(dust_value)))
# After every 100 (at most) coins added, push the tx and advance the chain
# This greatly speeds up the overall process
if dust_remaining % 100 == 0 and dust_remaining != new_dust:
# construct and send tx
async with farm_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await farm_wallet.generate_signed_transaction(
[uint64(0), *(payee.amount for payee in payees)],
[ph, *(payee.puzzle_hash for payee in payees)],
action_scope,
)
[tx] = action_scope.side_effects.transactions
assert tx.spend_bundle is not None
await full_node_api.send_transaction(SendTransaction(tx.spend_bundle))
# advance the chain and sync both wallets
await full_node_api.wait_transaction_records_entered_mempool([tx])
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# reset payees
payees = []
dust_remaining -= 1
# Only need to create tx if there was new dust to be added
if new_dust >= 1:
# construct and send tx
async with farm_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await farm_wallet.generate_signed_transaction(
[uint64(0), *(payee.amount for payee in payees)],
[ph, *(payee.puzzle_hash for payee in payees)],
action_scope,
)
[tx] = action_scope.side_effects.transactions
assert tx.spend_bundle is not None
await full_node_api.send_transaction(SendTransaction(tx.spend_bundle))
# advance the chain and sync both wallets
await full_node_api.wait_transaction_records_entered_mempool([tx])
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# Obtain and log important values
all_unspent = await dust_wallet_node.wallet_state_manager.coin_store.get_all_unspent_coins()
small_unspent_count = len([r for r in all_unspent if r.coin.amount < xch_spam_amount])
balance = await dust_wallet.get_confirmed_balance()
# Selecting coins by using the wallet's coin selection algorithm won't work for large
# numbers of coins, so we'll use the state manager for the rest of the test
spendable_coins = await dust_wallet_node.wallet_state_manager.get_spendable_coins_for_wallet(1)
num_coins = len(spendable_coins)
log.info(f"Small coin count is {small_unspent_count}")
log.info(f"Wallet balance is {balance}")
log.info(f"Number of coins is {num_coins}")
# obtain the total expected coins (new_dust could be negative)
if new_dust > 0:
dust_coins += new_dust
# Make sure the number of coins matches the expected number.
# At this point, nothing should be getting filtered unless spam_filter_after_n_txs is 0.
assert dust_coins == spam_filter_after_n_txs
assert balance == dust_coins * dust_value + large_dust_balance
assert num_coins == dust_coins + large_dust_coins
# Part 3: Create 10 coins that are exactly the size of the filter threshold.
# These should not get filtered.
large_coins = 10
payees = []
async with dust_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
for _ in range(large_coins):
payee_ph = await action_scope.get_puzzle_hash(
dust_wallet.wallet_state_manager, override_reuse_puzhash_with=False
)
payees.append(CreateCoin(payee_ph, uint64(xch_spam_amount)))
# construct and send tx
async with farm_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await farm_wallet.generate_signed_transaction(
[uint64(0), *(payee.amount for payee in payees)],
[ph, *(payee.puzzle_hash for payee in payees)],
action_scope,
)
[tx] = action_scope.side_effects.transactions
assert tx.spend_bundle is not None
await full_node_api.send_transaction(SendTransaction(tx.spend_bundle))
# advance the chain and sync both wallets
await full_node_api.wait_transaction_records_entered_mempool([tx])
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# Obtain and log important values
all_unspent = await dust_wallet_node.wallet_state_manager.coin_store.get_all_unspent_coins()
small_unspent_count = len([r for r in all_unspent if r.coin.amount < xch_spam_amount])
balance = await dust_wallet.get_confirmed_balance()
spendable_coins = await dust_wallet_node.wallet_state_manager.get_spendable_coins_for_wallet(1)
num_coins = len(spendable_coins)
log.info(f"Small coin count is {small_unspent_count}")
log.info(f"Wallet balance is {balance}")
log.info(f"Number of coins is {num_coins}")
large_coin_balance = large_coins * xch_spam_amount
# Determine whether the filter should have been activated.
# Make sure the number of coins matches the expected number.
# At this point, nothing should be getting filtered unless spam_filter_after_n_txs is 0.
assert dust_coins == spam_filter_after_n_txs
assert balance == dust_coins * dust_value + large_coins * xch_spam_amount + large_dust_balance
assert num_coins == dust_coins + large_coins + large_dust_coins
# Part 4: Create one more dust coin to test the threshold
payees = []
async with dust_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
payee_ph = await action_scope.get_puzzle_hash(dust_wallet.wallet_state_manager)
payees.append(CreateCoin(payee_ph, uint64(dust_value)))
# construct and send tx
async with farm_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await farm_wallet.generate_signed_transaction(
[uint64(0), *(payee.amount for payee in payees)],
[ph, *(payee.puzzle_hash for payee in payees)],
action_scope,
)
[tx] = action_scope.side_effects.transactions
assert tx.spend_bundle is not None
await full_node_api.send_transaction(SendTransaction(tx.spend_bundle))
# advance the chain and sync both wallets
await full_node_api.wait_transaction_records_entered_mempool([tx])
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# Obtain and log important values
all_unspent = await dust_wallet_node.wallet_state_manager.coin_store.get_all_unspent_coins()
small_unspent_count = len([r for r in all_unspent if r.coin.amount < xch_spam_amount])
balance = await dust_wallet.get_confirmed_balance()
spendable_coins = await dust_wallet_node.wallet_state_manager.get_spendable_coins_for_wallet(1)
num_coins = len(spendable_coins)
log.info(f"Small coin count is {small_unspent_count}")
log.info(f"Wallet balance is {balance}")
log.info(f"Number of coins is {num_coins}")
# In the edge case where the new "dust" is larger than the threshold,
# then it is actually a large dust coin that won't get filtered.
if dust_value >= xch_spam_amount:
large_dust_coins += 1
large_dust_balance += dust_value
assert dust_coins == spam_filter_after_n_txs
assert balance == dust_coins * dust_value + large_coins * xch_spam_amount + large_dust_balance
assert num_coins == dust_coins + large_dust_coins + large_coins
# Part 5: Create 5 coins below the threshold and 5 at or above.
# Those below the threshold should get filtered, and those above should not.
payees = []
async with dust_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
for i in range(5):
payee_ph = await action_scope.get_puzzle_hash(dust_wallet.wallet_state_manager)
# Create a large coin and add on the appropriate balance.
payees.append(CreateCoin(payee_ph, uint64(xch_spam_amount + i)))
large_coins += 1
large_coin_balance += xch_spam_amount + i
payee_ph = await action_scope.get_puzzle_hash(dust_wallet.wallet_state_manager)
# Make sure we are always creating coins with a positive value.
if xch_spam_amount - dust_value - i > 0:
payees.append(CreateCoin(payee_ph, uint64(xch_spam_amount - dust_value - i)))
else:
payees.append(CreateCoin(payee_ph, uint64(dust_value)))
# In cases where xch_spam_amount is sufficiently low,
# the new dust should be considered a large coina and not be filtered.
if xch_spam_amount <= dust_value:
large_dust_coins += 1
large_dust_balance += dust_value
# construct and send tx
async with farm_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await farm_wallet.generate_signed_transaction(
[uint64(0), *(payee.amount for payee in payees)],
[ph, *(payee.puzzle_hash for payee in payees)],
action_scope,
)
[tx] = action_scope.side_effects.transactions
assert tx.spend_bundle is not None
await full_node_api.send_transaction(SendTransaction(tx.spend_bundle))
# advance the chain and sync both wallets
await full_node_api.wait_transaction_records_entered_mempool([tx])
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# Obtain and log important values
all_unspent = await dust_wallet_node.wallet_state_manager.coin_store.get_all_unspent_coins()
small_unspent_count = len([r for r in all_unspent if r.coin.amount < xch_spam_amount])
balance = await dust_wallet.get_confirmed_balance()
spendable_coins = await dust_wallet_node.wallet_state_manager.get_spendable_coins_for_wallet(1)
num_coins = len(spendable_coins)
log.info(f"Small coin count is {small_unspent_count}")
log.info(f"Wallet balance is {balance}")
log.info(f"Number of coins is {num_coins}")
# The filter should have automatically been activated by now, regardless of filter value
assert dust_coins == spam_filter_after_n_txs
assert balance == dust_coins * dust_value + large_coin_balance + large_dust_balance
assert num_coins == dust_coins + large_dust_coins + large_coins
# Part 6: Clear all coins from the dust wallet.
# Send to the dust wallet "spam_filter_after_n_txs" coins that are equal in value to "xch_spam_amount".
# Send 1 mojo from the dust wallet. The dust wallet should receive a change coin valued at "xch_spam_amount-1".
async with farm_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
payee_ph = await action_scope.get_puzzle_hash(farm_wallet.wallet_state_manager)
payees = [CreateCoin(payee_ph, uint64(balance))]
# construct and send tx
async with dust_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await dust_wallet.generate_signed_transaction(
[uint64(0), *(payee.amount for payee in payees)],
[ph, *(payee.puzzle_hash for payee in payees)],
action_scope,
)
[tx] = action_scope.side_effects.transactions
assert tx.spend_bundle is not None
await full_node_api.send_transaction(SendTransaction(tx.spend_bundle))
# advance the chain and sync both wallets
await full_node_api.wait_transaction_records_entered_mempool([tx])
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# Obtain and log important values
all_unspent = await dust_wallet_node.wallet_state_manager.coin_store.get_all_unspent_coins()
unspent_count = len(all_unspent)
balance = await dust_wallet.get_confirmed_balance()
# Make sure the dust wallet is empty
assert unspent_count == 0
assert balance == 0
# create the same number of dust coins as the filter
if spam_filter_after_n_txs > 0:
coins_remaining = spam_filter_after_n_txs
else:
# in the edge case, create one coin
coins_remaining = 1
# The size of the coin to send the dust wallet is the same as xch_spam_amount
if xch_spam_amount > 1:
coin_value = xch_spam_amount
else:
# Handle the edge case to make sure the coin is at least 2 mojos
# This is needed to receive change
coin_value = 2
while coins_remaining > 0:
async with dust_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
payee_ph = await action_scope.get_puzzle_hash(dust_wallet.wallet_state_manager)
payees.append(CreateCoin(payee_ph, uint64(coin_value)))
# After every 100 (at most) coins added, push the tx and advance the chain
# This greatly speeds up the overall process
if coins_remaining % 100 == 0 and coins_remaining != spam_filter_after_n_txs:
# construct and send tx
async with farm_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await farm_wallet.generate_signed_transaction(
[uint64(0), *(payee.amount for payee in payees)],
[ph, *(payee.puzzle_hash for payee in payees)],
action_scope,
)
[tx] = action_scope.side_effects.transactions
assert tx.spend_bundle is not None
await full_node_api.send_transaction(SendTransaction(tx.spend_bundle))
await full_node_api.wait_transaction_records_entered_mempool([tx])
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# reset payees
payees = []
coins_remaining -= 1
# construct and send tx
async with farm_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await farm_wallet.generate_signed_transaction(
[uint64(0), *(payee.amount for payee in payees)],
[ph, *(payee.puzzle_hash for payee in payees)],
action_scope,
)
[tx] = action_scope.side_effects.transactions
assert tx.spend_bundle is not None
await full_node_api.send_transaction(SendTransaction(tx.spend_bundle))
# advance the chain and sync both wallets
await full_node_api.wait_transaction_records_entered_mempool([tx])
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# Obtain and log important values
all_unspent = await dust_wallet_node.wallet_state_manager.coin_store.get_all_unspent_coins()
unspent_count = len(all_unspent)
balance = await dust_wallet.get_confirmed_balance()
# Verify the number of coins and value
if spam_filter_after_n_txs > 0:
assert unspent_count == spam_filter_after_n_txs
else:
# in the edge case there should be 1 coin
assert unspent_count == 1
assert balance == unspent_count * coin_value
# Send a 1 mojo coin from the dust wallet to the farm wallet
async with farm_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
payee_ph = await action_scope.get_puzzle_hash(farm_wallet.wallet_state_manager)
payees = [CreateCoin(payee_ph, uint64(1))]
# construct and send tx
async with dust_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await dust_wallet.generate_signed_transaction(
[uint64(0), *(payee.amount for payee in payees)],
[ph, *(payee.puzzle_hash for payee in payees)],
action_scope,
)
[tx] = action_scope.side_effects.transactions
assert tx.spend_bundle is not None
await full_node_api.send_transaction(SendTransaction(tx.spend_bundle))
# advance the chain and sync both wallets
await full_node_api.wait_transaction_records_entered_mempool([tx])
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# Obtain and log important values
all_unspent = await dust_wallet_node.wallet_state_manager.coin_store.get_all_unspent_coins()
unspent_count = len(all_unspent)
balance = await dust_wallet.get_confirmed_balance()
# Make sure the dust wallet received a change coin worth 1 mojo less than the original coin size
if spam_filter_after_n_txs > 0:
assert unspent_count == spam_filter_after_n_txs
else:
# in the edge case there should be 1 coin
assert unspent_count == 1
assert balance == (unspent_count * coin_value) - 1
# Part 7: Create NFT wallets for the farmer and dust wallets.
# Generate an NFT in the farmer wallet.
# Send the NFT to the dust wallet, which already has enough coins to trigger the dust filter.
# The NFT should not be filtered.
# Start with new puzzlehashes for each wallet
async with farm_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
farm_ph = await action_scope.get_puzzle_hash(farm_wallet.wallet_state_manager)
async with dust_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
dust_ph = await action_scope.get_puzzle_hash(dust_wallet.wallet_state_manager)
# Create an NFT wallet for the farmer and dust wallet
farm_nft_wallet = await NFTWallet.create_new_nft_wallet(
farm_wallet_node.wallet_state_manager, farm_wallet, name="FARM NFT WALLET"
)
dust_nft_wallet = await NFTWallet.create_new_nft_wallet(
dust_wallet_node.wallet_state_manager, dust_wallet, name="DUST NFT WALLET"
)
# Create a new NFT and send it to the farmer's NFT wallet
metadata = Program.to(
[("u", ["https://www.chia.net/img/branding/chia-logo.svg"]), ("h", "0xD4584AD463139FA8C0D9F68F4B59F185")]
)
async with farm_nft_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await farm_nft_wallet.generate_new_nft(metadata, action_scope)
for tx in action_scope.side_effects.transactions:
if tx.spend_bundle is not None:
assert len(compute_memos(tx.spend_bundle)) > 0
await time_out_assert_not_none(
20, full_node_api.full_node.mempool_manager.get_spendbundle, tx.spend_bundle.name()
)
# Farm a new block
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(farm_ph))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# Make sure the dust wallet has enough unspent coins in that the next coin would be filtered
# if it were a normal dust coin (and not an NFT)
all_unspent = await dust_wallet_node.wallet_state_manager.coin_store.get_all_unspent_coins()
assert len(all_unspent) >= spam_filter_after_n_txs
# Make sure the NFT is in the farmer's NFT wallet, and the dust NFT wallet is empty
await time_out_assert(15, get_nft_count, 1, farm_nft_wallet)
await time_out_assert(15, get_nft_count, 0, dust_nft_wallet)
nft_coins = await farm_nft_wallet.get_current_nfts()
# Send the NFT to the dust wallet
async with farm_nft_wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await farm_nft_wallet.generate_signed_transaction(
[uint64(nft_coins[0].coin.amount)], [dust_ph], action_scope, coins={nft_coins[0].coin}
)
assert len(action_scope.side_effects.transactions) == 1
txs = await farm_wallet_node.wallet_state_manager.add_pending_transactions(action_scope.side_effects.transactions)
assert txs[0].spend_bundle is not None
assert len(compute_memos(txs[0].spend_bundle)) > 0
# Farm a new block.
await full_node_api.wait_transaction_records_entered_mempool(txs)
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(farm_ph))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[farm_wallet_node, dust_wallet_node], timeout=20)
# Make sure the dust wallet has enough unspent coins in that the next coin would be filtered
# if it were a normal dust coin (and not an NFT)
all_unspent = await dust_wallet_node.wallet_state_manager.coin_store.get_all_unspent_coins()
assert len(all_unspent) >= spam_filter_after_n_txs
# The dust wallet should now hold the NFT. It should not be filtered
await time_out_assert(15, get_nft_count, 0, farm_nft_wallet)
await time_out_assert(15, get_nft_count, 1, dust_nft_wallet)
@pytest.mark.anyio
async def test_retry_store(
two_wallet_nodes: OldSimulatorsAndWallets, self_hostname: str, monkeypatch: pytest.MonkeyPatch
) -> None:
full_nodes, wallets, _ = two_wallet_nodes
full_node_api = full_nodes[0]
full_node_server = full_node_api.full_node.server
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32.zeros))
# Trusted node sync
wallets[0][0].config["trusted_peers"] = {full_node_server.node_id.hex(): full_node_server.node_id.hex()}
# Untrusted node sync
wallets[1][0].config["trusted_peers"] = {}
@dataclass
class FlakinessInfo:
coin_state_flaky: bool = True
fetch_children_flaky: bool = True
get_timestamp_flaky: bool = True
db_flaky: bool = True
def flaky_get_coin_state(
flakiness_info: FlakinessInfo,
func: Callable[[list[bytes32], WSChiaConnection, Optional[uint32]], Awaitable[list[CoinState]]],
) -> Callable[[list[bytes32], WSChiaConnection, Optional[uint32]], Awaitable[list[CoinState]]]:
async def new_func(
coin_names: list[bytes32], peer: WSChiaConnection, fork_height: Optional[uint32] = None
) -> list[CoinState]:
if flakiness_info.coin_state_flaky:
flakiness_info.coin_state_flaky = False
raise PeerRequestException
else:
return await func(coin_names, peer, fork_height)
return new_func
request_puzzle_solution_failure_tested = False
def flaky_request_puzzle_solution(
func: Callable[[FullNodeAPI, wallet_protocol.RequestPuzzleSolution], Awaitable[Optional[Message]]],
) -> Callable[[FullNodeAPI, wallet_protocol.RequestPuzzleSolution], Awaitable[Optional[Message]]]:
@functools.wraps(func)
async def new_func(self: FullNodeAPI, request: wallet_protocol.RequestPuzzleSolution) -> Optional[Message]:
nonlocal request_puzzle_solution_failure_tested
if not request_puzzle_solution_failure_tested:
request_puzzle_solution_failure_tested = True
# This can just return None if we have `none_response` enabled.
reject = wallet_protocol.RejectPuzzleSolution(bytes32.zeros, uint32(0))
return make_msg(ProtocolMessageTypes.reject_puzzle_solution, reject)
else:
return await func(self, request)
return new_func
def flaky_fetch_children(
flakiness_info: FlakinessInfo,
func: Callable[[bytes32, WSChiaConnection, Optional[uint32]], Awaitable[list[CoinState]]],
) -> Callable[[bytes32, WSChiaConnection, Optional[uint32]], Awaitable[list[CoinState]]]:
async def new_func(
coin_name: bytes32, peer: WSChiaConnection, fork_height: Optional[uint32] = None
) -> list[CoinState]:
if flakiness_info.fetch_children_flaky:
flakiness_info.fetch_children_flaky = False
raise PeerRequestException
else:
return await func(coin_name, peer, fork_height)
return new_func
def flaky_get_timestamp(
flakiness_info: FlakinessInfo, func: Callable[[uint32], Awaitable[uint64]]
) -> Callable[[uint32], Awaitable[uint64]]:
async def new_func(height: uint32) -> uint64:
if flakiness_info.get_timestamp_flaky:
flakiness_info.get_timestamp_flaky = False
raise PeerRequestException
else:
return await func(height)
return new_func
def flaky_info_for_puzhash(
flakiness_info: FlakinessInfo, func: Callable[[bytes32], Awaitable[Optional[WalletIdentifier]]]
) -> Callable[[bytes32], Awaitable[Optional[WalletIdentifier]]]:
async def new_func(puzzle_hash: bytes32) -> Optional[WalletIdentifier]:
if flakiness_info.db_flaky:
flakiness_info.db_flaky = False
raise AIOSqliteError
else:
return await func(puzzle_hash)
return new_func
with contextlib.ExitStack() as exit_stack:
exit_stack.enter_context(
patch_request_handler(
api=full_node_api,
handler=flaky_request_puzzle_solution(FullNodeAPI.request_puzzle_solution),
request_type=ProtocolMessageTypes.request_puzzle_solution,
)
)
m = exit_stack.enter_context(monkeypatch.context())
for wallet_node, wallet_server in wallets:
wallet_node.coin_state_retry_seconds = 1
request_puzzle_solution_failure_tested = False
flakiness_info = FlakinessInfo()
m.setattr(wallet_node, "get_coin_state", flaky_get_coin_state(flakiness_info, wallet_node.get_coin_state))
m.setattr(wallet_node, "fetch_children", flaky_fetch_children(flakiness_info, wallet_node.fetch_children))
m.setattr(
wallet_node,
"get_timestamp_for_height",
flaky_get_timestamp(flakiness_info, wallet_node.get_timestamp_for_height),
)
m.setattr(
wallet_node.wallet_state_manager.puzzle_store,
"get_wallet_identifier_for_puzzle_hash",
flaky_info_for_puzhash(
flakiness_info, wallet_node.wallet_state_manager.puzzle_store.get_wallet_identifier_for_puzzle_hash
),
)
await wallet_server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
wallet = wallet_node.wallet_state_manager.main_wallet
async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
ph = await action_scope.get_puzzle_hash(wallet.wallet_state_manager)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph))
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32.zeros))
async def retry_store_empty() -> bool:
return len(await wallet_node.wallet_state_manager.retry_store.get_all_states_to_retry()) == 0
async def assert_coin_state_retry() -> None:
# Wait for retry coin states to show up
await time_out_assert(15, retry_store_empty, False)
# And become retried/removed
await time_out_assert(30, retry_store_empty, True)
await assert_coin_state_retry()
async with wallet.wallet_state_manager.new_action_scope(DEFAULT_TX_CONFIG, push=True) as action_scope:
await wallet.generate_signed_transaction(
[uint64(1_000_000_000_000)], [bytes32.zeros], action_scope, memos=[[ph]]
)
[tx] = action_scope.side_effects.transactions
await time_out_assert(30, wallet.get_confirmed_balance, 2_000_000_000_000)
async def tx_in_mempool() -> bool:
return full_node_api.full_node.mempool_manager.get_spendbundle(tx.name) is not None
await time_out_assert(15, tx_in_mempool)
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(bytes32.zeros))
await assert_coin_state_retry()
assert not flakiness_info.coin_state_flaky
assert request_puzzle_solution_failure_tested
assert not flakiness_info.fetch_children_flaky
assert not flakiness_info.get_timestamp_flaky
assert not flakiness_info.db_flaky
await time_out_assert(30, wallet.get_confirmed_balance, 1_000_000_000_000)
# TODO: fix this test
@pytest.mark.limit_consensus_modes(reason="save time")
@pytest.mark.anyio
@pytest.mark.skip("the test fails with 'wallet_state_manager not assigned'. This test doesn't work, skip it for now")
async def test_bad_peak_mismatch(
two_wallet_nodes: OldSimulatorsAndWallets,
default_1000_blocks: list[FullBlock],
self_hostname: str,
blockchain_constants: ConsensusConstants,
monkeypatch: pytest.MonkeyPatch,
) -> None:
[full_node_api], [(wallet_node, wallet_server), _], _ = two_wallet_nodes
full_node = full_node_api.full_node
full_node_server = full_node.server
blocks = default_1000_blocks
header_cache, height_to_hash, sub_blocks, summaries = await load_blocks_dont_validate(blocks, blockchain_constants)
wpf = WeightProofHandler(blockchain_constants, BlockchainMock(sub_blocks, header_cache, height_to_hash, summaries))
await wallet_server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
await add_blocks_in_batches(blocks, full_node)
await wallet_server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
# make wp for lower height
wp = await wpf.get_proof_of_weight(height_to_hash[uint32(800)])
assert wp is not None
# create the node respond with the lighter proof
wp_msg = make_msg(
ProtocolMessageTypes.respond_proof_of_weight,
full_node_protocol.RespondProofOfWeight(wp, wp.recent_chain_data[-1].header_hash),
)
with monkeypatch.context() as m:
f: asyncio.Future[Optional[Message]] = asyncio.Future()
f.set_result(wp_msg)
m.setattr(full_node_api, "request_proof_of_weight", MagicMock(return_value=f))
# create the node respond with the lighter header block
header_block_msg = make_msg(
ProtocolMessageTypes.respond_block_header,
wallet_protocol.RespondBlockHeader(wp.recent_chain_data[-1]),
)
f2: asyncio.Future[Optional[Message]] = asyncio.Future()
f2.set_result(header_block_msg)
m.setattr(full_node_api, "request_block_header", MagicMock(return_value=f2))
# create new fake peak msg
fake_peak_height = uint32(11_000)
fake_peak_weight = uint128(1_000_000_000)
msg = wallet_protocol.NewPeakWallet(
blocks[-1].header_hash, fake_peak_height, fake_peak_weight, uint32(max(blocks[-1].height - 1, uint32(0)))
)
await asyncio.sleep(3)
await wallet_server.start_client(PeerInfo(self_hostname, full_node_server.get_port()), None)
await wallet_node.new_peak_wallet(msg, wallet_server.all_connections.popitem()[1])
await asyncio.sleep(3)
peak = await wallet_node.wallet_state_manager.blockchain.get_peak_block()
assert peak is not None
assert peak.height != fake_peak_height
@pytest.mark.limit_consensus_modes(reason="save time")
@pytest.mark.anyio
async def test_long_sync_untrusted_break(
setup_two_nodes_and_wallet: OldSimulatorsAndWallets,
default_1000_blocks: list[FullBlock],
default_400_blocks: list[FullBlock],
self_hostname: str,
caplog: pytest.LogCaptureFixture,
use_delta_sync: bool,
) -> None:
[trusted_full_node_api, untrusted_full_node_api], [(wallet_node, wallet_server)], _ = setup_two_nodes_and_wallet
trusted_full_node_server = trusted_full_node_api.full_node.server
untrusted_full_node_server = untrusted_full_node_api.full_node.server
wallet_node.config["trusted_peers"] = {trusted_full_node_server.node_id.hex(): None}
wallet_node.config["use_delta_sync"] = use_delta_sync
sync_canceled = False
async def register_for_ph_updates(
self: object,
request: wallet_protocol.RegisterForPhUpdates,
peer: WSChiaConnection,
) -> None:
nonlocal sync_canceled
# Just sleep a long time here to simulate a long-running untrusted sync
try:
await asyncio.sleep(120)
except Exception:
sync_canceled = True
raise
def wallet_syncing() -> bool:
return wallet_node.wallet_state_manager.sync_mode
def check_sync_canceled() -> bool:
return sync_canceled
def synced_to_trusted() -> bool:
return trusted_full_node_server.node_id in wallet_node.synced_peers
def only_trusted_peer() -> bool:
trusted_peers = sum(wallet_node.is_trusted(peer) for peer in wallet_server.all_connections.values())
untrusted_peers = sum(not wallet_node.is_trusted(peer) for peer in wallet_server.all_connections.values())
return trusted_peers == 1 and untrusted_peers == 0
await add_blocks_in_batches(default_400_blocks, trusted_full_node_api.full_node)
await add_blocks_in_batches(default_1000_blocks[:400], untrusted_full_node_api.full_node)
with patch_request_handler(api=untrusted_full_node_api, handler=register_for_ph_updates):
# Connect to the untrusted peer and wait until the long sync started
await wallet_server.start_client(PeerInfo(self_hostname, untrusted_full_node_server.get_port()), None)
await time_out_assert(30, wallet_syncing)
with caplog.at_level(logging.INFO):
# Connect to the trusted peer and make sure the running untrusted long sync gets interrupted via disconnect
await wallet_server.start_client(PeerInfo(self_hostname, trusted_full_node_server.get_port()), None)
await time_out_assert(600, wallet_height_at_least, True, wallet_node, len(default_400_blocks) - 1)
assert time_out_assert(10, synced_to_trusted)
assert untrusted_full_node_server.node_id not in wallet_node.synced_peers
assert "Connected to a synced trusted peer, disconnecting from all untrusted nodes." in caplog.text
# Make sure the sync was interrupted
assert time_out_assert(30, check_sync_canceled)
# And that we only have a trusted peer left
assert time_out_assert(30, only_trusted_peer)
@pytest.mark.anyio
@pytest.mark.parametrize("chain_length", [0, 100])
@pytest.mark.parametrize("fork_point", [500, 1500])
# TODO: todo_v2_plots once we have new test chains, we can probably re-enable this
@pytest.mark.limit_consensus_modes(
allows=[ConsensusMode.PLAIN, ConsensusMode.HARD_FORK_2_0],
reason="after plot-v1 phase-out, the chains aren't valid anymore",
)
async def test_long_reorg_nodes_and_wallet(
chain_length: int,
fork_point: int,
three_nodes: list[FullNodeAPI],
simulator_and_wallet: OldSimulatorsAndWallets,
default_10000_blocks: list[FullBlock],
test_long_reorg_blocks: list[FullBlock],
test_long_reorg_1500_blocks: list[FullBlock],
self_hostname: str,
) -> None:
full_node_1, full_node_2, _ = three_nodes
_, [wallet], _ = simulator_and_wallet
wallet_node = wallet[0]
wallet_server = wallet[1]
# Trusted node sync
wallet_node.config["trusted_peers"] = {full_node_1.server.node_id.hex(): full_node_1.server.node_id.hex()}
if fork_point == 1500:
blocks = default_10000_blocks[: 3600 - chain_length]
else:
blocks = default_10000_blocks[: 1600 - chain_length]
if fork_point == 1500:
reorg_blocks = test_long_reorg_1500_blocks[: 3100 - chain_length]
else:
reorg_blocks = test_long_reorg_blocks[: 1200 - chain_length]
pytest.skip("We rely on the light-blocks test for a 0 forkpoint")
last_blk = blocks[-1]
last_reorg_blk = reorg_blocks[-1]
assert last_blk.header_hash != last_reorg_blk.header_hash
assert last_blk.weight < last_reorg_blk.weight
await wallet_server.start_client(PeerInfo(self_hostname, full_node_1.server.get_port()), None)
assert len(wallet_server.all_connections) == 1
assert len(full_node_1.server.all_connections) == 1
await add_blocks_in_batches(blocks, full_node_1.full_node)
node_1_peak = full_node_1.full_node.blockchain.get_peak()
assert node_1_peak is not None
await time_out_assert(600, wallet_height_at_least, True, wallet_node, node_1_peak.height)
log.info(f"wallet node height is {node_1_peak.height}")
# full node 2 has the reorg-chain
await add_blocks_in_batches(reorg_blocks[:-1], full_node_2.full_node)
await connect_and_get_peer(full_node_1.full_node.server, full_node_2.full_node.server, self_hostname)
# # TODO: There appears to be an issue where the node with the lighter chain
# # fails to initiate the reorg until there's a new block farmed onto the
# # heavier chain.
await full_node_2.full_node.add_block(reorg_blocks[-1])
def check_nodes_in_sync() -> bool:
p1 = full_node_1.full_node.blockchain.get_peak()
p2 = full_node_2.full_node.blockchain.get_peak()
return p1 is not None and p1 == p2
await time_out_assert(600, check_nodes_in_sync)
node_2_peak = full_node_2.full_node.blockchain.get_peak()
assert node_2_peak is not None
print(f"peak: {str(node_2_peak.header_hash)[:6]}")
await time_out_assert(600, wallet_height_at_least, True, wallet_node, node_2_peak.height)
# reorg1_timing = time.monotonic() - start
# we already checked p1==p2
p1 = full_node_2.full_node.blockchain.get_peak()
assert p1 is not None
assert p1.header_hash == last_reorg_blk.header_hash