From d8fa2533727eb6554b14e5bfb56f614cb8620399 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Wed, 13 Apr 2022 17:11:36 -0700 Subject: [PATCH 1/8] Resend transactions --- chia/wallet/wallet_node.py | 4 +++- chia/wallet/wallet_state_manager.py | 4 ++++ chia/wallet/wallet_transaction_store.py | 25 +++++++++++++++++++++++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index f417e58d7a4a..a6678de32c04 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -1044,7 +1044,9 @@ async def new_peak_wallet(self, new_peak: wallet_protocol.NewPeakWallet, peer: W if peer.peer_node_id in self.synced_peers: await self.wallet_state_manager.blockchain.set_finished_sync_up_to(new_peak.height) - await self.wallet_state_manager.new_peak(new_peak) + + async with self.wallet_state_manager.lock: + await self.wallet_state_manager.new_peak(new_peak) async def wallet_short_sync_backtrack(self, header_block: HeaderBlock, peer: WSChiaConnection) -> int: assert self.wallet_state_manager is not None diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 84d31c2af203..006df711fee0 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1212,6 +1212,10 @@ async def new_peak(self, peak: wallet_protocol.NewPeakWallet): for wallet_id, wallet in self.wallets.items(): if wallet.type() == uint8(WalletType.POOLING_WALLET): await wallet.new_peak(peak.height) + current_time = int(time.time()) + + if self.tx_store.last_global_tx_resend_time < current_time - self.tx_store.global_tx_resend_timeout_secs: + self.tx_pending_changed() async def add_interested_puzzle_hashes( self, puzzle_hashes: List[bytes32], wallet_ids: List[int], in_transaction: bool = False diff --git a/chia/wallet/wallet_transaction_store.py b/chia/wallet/wallet_transaction_store.py index 6d9b91d2301b..698d2dc59ef9 100644 --- a/chia/wallet/wallet_transaction_store.py +++ b/chia/wallet/wallet_transaction_store.py @@ -1,3 +1,4 @@ +import dataclasses import time from typing import Dict, List, Optional, Tuple @@ -13,6 +14,15 @@ from chia.wallet.util.transaction_type import TransactionType +def filter_ok_mempool_status(sent_to: List[Tuple[str, uint8, Optional[str]]]) -> List[Tuple[str, uint8, Optional[str]]]: + """Remove SUCCESS and PENDING status records from a TransactionRecord sent_to field""" + new_sent_to = [] + for peer, status, err in sent_to: + if status == MempoolInclusionStatus.FAILED.value: + new_sent_to.append((peer, status, err)) + return new_sent_to + + class WalletTransactionStore: """ WalletTransactionStore stores transaction history for the wallet. @@ -23,6 +33,8 @@ class WalletTransactionStore: tx_record_cache: Dict[bytes32, TransactionRecord] tx_submitted: Dict[bytes32, Tuple[int, int]] # tx_id: [time submitted: count] unconfirmed_for_wallet: Dict[int, Dict[bytes32, TransactionRecord]] + last_global_tx_resend_time: int # Epoch time in seconds + global_tx_resend_timeout_secs: int # Duration in seconds @classmethod async def create(cls, db_wrapper: DBWrapper): @@ -79,6 +91,8 @@ async def create(cls, db_wrapper: DBWrapper): self.tx_record_cache = {} self.tx_submitted = {} self.unconfirmed_for_wallet = {} + self.last_global_tx_resend_time = int(time.time()) + self.global_tx_resend_timeout_secs = 60 * 60 await self.rebuild_tx_cache() return self @@ -279,7 +293,7 @@ async def get_transaction_record(self, tx_id: bytes32) -> Optional[TransactionRe async def get_not_sent(self) -> List[TransactionRecord]: """ - Returns the list of transaction that have not been received by full node yet. + Returns the list of transactions that have not been received by full node yet. """ current_time = int(time.time()) cursor = await self.db_connection.execute( @@ -291,7 +305,14 @@ async def get_not_sent(self) -> List[TransactionRecord]: records = [] for row in rows: record = TransactionRecord.from_bytes(row[0]) - if record.name in self.tx_submitted: + if self.last_global_tx_resend_time < current_time - self.global_tx_resend_timeout_secs: + # Reset the "sent" state for peers that have replied about this transaction. Retain errors. + record = dataclasses.replace(record, sent=1, sent_to=filter_ok_mempool_status(record.sent_to)) + await self.add_transaction_record(record, False) + self.tx_submitted[record.name] = current_time, 1 + records.append(record) + self.last_global_tx_resend_time = current_time + elif record.name in self.tx_submitted: time_submitted, count = self.tx_submitted[record.name] if time_submitted < current_time - (60 * 10): records.append(record) From d2a6ed1e4e667b1d5269eea6f376ea6d7e2768cf Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Fri, 15 Apr 2022 09:14:20 -0700 Subject: [PATCH 2/8] Don't recheck transactions more frequently than timeout --- chia/wallet/wallet_transaction_store.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/chia/wallet/wallet_transaction_store.py b/chia/wallet/wallet_transaction_store.py index 698d2dc59ef9..a2adaff573ed 100644 --- a/chia/wallet/wallet_transaction_store.py +++ b/chia/wallet/wallet_transaction_store.py @@ -303,15 +303,19 @@ async def get_not_sent(self) -> List[TransactionRecord]: rows = await cursor.fetchall() await cursor.close() records = [] + retry_accepted = False + if self.last_global_tx_resend_time < current_time - self.global_tx_resend_timeout_secs: + retry_accepted = True + self.last_global_tx_resend_time = current_time + for row in rows: record = TransactionRecord.from_bytes(row[0]) - if self.last_global_tx_resend_time < current_time - self.global_tx_resend_timeout_secs: + if retry_accepted: # Reset the "sent" state for peers that have replied about this transaction. Retain errors. record = dataclasses.replace(record, sent=1, sent_to=filter_ok_mempool_status(record.sent_to)) await self.add_transaction_record(record, False) self.tx_submitted[record.name] = current_time, 1 records.append(record) - self.last_global_tx_resend_time = current_time elif record.name in self.tx_submitted: time_submitted, count = self.tx_submitted[record.name] if time_submitted < current_time - (60 * 10): From b2766ef471bae920256e4151153420d09d694a9e Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Fri, 15 Apr 2022 12:40:28 -0700 Subject: [PATCH 3/8] Add wallet resend parameter to config, move timeout code out of tx store, but close to call site --- chia/util/initial-config.yaml | 3 +++ chia/wallet/wallet_node.py | 14 +++++++++++++- chia/wallet/wallet_state_manager.py | 2 +- chia/wallet/wallet_transaction_store.py | 16 ++++++---------- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/chia/util/initial-config.yaml b/chia/util/initial-config.yaml index ef6aa9874c35..ffafa9bd52e0 100644 --- a/chia/util/initial-config.yaml +++ b/chia/util/initial-config.yaml @@ -542,3 +542,6 @@ wallet: # if an unknown CAT belonging to us is seen, a wallet will be automatically created # the user accepts the risk/responsibility of verifying the authenticity and origin of unknown CATs automatically_add_unknown_cats: False + + # Interval to resend unconfirmed transactions, even if previously accepted into Mempool + tx_resend_timeout_secs: 1800 diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index a6678de32c04..bea9834426ac 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -141,6 +141,8 @@ def __init__( self.validation_semaphore = None self.local_node_synced = False self.LONG_SYNC_THRESHOLD = 200 + self.last_wallet_tx_resend_time: int = 0 + self.wallet_tx_resend_timeout_secs: int = 60 async def ensure_keychain_proxy(self) -> KeychainProxy: if self.keychain_proxy is None: @@ -230,6 +232,9 @@ async def _start( if self.state_changed_callback is not None: self.wallet_state_manager.set_callback(self.state_changed_callback) + self.last_wallet_tx_resend_time = int(time.time()) + if "tx_resend_timeout_secs" in self.config: + self.wallet_tx_resend_timeout_secs = self.config["tx_resend_timeout_secs"] self.wallet_state_manager.set_pending_callback(self._pending_tx_handler) self._shut_down = False self._process_new_subscriptions_task = asyncio.create_task(self._process_new_subscriptions()) @@ -332,7 +337,14 @@ async def _messages_to_resend(self) -> List[Tuple[Message, Set[bytes32]]]: return [] messages: List[Tuple[Message, Set[bytes32]]] = [] - records: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_not_sent() + current_time = int(time.time()) + retry_accepted_txs = False + if self.last_wallet_tx_resend_time < current_time - self.wallet_tx_resend_timeout_secs: + self.last_wallet_tx_resend_time = current_time + retry_accepted_txs = True + records: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_not_sent( + include_accepted_txs=retry_accepted_txs + ) for record in records: if record.spend_bundle is None: diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 006df711fee0..a7497f289851 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1214,7 +1214,7 @@ async def new_peak(self, peak: wallet_protocol.NewPeakWallet): await wallet.new_peak(peak.height) current_time = int(time.time()) - if self.tx_store.last_global_tx_resend_time < current_time - self.tx_store.global_tx_resend_timeout_secs: + if self.wallet_node.last_wallet_tx_resend_time < current_time - self.wallet_node.wallet_tx_resend_timeout_secs: self.tx_pending_changed() async def add_interested_puzzle_hashes( diff --git a/chia/wallet/wallet_transaction_store.py b/chia/wallet/wallet_transaction_store.py index a2adaff573ed..b6d8b36da1d1 100644 --- a/chia/wallet/wallet_transaction_store.py +++ b/chia/wallet/wallet_transaction_store.py @@ -33,8 +33,8 @@ class WalletTransactionStore: tx_record_cache: Dict[bytes32, TransactionRecord] tx_submitted: Dict[bytes32, Tuple[int, int]] # tx_id: [time submitted: count] unconfirmed_for_wallet: Dict[int, Dict[bytes32, TransactionRecord]] - last_global_tx_resend_time: int # Epoch time in seconds - global_tx_resend_timeout_secs: int # Duration in seconds + last_wallet_tx_resend_time: int # Epoch time in seconds + wallet_tx_resend_timeout_secs: int # Duration in seconds @classmethod async def create(cls, db_wrapper: DBWrapper): @@ -91,8 +91,8 @@ async def create(cls, db_wrapper: DBWrapper): self.tx_record_cache = {} self.tx_submitted = {} self.unconfirmed_for_wallet = {} - self.last_global_tx_resend_time = int(time.time()) - self.global_tx_resend_timeout_secs = 60 * 60 + self.last_wallet_tx_resend_time = int(time.time()) + self.wallet_tx_resend_timeout_secs = 60 * 60 await self.rebuild_tx_cache() return self @@ -291,7 +291,7 @@ async def get_transaction_record(self, tx_id: bytes32) -> Optional[TransactionRe return record return None - async def get_not_sent(self) -> List[TransactionRecord]: + async def get_not_sent(self, *, include_accepted_txs=False) -> List[TransactionRecord]: """ Returns the list of transactions that have not been received by full node yet. """ @@ -303,14 +303,10 @@ async def get_not_sent(self) -> List[TransactionRecord]: rows = await cursor.fetchall() await cursor.close() records = [] - retry_accepted = False - if self.last_global_tx_resend_time < current_time - self.global_tx_resend_timeout_secs: - retry_accepted = True - self.last_global_tx_resend_time = current_time for row in rows: record = TransactionRecord.from_bytes(row[0]) - if retry_accepted: + if include_accepted_txs: # Reset the "sent" state for peers that have replied about this transaction. Retain errors. record = dataclasses.replace(record, sent=1, sent_to=filter_ok_mempool_status(record.sent_to)) await self.add_transaction_record(record, False) From f0ae12c99458ac35c4926cf043decf189c3fda43 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Wed, 4 May 2022 10:42:50 -0700 Subject: [PATCH 4/8] Add a test for wallet transaction resend --- chia/util/config.py | 10 ++++++++++ chia/wallet/wallet_node.py | 5 ++--- chia/wallet/wallet_transaction_store.py | 2 -- tests/block_tools.py | 15 +++++++-------- tests/conftest.py | 13 +++++++++++-- tests/pools/test_pool_rpc.py | 9 ++++----- tests/setup_nodes.py | 7 ++++--- 7 files changed, 38 insertions(+), 23 deletions(-) diff --git a/chia/util/config.py b/chia/util/config.py index 1405cacaadd0..199f967be121 100644 --- a/chia/util/config.py +++ b/chia/util/config.py @@ -1,5 +1,6 @@ import argparse import contextlib +import copy import logging import os import shutil @@ -262,3 +263,12 @@ def process_config_start_method( log.info(f"Selected multiprocessing start method: {choice}") return processed_method + + +def override_config(config: Dict[str, Any], config_overrides: Optional[Dict[str, Any]]): + new_config = copy.deepcopy(config) + if config_overrides is None: + return new_config + for k, v in config_overrides.items(): + add_property(new_config, k, v) + return new_config diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index bea9834426ac..a545a892b4e2 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -142,7 +142,7 @@ def __init__( self.local_node_synced = False self.LONG_SYNC_THRESHOLD = 200 self.last_wallet_tx_resend_time: int = 0 - self.wallet_tx_resend_timeout_secs: int = 60 + self.wallet_tx_resend_timeout_secs: int = 1800 # Duration in seconds async def ensure_keychain_proxy(self) -> KeychainProxy: if self.keychain_proxy is None: @@ -233,8 +233,7 @@ async def _start( self.wallet_state_manager.set_callback(self.state_changed_callback) self.last_wallet_tx_resend_time = int(time.time()) - if "tx_resend_timeout_secs" in self.config: - self.wallet_tx_resend_timeout_secs = self.config["tx_resend_timeout_secs"] + self.wallet_tx_resend_timeout_secs = self.config.get("tx_resend_timeout_secs", 60 * 60) self.wallet_state_manager.set_pending_callback(self._pending_tx_handler) self._shut_down = False self._process_new_subscriptions_task = asyncio.create_task(self._process_new_subscriptions()) diff --git a/chia/wallet/wallet_transaction_store.py b/chia/wallet/wallet_transaction_store.py index b6d8b36da1d1..02b330f3117f 100644 --- a/chia/wallet/wallet_transaction_store.py +++ b/chia/wallet/wallet_transaction_store.py @@ -34,7 +34,6 @@ class WalletTransactionStore: tx_submitted: Dict[bytes32, Tuple[int, int]] # tx_id: [time submitted: count] unconfirmed_for_wallet: Dict[int, Dict[bytes32, TransactionRecord]] last_wallet_tx_resend_time: int # Epoch time in seconds - wallet_tx_resend_timeout_secs: int # Duration in seconds @classmethod async def create(cls, db_wrapper: DBWrapper): @@ -92,7 +91,6 @@ async def create(cls, db_wrapper: DBWrapper): self.tx_submitted = {} self.unconfirmed_for_wallet = {} self.last_wallet_tx_resend_time = int(time.time()) - self.wallet_tx_resend_timeout_secs = 60 * 60 await self.rebuild_tx_cache() return self diff --git a/tests/block_tools.py b/tests/block_tools.py index 0cb9e541a3f7..d384a5ea25f1 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -76,7 +76,7 @@ from chia.types.unfinished_block import UnfinishedBlock from chia.util.bech32m import encode_puzzle_hash from chia.util.block_cache import BlockCache -from chia.util.config import load_config, lock_config, save_config +from chia.util.config import load_config, lock_config, save_config, override_config from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64, uint128 @@ -142,6 +142,7 @@ def __init__( root_path: Optional[Path] = None, const_dict=None, keychain: Optional[Keychain] = None, + config_overrides: Optional[Dict] = None, ): self._block_cache_header = bytes32([0] * 32) @@ -171,6 +172,7 @@ def __init__( # some tests start the daemon, make sure it's on a free port self._config["daemon_port"] = find_available_listen_port("BlockTools daemon") + self._config = override_config(self._config, config_overrides) with lock_config(self.root_path, "config.yaml"): save_config(self.root_path, "config.yaml", self._config) @@ -2036,16 +2038,12 @@ async def create_block_tools_async( root_path: Optional[Path] = None, const_dict=None, keychain: Optional[Keychain] = None, + config_overrides: Optional[Dict] = None, ) -> BlockTools: global create_block_tools_async_count create_block_tools_async_count += 1 print(f" create_block_tools_async called {create_block_tools_async_count} times") - bt = BlockTools( - constants, - root_path, - const_dict, - keychain, - ) + bt = BlockTools(constants, root_path, const_dict, keychain, config_overrides=config_overrides) await bt.setup_keys() await bt.setup_plots() @@ -2057,11 +2055,12 @@ def create_block_tools( root_path: Optional[Path] = None, const_dict=None, keychain: Optional[Keychain] = None, + config_overrides: Optional[Dict] = None, ) -> BlockTools: global create_block_tools_count create_block_tools_count += 1 print(f" create_block_tools called {create_block_tools_count} times") - bt = BlockTools(constants, root_path, const_dict, keychain) + bt = BlockTools(constants, root_path, const_dict, keychain, config_overrides=config_overrides) asyncio.get_event_loop().run_until_complete(bt.setup_keys()) asyncio.get_event_loop().run_until_complete(bt.setup_plots()) diff --git a/tests/conftest.py b/tests/conftest.py index cad1186f7e48..82549d23fd16 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,9 +7,10 @@ import pytest_asyncio import tempfile +from chia.full_node.full_node_api import FullNodeAPI from tests.setup_nodes import setup_node_and_wallet, setup_n_nodes, setup_two_nodes from pathlib import Path -from typing import Any, AsyncIterator, Dict, List, Tuple +from typing import Any, AsyncIterator, Dict, List, Tuple, AsyncGenerator from chia.server.start_service import Service # Set spawn after stdlib imports, but before other imports @@ -306,7 +307,15 @@ async def three_sim_two_wallets(): @pytest_asyncio.fixture(scope="function") async def setup_two_nodes_and_wallet(): - async for _ in setup_simulators_and_wallets(2, 1, {}, db_version=2): # xxx + async for _ in setup_simulators_and_wallets(2, 1, {}, db_version=2): + yield _ + + +@pytest_asyncio.fixture(scope="function") +async def setup_two_nodes_and_wallet_fast_retry(): + async for _ in setup_simulators_and_wallets( + 1, 1, {}, config_overrides={"wallet.tx_resend_timeout_secs": 1}, db_version=2 + ): yield _ diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py index 77d6da1933bc..7a3a0fe37e79 100644 --- a/tests/pools/test_pool_rpc.py +++ b/tests/pools/test_pool_rpc.py @@ -82,12 +82,11 @@ async def __aexit__(self, exc_type, exc_value, exc_traceback): self._tmpdir.__exit__(None, None, None) -async def wallet_is_synced(wallet_node: WalletNode, full_node_api): +async def wallet_is_synced(wallet_node: WalletNode, full_node_api) -> bool: assert wallet_node.wallet_state_manager is not None - return ( - await wallet_node.wallet_state_manager.blockchain.get_finished_sync_up_to() - == full_node_api.full_node.blockchain.get_peak_height() - ) + wallet_height = await wallet_node.wallet_state_manager.blockchain.get_finished_sync_up_to() + full_node_height = full_node_api.full_node.blockchain.get_peak_height() + return wallet_height == full_node_height PREFARMED_BLOCKS = 4 diff --git a/tests/setup_nodes.py b/tests/setup_nodes.py index 03a97c386d67..9ec6890f987b 100644 --- a/tests/setup_nodes.py +++ b/tests/setup_nodes.py @@ -1,7 +1,7 @@ import asyncio import logging from secrets import token_bytes -from typing import AsyncIterator, Dict, List, Tuple +from typing import AsyncIterator, Dict, List, Tuple, Optional from pathlib import Path from chia.consensus.constants import ConsensusConstants @@ -234,6 +234,7 @@ async def setup_simulators_and_wallets( key_seed=None, initial_num_public_keys=5, db_version=1, + config_overrides: Optional[Dict] = None, ): with TempKeyring(populate=True) as keychain1, TempKeyring(populate=True) as keychain2: simulators: List[FullNodeAPI] = [] @@ -246,7 +247,7 @@ async def setup_simulators_and_wallets( rpc_port = find_available_listen_port(f"node{index} rpc") db_name = f"blockchain_test_{port}.db" bt_tools = await create_block_tools_async( - consensus_constants, const_dict=dic, keychain=keychain1 + consensus_constants, const_dict=dic, keychain=keychain1, config_overrides=config_overrides ) # block tools modifies constants sim = setup_full_node( bt_tools.constants, @@ -269,7 +270,7 @@ async def setup_simulators_and_wallets( port = find_available_listen_port(f"wallet{index}") rpc_port = find_available_listen_port(f"wallet{index} rpc") bt_tools = await create_block_tools_async( - consensus_constants, const_dict=dic, keychain=keychain2 + consensus_constants, const_dict=dic, keychain=keychain2, config_overrides=config_overrides ) # block tools modifies constants wlt = setup_wallet_node( bt_tools.config["self_hostname"], From 6c95af43a6a3428c4d8bafff2c1a402d74371d25 Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Wed, 11 May 2022 10:24:05 -0700 Subject: [PATCH 5/8] Add test for wallet retry --- tests/wallet/test_wallet_retry.py | 140 ++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 tests/wallet/test_wallet_retry.py diff --git a/tests/wallet/test_wallet_retry.py b/tests/wallet/test_wallet_retry.py new file mode 100644 index 000000000000..46e9fe68dd42 --- /dev/null +++ b/tests/wallet/test_wallet_retry.py @@ -0,0 +1,140 @@ +import time +from typing import Tuple, List, Any + +import pytest + +from chia.full_node.full_node_api import FullNodeAPI +from chia.protocols import full_node_protocol +from chia.simulator.full_node_simulator import FullNodeSimulator +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.peer_info import PeerInfo +from chia.types.spend_bundle import SpendBundle +from chia.util.ints import uint64, uint16, uint32 +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.transaction_type import TransactionType +from chia.wallet.wallet_node import WalletNode +from tests.block_tools import BlockTools +from tests.core.full_node.test_mempool import generate_test_spend_bundle +from tests.core.node_height import node_height_at_least +from tests.pools.test_pool_rpc import wallet_is_synced, farm_blocks +from tests.time_out_assert import time_out_assert, time_out_assert_custom_interval +from tests.wallet_tools import WalletTool + + +@pytest.fixture(scope="module") +def wallet_a(bt: BlockTools) -> WalletTool: + return bt.get_pool_wallet_tool() + + +def assert_sb_in_pool(node: FullNodeAPI, sb: SpendBundle) -> None: + assert sb == node.full_node.mempool_manager.get_spendbundle(sb.name()) + + +def assert_sb_not_in_pool(node: FullNodeAPI, sb: SpendBundle) -> None: + assert node.full_node.mempool_manager.get_spendbundle(sb.name()) is None + assert not node.full_node.mempool_manager.seen(sb.name()) + + +def evict_from_pool(node: FullNodeAPI, sb: SpendBundle) -> None: + mempool_item = node.full_node.mempool_manager.mempool.spends[sb.name()] + node.full_node.mempool_manager.mempool.remove_from_pool(mempool_item) + node.full_node.mempool_manager.remove_seen(sb.name()) + + +@pytest.mark.asyncio +async def test_wallet_tx_retry( + bt: BlockTools, + setup_node_and_wallet_fast_retry: Tuple[List[FullNodeSimulator], List[Tuple[Any, Any]]], + wallet_a: WalletTool, + self_hostname: str, +) -> None: + wait_secs = 1000 + reward_ph = wallet_a.get_new_puzzlehash() + nodes, wallets = setup_node_and_wallet_fast_retry + server_1 = nodes[0].full_node.server + + full_node_1: FullNodeSimulator = nodes[0] + + wallet_node_1: WalletNode = wallets[0][0] + wallet_node_1.config["tx_resend_timeout_secs"] = 5 + wallet_server_1 = wallets[0][1] + assert wallet_node_1.wallet_state_manager is not None + wallet_1 = wallet_node_1.wallet_state_manager.main_wallet + + await wallet_server_1.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) + + blocks = await full_node_1.get_all_full_blocks() + start_height = blocks[-1].height if len(blocks) > 0 else -1 + blocks = bt.get_consecutive_blocks( + 3, + block_list_input=blocks, + guarantee_transaction_block=True, + farmer_reward_puzzle_hash=reward_ph, + pool_reward_puzzle_hash=reward_ph, + ) + + for block in blocks: + await full_node_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) + await time_out_assert(60, node_height_at_least, True, full_node_1, start_height + 3) + current_height = 3 + + coins = iter(blocks[-1].get_included_reward_coins()) + coin1 = next(coins) + + sb1 = generate_test_spend_bundle(wallet_a, coin1, new_puzzle_hash=bytes32(b"A" * 32)) + amount_out = sum(_.amount for _ in sb1.additions()) + tx = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=bytes32(b"A" * 32), + amount=uint64(amount_out), + fee_amount=uint64(sb1.fees()), + confirmed=False, + sent=uint32(0), + spend_bundle=sb1, + additions=sb1.additions(), + removals=sb1.removals(), + wallet_id=wallet_node_1.wallet_state_manager.main_wallet.id(), + sent_to=[], + memos=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=sb1.name(), + ) + + await wallet_node_1.wallet_state_manager.add_pending_transaction(tx) + + async def sb_in_mempool() -> bool: + return full_node_1.full_node.mempool_manager.get_spendbundle(sb1.name()) == sb1 + + # SpendBundle is accepted by peer + await time_out_assert(wait_secs, sb_in_mempool) + + async def wallet_synced() -> bool: + return await wallet_is_synced(wallet_node_1, full_node_1) + + # Wait for wallet to catch up + await time_out_assert(wait_secs, wallet_synced) + + # Evict SpendBundle from peer + evict_from_pool(full_node_1, sb1) + assert_sb_not_in_pool(full_node_1, sb1) + + print(f"mempool spends: {full_node_1.full_node.mempool_manager.mempool.spends}") + + our_ph = await wallet_1.get_new_puzzlehash() + current_height += await farm_blocks(full_node_1, our_ph, 3) + await time_out_assert(wait_secs, wallet_synced) + + async def check_transaction_in_mempool_or_confirmed(transaction: TransactionRecord) -> bool: + assert wallet_node_1.wallet_state_manager is not None + txn = await wallet_node_1.wallet_state_manager.get_transaction(transaction.name) + assert txn is not None + sb = txn.spend_bundle + assert sb is not None + full_node_sb = full_node_1.full_node.mempool_manager.get_spendbundle(sb.name()) + in_mempool: bool = full_node_sb == sb + return txn.confirmed or in_mempool + + # Check that wallet resent the unconfirmed SpendBundle + await time_out_assert_custom_interval(wait_secs, 1, check_transaction_in_mempool_or_confirmed, True, tx) From 3837d2759e9378c5468533730a1b7d070621bd3c Mon Sep 17 00:00:00 2001 From: Earle Lowe Date: Thu, 12 May 2022 09:29:15 -0700 Subject: [PATCH 6/8] isort new files for precommit and update workflows --- .github/workflows/build-test-macos-wallet.yml | 2 +- .github/workflows/build-test-ubuntu-wallet.yml | 2 +- tests/wallet/test_wallet_retry.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index 6bc89ae0be14..49263caf33ec 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -93,7 +93,7 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" tests/wallet/test_bech32m.py tests/wallet/test_chialisp.py tests/wallet/test_puzzle_store.py tests/wallet/test_singleton.py tests/wallet/test_singleton_lifecycle.py tests/wallet/test_singleton_lifecycle_fast.py tests/wallet/test_taproot.py tests/wallet/test_wallet.py tests/wallet/test_wallet_blockchain.py tests/wallet/test_wallet_interested_store.py tests/wallet/test_wallet_key_val_store.py tests/wallet/test_wallet_store.py tests/wallet/test_wallet_user_store.py + venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" tests/wallet/test_bech32m.py tests/wallet/test_chialisp.py tests/wallet/test_puzzle_store.py tests/wallet/test_singleton.py tests/wallet/test_singleton_lifecycle.py tests/wallet/test_singleton_lifecycle_fast.py tests/wallet/test_taproot.py tests/wallet/test_wallet.py tests/wallet/test_wallet_blockchain.py tests/wallet/test_wallet_interested_store.py tests/wallet/test_wallet_key_val_store.py tests/wallet/test_wallet_retry.py tests/wallet/test_wallet_store.py tests/wallet/test_wallet_user_store.py - name: Process coverage data run: | diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index 35fe079239ec..55a499718b69 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -92,7 +92,7 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" -p no:monitor tests/wallet/test_bech32m.py tests/wallet/test_chialisp.py tests/wallet/test_puzzle_store.py tests/wallet/test_singleton.py tests/wallet/test_singleton_lifecycle.py tests/wallet/test_singleton_lifecycle_fast.py tests/wallet/test_taproot.py tests/wallet/test_wallet.py tests/wallet/test_wallet_blockchain.py tests/wallet/test_wallet_interested_store.py tests/wallet/test_wallet_key_val_store.py tests/wallet/test_wallet_store.py tests/wallet/test_wallet_user_store.py + venv/bin/coverage run --rcfile=.coveragerc --module pytest --durations=10 -n 4 -m "not benchmark" -p no:monitor tests/wallet/test_bech32m.py tests/wallet/test_chialisp.py tests/wallet/test_puzzle_store.py tests/wallet/test_singleton.py tests/wallet/test_singleton_lifecycle.py tests/wallet/test_singleton_lifecycle_fast.py tests/wallet/test_taproot.py tests/wallet/test_wallet.py tests/wallet/test_wallet_blockchain.py tests/wallet/test_wallet_interested_store.py tests/wallet/test_wallet_key_val_store.py tests/wallet/test_wallet_retry.py tests/wallet/test_wallet_store.py tests/wallet/test_wallet_user_store.py - name: Process coverage data run: | diff --git a/tests/wallet/test_wallet_retry.py b/tests/wallet/test_wallet_retry.py index 46e9fe68dd42..df17c8eb03f3 100644 --- a/tests/wallet/test_wallet_retry.py +++ b/tests/wallet/test_wallet_retry.py @@ -1,5 +1,5 @@ import time -from typing import Tuple, List, Any +from typing import Any, List, Tuple import pytest @@ -9,14 +9,14 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.peer_info import PeerInfo from chia.types.spend_bundle import SpendBundle -from chia.util.ints import uint64, uint16, uint32 +from chia.util.ints import uint16, uint32, uint64 from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.transaction_type import TransactionType from chia.wallet.wallet_node import WalletNode from tests.block_tools import BlockTools from tests.core.full_node.test_mempool import generate_test_spend_bundle from tests.core.node_height import node_height_at_least -from tests.pools.test_pool_rpc import wallet_is_synced, farm_blocks +from tests.pools.test_pool_rpc import farm_blocks, wallet_is_synced from tests.time_out_assert import time_out_assert, time_out_assert_custom_interval from tests.wallet_tools import WalletTool From 24fe95560f04188d0879a6b68515bdacfe2e4692 Mon Sep 17 00:00:00 2001 From: Earle Lowe Date: Thu, 12 May 2022 10:03:08 -0700 Subject: [PATCH 7/8] Use correct fixture name --- tests/wallet/test_wallet_retry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/wallet/test_wallet_retry.py b/tests/wallet/test_wallet_retry.py index df17c8eb03f3..c23f8535973c 100644 --- a/tests/wallet/test_wallet_retry.py +++ b/tests/wallet/test_wallet_retry.py @@ -44,13 +44,13 @@ def evict_from_pool(node: FullNodeAPI, sb: SpendBundle) -> None: @pytest.mark.asyncio async def test_wallet_tx_retry( bt: BlockTools, - setup_node_and_wallet_fast_retry: Tuple[List[FullNodeSimulator], List[Tuple[Any, Any]]], + setup_two_nodes_and_wallet_fast_retry: Tuple[List[FullNodeSimulator], List[Tuple[Any, Any]]], wallet_a: WalletTool, self_hostname: str, ) -> None: wait_secs = 1000 reward_ph = wallet_a.get_new_puzzlehash() - nodes, wallets = setup_node_and_wallet_fast_retry + nodes, wallets = setup_two_nodes_and_wallet_fast_retry server_1 = nodes[0].full_node.server full_node_1: FullNodeSimulator = nodes[0] From 645ca3eee0e073021ae1442a46e5f4e03ab03646 Mon Sep 17 00:00:00 2001 From: Earle Lowe Date: Thu, 12 May 2022 15:23:39 -0700 Subject: [PATCH 8/8] LGTM - remove unused import --- tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 82549d23fd16..14a91de3dea9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,6 @@ import pytest_asyncio import tempfile -from chia.full_node.full_node_api import FullNodeAPI from tests.setup_nodes import setup_node_and_wallet, setup_n_nodes, setup_two_nodes from pathlib import Path from typing import Any, AsyncIterator, Dict, List, Tuple, AsyncGenerator